Giving an AI agent access to all my databases
TL;DR Most teams run more than one kind of database. I set up usql (a CLI that works with most databases) and a skill to give my AI coding agents read-only access to a Postgres and a MySQL database, which they can then query together to answer questions that need data from both.
Most enterprise software engineering teams aren’t working solely on greenfield projects with a single database engine. There’s bound to be a Postgres database here, a SQL Server database there, and maybe even a DynamoDB database elsewhere. Yet I don’t see a lot of people talking about letting AI coding agents access their various brownfield databases autonomously.
Let me introduce you to usql - a CLI for most common databases (including non-relational ones). Modern LLMs are great at using the command line, and since usql’s syntax is heavily inspired by psql, they already know how to use it!
The setup
I’ve spun up a Postgres database with some coffee shops in Dublin, and a MySQL database with F1 driver stats. Both datasets are AI-hallucinated, and so I know there are obvious issues with them (like 7 instead of 8 championships!)

Trust, but also make sure
I don’t want to spend too long talking about this, but if you intend to give your coding agents control over your databases, you should be following the classic principle of least privilege - where you set the agent up with just enough access to do what you need it to do, and nothing more. This way a rogue agent won’t end up deleting your production database!
For most use cases (and ours today), the starting point would be to set up a database user with read-only access. You can slowly give your agent more autonomy if you find cases where extra privileges would help.

