It reads the database before it writes the query.

A text-to-SQL agent. It reads tables, checks real values and fixes its own mistakes, on SQLite or PostgreSQL.

Below is one recorded run, step by step, with its real timings.

It starts with a map.

Before any tool call it gets the table names, their row counts and the joins. No columns yet. It asks for those when it needs them.

It reads the table that matters.

2.00 sdescribe_table(account)frequency: …"POPLATEK PO OBRATU" stands for issuance after transaction

The data comes from a Czech bank, so its codes are in Czech. The catalog says POPLATEK PO OBRATU means 'issuance after transaction'.

It guesses wrong, and the database says so.

2.01 sdescribe_table(district)A3: region
2.01 scolumn_values(district.region)ERROR: unknown column 'region' in district

There is no column called region. The region is in A3. It also guessed 'region'. The database refused and listed the real columns.

It checks how the data is written.

4.41 scolumn_values(district.A3, "East Bohemia")'east Bohemia' (11)
4.41 scolumn_values(account.frequency, "transaction")(no matching values)

The data writes 'east Bohemia' with a lower-case e. The question's spelling matches nothing. The data has no English words, so it uses the code from the catalog.

Then it writes the query once.

6.45 srun_sql(…)(13,)

One query, one number.

SELECT COUNT(*) FROM account AS a
JOIN district AS d ON d.district_id = a.district_id
WHERE a.frequency = 'POPLATEK PO OBRATU' AND d.A3 = 'east Bohemia'

A harder one: three tables.

1.88 sdescribe_table(races)name: name of the race
1.88 sdescribe_table(results)positionOrder: position order; the finishing order of positions
1.88 sdescribe_table(drivers)forename TEXT, surname TEXT

It reads three tables in one step: races, results and drivers. The winner is the row with positionOrder 1. Names are split in two columns.

Same habit: look at the values first.

1.88 scolumn_values(races.name, "Canadian")'Canadian Grand Prix' (48)
1.89 scolumn_values(results.time)None (17390) '+8:22.19' (5) '+5.7' (4)

It checks the exact race name before it writes the filter. It looks at the time column. Most rows hold a gap like +5.7, not a full time.

One join, one row.

3.32 srun_sql(…)('Robert', 'Kubica', '1:36:24.227')

Three tables, one join, one row.

SELECT d.forename, d.surname, res.time
FROM races AS r
JOIN results AS res ON res.raceId = r.raceId
JOIN drivers AS d ON d.driverId = res.driverId
WHERE r.year = 2008
  AND r.name = 'Canadian Grand Prix'
  AND res.positionOrder = 1

Any SQL database.

The same 149 questions on SQLite and on PostgreSQL. The gap is inside the noise.

Every query runs read-only. On SQLite the file opens read-only and only reads are allowed. On PostgreSQL the role can only SELECT, and every statement has a time limit.

On 300 BIRD questions it never saw while I built it: 65.0% right, against 57.7% for the same model with the schema in one prompt and no tools.

Code and every number