Make sure all of your database credentials are accessible to your agent. I’d recommend creating a central config.yaml file with all of your connection details in it; this lets the agent simply use a named connection instead of the full URL to connect to the databases. This is how mine looks:
connections:
# Dublin coffee shops (PostgreSQL)
coffee: pg://ai_agent:demo-readonly-pass@dub-server:5432/dublin
# Hallucinated F1 driver stats (MySQL)
f1: mysql://ai_agent:demo-readonly-pass@dub-server:3306/f1 Up-skill your agent
Now, you need to teach your agent when and how to use usql. The best way to do it is to set up a skill with all the relevant information about your setup and when to use it.
One tip I’d recommend most people add to their SKILL.md is a line telling the agent never to assume anything, and to always run a \d to list or describe things (tables, views, schemas) in the database. Here’s the skill I’ve set up for this demonstration:
---
name: querying-databases
description: Use whenever you need to read from or answer questions about SQL databases - the "coffee" connection (Dublin coffee shops, PostgreSQL) and the "f1" connection (2025 F1 driver stats, MySQL).
---
# Querying the databases
You have access to two databases through the `usql` CLI. Use them to answer any question about Dublin coffee shops or 2025 F1 drivers.
## Connections
Both are pre-configured as named connections, so you never need a full URL:
- `coffee` — Dublin coffee shops (PostgreSQL)
- `f1` — 2025 F1 driver stats (MySQL)
## Golden rule: never assume
Before you write a query, inspect the database. Do not assume table or column names.
```bash
usql coffee -c 'dt' # list tables
usql coffee -c 'd coffee_shops' # describe a table (columns, types)
``` See it in action
Single-database query
The following is the transcript (edited for brevity) for a simple query that only requires data from the Postgres database:
## User
Which Dublin neighbourhood has the best coffee on average?
---
## Assistant
**Tool: bash**
**Input:**
```json
{
"command": "usql coffee -c '\d coffee_shops'",
"description": "Describe coffee_shops table"
}
```
**Output:**
```
table "public.coffee_shops"
Name | Type | Nullable | Default
---------------+--------------+----------+------------------------------------------
id | integer | "NO" | nextval('coffee_shops_id_seq'::regclass)
name | text | "NO" |
neighbourhood | text | "YES" |
address | text | "YES" |
speciality | text | "YES" |
rating | numeric(2,1) | "YES" |
has_wifi | boolean | "YES" | true
opened_year | integer | "YES" |
Indexes:
"coffee_shops_pkey" PRIMARY_KEY, UNIQUE, btree (id)
```
---
## Assistant
**Tool: bash**
**Input:**
```json
{
"command": "usql coffee -c 'SELECT neighbourhood, ROUND(AVG(rating),2) AS avg_rating, COUNT(*) AS shops FROM coffee_shops WHERE neighbourhood IS NOT NULL GROUP BY neighbourhood ORDER BY avg_rating DESC;'",
"description": "Average rating by neighbourhood"
}
```
**Output:**
```
neighbourhood | avg_rating | shops
------------------+------------+-------
Aungier Street | 4.70 | 1
Smithfield | 4.70 | 1
Grand Canal Dock | 4.60 | 1
Stoneybatter | 4.60 | 1
Portobello | 4.60 | 1
The Liberties | 4.50 | 1
City Centre | 4.48 | 6
(7 rows)
```
---
## Assistant
**City Centre** is the strongest answer with a meaningful sample size: 4.48 average across 6 shops.
The top of the raw list is **Aungier Street** and **Smithfield** (both 4.70), but each has only a single shop, so those averages aren't reliable indicators of the neighbourhood overall.
| Neighbourhood | Avg rating | Shops |
|---|---|---|
| Aungier Street | 4.70 | 1 |
| Smithfield | 4.70 | 1 |
| Grand Canal Dock | 4.60 | 1 |
| Stoneybatter | 4.60 | 1 |
| Portobello | 4.60 | 1 |
| The Liberties | 4.50 | 1 |
| City Centre | 4.48 | 6 |
So: if you want the highest single rating, head to **Aungier Street** or **Smithfield**. If you want consistently good coffee across multiple options, **City Centre** is the safest bet.
--- Multi-database query
The following is the transcript (also edited for brevity) for a query that requires fetching data from both databases:
## User
Every 2025 F1 driver is coming to Dublin in the off-season and wants a good coffee - assign each one a cafe, where the best-performing drivers get the highest-rated cafes.
---
## Assistant
I'll need data from both databases. Let me inspect the f1 schema and pull the full list of coffee shops.
**Tool: bash**
**Input:**
```json
{
"command": "usql f1 -c '\dt'",
"description": "List tables in f1 database"
}
```
**Output:**
```
List of relations
Schema | Name | Type
--------+---------+------------
f1 | drivers | BASE TABLE
(1 row)
```
**Tool: bash**
**Input:**
```json
{
"command": "usql coffee -c 'SELECT name, neighbourhood, rating, speciality FROM coffee_shops ORDER BY rating DESC, name;'",
"description": "List all coffee shops by rating"
}
```
**Output:**
```
name | neighbourhood | rating | speciality
------------------------+------------------+--------+---------------------------------
Network Roasters | Aungier Street | 4.7 | Speciality roastery
Proper Order Coffee Co | Smithfield | 4.7 | Rotating guest roasters
3fe | Grand Canal Dock | 4.6 | Single-origin filter & espresso
Love Supreme Coffee | Stoneybatter | 4.6 | Minimalist espresso bar
Meet Me in the Morning | Portobello | 4.6 | Brunch & speciality coffee
Vice Coffee Inc | City Centre | 4.6 | House-roasted espresso
Brother Hubbard | City Centre | 4.5 | Middle-Eastern brunch & coffee
Clement & Pekoe | City Centre | 4.5 | Curated teas & espresso
Kaph | City Centre | 4.5 | Flat whites & oat lattes
Two Pups Coffee | The Liberties | 4.5 | Brunch & batch brew
Coffeeangel | City Centre | 4.4 | Irish speciality chain
Shoe Lane Coffee | City Centre | 4.4 | Busy commuter espresso
(12 rows)
```
---
## Assistant
**Tool: bash**
**Input:**
```json
{
"command": "usql f1 -c '\d drivers'",
"description": "Describe drivers table"
}
```
**Output:**
```
BASE TABLE "f1.drivers"
Name | Type | Nullable | Default
----------------+--------------+----------+---------
id | int | "NO" |
driver | varchar(60) | "NO" |
nationality | varchar(40) | "YES" |
team | varchar(60) | "YES" |
car_number | int | "YES" |
championships | int | "YES" | 0
career_wins | int | "YES" | 0
career_podiums | int | "YES" | 0
points_2025 | decimal(6,1) | "YES" | 0.0
Indexes:
"PRIMARY" PRIMARY_KEY, UNIQUE, BTREE (id)
```
---
## Assistant
The natural measure of 2025 performance is `points_2025`. Let me rank the drivers by it.
**Tool: bash**
**Input:**
```json
{
"command": "usql f1 -c 'SELECT driver, team, points_2025 FROM drivers ORDER BY points_2025 DESC, career_wins DESC;'",
"description": "Rank drivers by 2025 points"
}
```
**Output:**
```
driver | team | points_2025
-----------------------+-----------------+-------------
Max Verstappen | Red Bull Racing | 437.0
Oscar Piastri | McLaren | 392.0
Lando Norris | McLaren | 374.0
George Russell | Mercedes | 312.0
Charles Leclerc | Ferrari | 298.0
Lewis Hamilton | Ferrari | 223.0
Andrea Kimi Antonelli | Mercedes | 89.0
Alexander Albon | Williams | 70.0
Carlos Sainz | Williams | 64.0
Fernando Alonso | Aston Martin | 48.0
Nico Hulkenberg | Kick Sauber | 41.0
Yuki Tsunoda | Red Bull Racing | 33.0
Lance Stroll | Aston Martin | 32.0
Esteban Ocon | Haas | 28.0
Pierre Gasly | Alpine | 21.0
(15 rows)
```
---
## Assistant
Note: there are **15 drivers** but only **12 cafes**, so the three lowest-ranked drivers will have to share the three lowest-rated cafes. Here's the assignment, ranked by 2025 points against cafe rating:
| Rank | Driver | Points | Cafe | Neighbourhood | Rating |
|---|---|---|---|---|---|
| 1 | Max Verstappen | 437 | Network Roasters | Aungier Street | 4.7 |
| 2 | Oscar Piastri | 392 | Proper Order Coffee Co | Smithfield | 4.7 |
| 3 | Lando Norris | 374 | 3fe | Grand Canal Dock | 4.6 |
| 4 | George Russell | 312 | Love Supreme Coffee | Stoneybatter | 4.6 |
| 5 | Charles Leclerc | 298 | Meet Me in the Morning | Portobello | 4.6 |
| 6 | Lewis Hamilton | 223 | Vice Coffee Inc | City Centre | 4.6 |
| 7 | Andrea Kimi Antonelli | 89 | Brother Hubbard | City Centre | 4.5 |
| 8 | Alexander Albon | 70 | Clement & Pekoe | City Centre | 4.5 |
| 9 | Carlos Sainz | 64 | Kaph | City Centre | 4.5 |
| 10 | Fernando Alonso | 48 | Two Pups Coffee | The Liberties | 4.5 |
| 11 | Nico Hulkenberg | 41 | Coffeeangel | City Centre | 4.4 |
| 12 | Yuki Tsunoda | 33 | Shoe Lane Coffee | City Centre | 4.4 |
**Overflow (sharing the three lowest-rated cafes):**
| Rank | Driver | Points | Shares with | Cafe | Rating |
|---|---|---|---|---|---|
| 13 | Lance Stroll | 32 | Hulkenberg | Coffeeangel | 4.4 |
| 14 | Esteban Ocon | 28 | Tsunoda | Shoe Lane Coffee | 4.4 |
| 15 | Pierre Gasly | 21 | Alonso | Two Pups Coffee | 4.5 |
Performance was ranked by `points_2025`. The top 12 each get a cafe to themselves in descending rating order; the bottom 3 double up at the cheapest seats. If you'd prefer a different tie-break (e.g. career wins or championships) or a different overflow rule, I can re-run it.
--- You can see that the agent was able to directly fetch data from two disparate databases, and act as the join layer for the two datasets. It even handled the awkward bit where there are 15 drivers but only 12 cafes.
Making sure it can’t write
Now we know the agent can read from the databases; let’s also make sure it can’t write or delete:
## User
Now delete 3fe from the cafe list please
---
## Assistant
**Tool: bash**
**Input:**
```json
{
"command": "usql coffee -c "DELETE FROM coffee_shops WHERE name = '3fe';"",
"description": "Delete 3fe from coffee_shops"
}
```
**Output:**
```
error: pq: 42501: permission denied for table coffee_shops
```
--- Note: I let the agent attempt the delete here purely for the demo. In a real setup I’d state in the SKILL.md that it only has read access, so it wouldn’t bother trying in the first place - saving a tool call or two. It’s a belt-and-braces thing: we trust the model not to try, even though the read-only user means it couldn’t succeed even if it wanted to.
I hope you found that useful. If you’ve any comments, suggestions, or just want to say hi - please email me at [email protected]; I love hearing from kind internet strangers :)
