Skip to content

Commit d77d3cf

Browse files
subsetparkclaude
andcommitted
feat(sqlite3): translate dot-commands (.tables, .schema, .mode, .read, ...)
Real sqlite3 ships a family of dot-commands that are a feature of the CLI, not the library. sql.js doesn't implement them, so agent scripts pasted verbatim from a real sqlite3 session hit `near ".": syntax error` on the first dot-command (.tables, .schema, .mode csv, .read seed.sql, ...). Add a host-side preprocessor (commands/sqlite3/dot-commands.ts) that runs before SQL reaches the worker. The scanner is char-level with state for SQL string literals ('...', "...", including the doubled-quote escapes) and comments (-- ..., /* ... */); a dot-command is recognized only at a boundary (start-of-input, after `;`, or after `\n`) and only outside strings/comments — `'a\n.tables'` round-trips intact, and `.headers on; .mode csv; CREATE TABLE...` is three tokens on one line. Each recognized dot-command resolves to one of: - SQL replacement — .tables/.schema/.indexes/.databases/.help translate to sqlite_master/PRAGMA queries (with shell-glob → SQL-LIKE conversion). - Formatter mutation — .headers, .mode, .separator, .nullvalue adjust output state for downstream SQL; bad args surface real-sqlite3-shaped errors (e.g. "Error: unknown mode: parquet"). Last write wins. - .read FILE — recursive scan of the included file, sharing formatter state, capped at MAX_READ_DEPTH to prevent loops. - .quit / .exit — terminate preprocessing; subsequent input is dropped. - Silent drop — .echo, .timer, .changes, .bail, .show, .eqp, .width, .prompt, .print, .explain are recognized and discarded. - Not-implemented family — .dump, .save, .backup, .import, .clone, .restore, .open, .output, .shell, .system, .cd, .load, .iotrace, .log, .excel translate to `SELECT 'Error: ...' AS error` so the message rides in stdout in script-order without aborting SQL. - Passthrough — unknown dot-commands are left verbatim so sql.js produces its native syntax error. Pairs with two new CLI flags: - `-init FILENAME` — read SQL from FILENAME before main SQL. - `-batch` — no-op (just-bash is always non-interactive). Dot-command errors are routed in-band to stdout alongside SQL errors when -bail is unset (matches real sqlite3's single-channel reporting); with -bail they go to stderr with exit 1. Tests: 247 sqlite3 tests pass against the existing worker (no worker changes needed — preprocessor runs entirely host-side). New test files: - sqlite3.dot-commands.test.ts: comprehensive preprocessor coverage - sqlite3.invocation-shapes.test.ts: pinned agent invocation patterns - sqlite3.flags.test.ts: -init / -cmd / -batch flag behavior - sqlite3.sql-features.test.ts: generic SQL features through the pipeline Docs: packages/just-bash/docs/sqlite3-invocation-shapes.md catalogues the canonical CLI invocation shapes agents use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1369b77 commit d77d3cf

8 files changed

Lines changed: 1969 additions & 9 deletions

File tree

.changeset/sqlite3-dot-commands.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"just-bash": minor
3+
---
4+
5+
sqlite3: translate `.tables`, `.schema`, `.mode`, `.read`, etc. so pasted CLI scripts work
6+
7+
Real `sqlite3` ships a set of dot-commands (`.tables`, `.schema`, `.mode csv`, `.read script.sql`, `.headers on`, `.separator`, …) that are a feature of the CLI, not the library. sql.js doesn't implement them, so agent scripts pasted verbatim from a real `sqlite3` session hit `near ".": syntax error` on the first dot-command.
8+
9+
This change adds a host-side preprocessor (`commands/sqlite3/dot-commands.ts`) that runs before SQL reaches the worker:
10+
11+
- **SQL replacement**`.tables`, `.schema`, `.indexes`/`.indices`, `.databases`, `.help` are translated to `sqlite_master`/PRAGMA queries with glob → SQL-LIKE pattern conversion (`*``%`, `?``_`).
12+
- **Formatter mutations**`.headers`/`.header`, `.mode`, `.separator`, `.nullvalue` adjust output state for downstream SQL; bad arguments surface real-sqlite3-shaped errors (`Error: unknown mode: parquet`).
13+
- **`.read FILE`** — recursively scans the included file (sharing formatter state, bumping depth, capped at `MAX_READ_DEPTH`).
14+
- **`.quit` / `.exit`** — terminate preprocessing and drop subsequent input.
15+
- **Silent drop** — recognize-and-discard for sandbox no-op commands (`.echo`, `.timer`, `.changes`, `.bail`, `.show`, `.eqp`, `.width`, `.prompt`, `.print`, `.explain`).
16+
- **Not-implemented family**`.dump`, `.save`, `.backup`, `.import`, `.clone`, `.restore`, `.open`, `.output`, `.shell`, `.system`, `.cd`, `.load`, `.iotrace`, `.log`, `.excel` translate to a `SELECT 'Error: …' AS error` so the message rides in stdout in script-order without aborting surrounding SQL.
17+
- **Passthrough** — unknown dot-commands are left verbatim so sql.js produces its native syntax error.
18+
19+
The scanner is char-level with state for string literals (`'…'`, `"…"`, including the SQL `''`/`""` escapes) and comments (`-- …`, `/* … */`); a dot-command is recognized only at a boundary (start-of-input, after `;`, or after `\n`) and only outside strings/comments. This lets `'a\n.tables'` round-trip intact and lets `.headers on; .mode csv; CREATE TABLE…` be three tokens on a single line.
20+
21+
Also adds two CLI flags that pair with the preprocessor:
22+
23+
- `-init FILENAME` — read SQL from `FILENAME` before main SQL (matches real sqlite3).
24+
- `-batch` — accepted as a no-op since just-bash is always non-interactive.
25+
26+
Dot-command errors are routed in-band to stdout (alongside SQL errors) when `-bail` is unset, matching real-sqlite3's single-channel reporting; with `-bail` they go to stderr and exit 1.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# sqlite3 invocation shapes — agent parity catalogue
2+
3+
This index tracks how reasoning agents actually invoke `sqlite3` in production
4+
(via the `bash` tool) and our parity status. Source: a Braintrust review of
5+
~924 spans across the four `flowglad-pay-agent*` projects in 2026-04.
6+
7+
Each shape maps to one or more pinning tests under
8+
`src/commands/sqlite3/sqlite3.{invocation-shapes,sql-features,dot-commands,flags}.test.ts`.
9+
If a test in this directory regresses, an agent in production has hit (or will
10+
hit) the same failure.
11+
12+
This file is the **triage list** — the GitHub issue tracker is disabled on
13+
this fork, so deferred work lives here. Update the **Status** column when
14+
patches land.
15+
16+
## Shell invocation shapes
17+
18+
| ID | Shape | Status | Test |
19+
| --- | ----------------------------------------------------------- | ------------- | ---- |
20+
| S1 | `sqlite3 :memory: "SELECT sqlite_version();"` | Supported | invocation-shapes / S1 |
21+
| S2 | `which sqlite3 && file $(which sqlite3)` (capability probe) | **Partial**: `file` builtin missing ||
22+
| S3 | `sqlite3 <db> "<single SQL>"` | Supported | invocation-shapes / S3 |
23+
| S4 | `sqlite3 <db> "stmt1; stmt2; stmt3"` | Supported | invocation-shapes / S4 |
24+
| S5 | `sqlite3 <db> < /workspace/file.sql` (script redirect) | Supported | invocation-shapes / S5 |
25+
| S6 | `echo "<SQL>" \| sqlite3 <db>` (stdin pipe) | Supported | invocation-shapes / S6 |
26+
| S7 | `sqlite3 <db> "$(cat file.sql)"` (command substitution) | Supported | invocation-shapes / S7 |
27+
| S8 | `sqlite3 -header -separator $'\t' <db> "..."` (TSV+header) | Supported | invocation-shapes / S8 |
28+
| S9 | `sqlite3 -separator $'\t' <db> "..."` (TSV no header) | Supported | invocation-shapes / S9 |
29+
| S10 | `sqlite3 <db> "..." > /workspace/out.tsv` | Supported | invocation-shapes / S10 |
30+
| S11 | Per-statement loop (sequential calls) | Supported | invocation-shapes / S11 |
31+
32+
## SQL features
33+
34+
| ID | Feature | Status | Test |
35+
| --- | -------------------------------------------------------- | --------- | ---- |
36+
| F1 | `CREATE TABLE … AS SELECT …` | Supported | sql-features / F1 |
37+
| F2 | Bulk `INSERT` blocks from script-redirected `.sql` | Supported | sql-features / F2 |
38+
| F3 | Window functions (`SUM(...) OVER (PARTITION BY ...)`) | Supported | sql-features / F3 |
39+
| F4 | CTEs (`WITH x AS …`) including `WITH RECURSIVE` | Supported | sql-features / F4 |
40+
| F5 | `strftime('%Y-%m', …)`, `strftime('%Y-W%W', …)` | Supported | sql-features / F5 |
41+
| F6 | `sqlite_master` introspection | Supported | sql-features / F6 |
42+
| F7 | `CASE WHEN`, scalar subqueries | Supported | sql-features / F7 |
43+
| X4 | `PRAGMA table_info(t)` and friends | Supported | sql-features / X4 |
44+
45+
## Dot-commands
46+
47+
| ID | Command | Status | Test |
48+
| --- | ------------------------------------ | ---------------------- | ---- |
49+
| D1 | `.tables [pat]` | Supported (one-name-per-line, not multi-column) | dot-commands / D1 |
50+
| D2 | `.schema [pat]` | Supported | dot-commands / D2 |
51+
| D3 | `.headers on/off` (alias `.header`) | Supported | dot-commands / D3 |
52+
| D4 | `.mode <list\|csv\|json\|line\|column\|table\|markdown\|tabs\|box\|quote\|html\|ascii>` | Supported (last-mode-wins limitation) | dot-commands / D4 |
53+
| D5 | `.separator <col> [<row>]` | Supported | dot-commands / D5 |
54+
| D6 | `.nullvalue <text>` | Supported | dot-commands / D6 |
55+
| D7 | `.read <file>` (recursive) | Supported | dot-commands / D7 |
56+
| D8 | `.import <file> <table>` | **Deferred — see Open work / D8** | dot-commands / D8 (todo) |
57+
| D9 | `.dump` | **Deferred — see Open work / D9** | dot-commands / D9 (todo) |
58+
|| `.help .show .timer .changes .bail .echo .eqp .width .prompt .print .explain` | Accepted as no-op ||
59+
|| `.import .dump .clone .save .restore .backup .open .shell .system .iotrace .log .cd .load .excel` | Rejected with explicit error | dot-commands / "explicitly unsupported" |
60+
61+
## Flags
62+
63+
| ID | Flag | Status | Test |
64+
| --- | ------------------------------------- | --------- | ---- |
65+
|| `-list -csv -json -line -column -table -markdown -tabs -box -quote -html -ascii` | Supported | options / output-modes |
66+
|| `-header / -noheader / -separator / -newline / -nullvalue / -readonly / -bail / -echo / -cmd / -version / --` | Supported | options |
67+
| X1 | `-init <file>` | Supported | flags / X1 |
68+
| X2 | `-batch` | Supported (no-op) | flags / X2 |
69+
70+
## Limitations
71+
72+
- **Float precision**: `ROUND(x, 2)` does not emit clean two-decimal output.
73+
just-bash mirrors real sqlite3's full IEEE-754 precision (`999.99`
74+
`999.99000000000001`). Agents who need clean cents should compute in
75+
integer cents. Pinned in invocation-shapes / S8.
76+
- **Last-mode-wins**: dot-commands that mutate formatter state (`.mode`,
77+
`.headers`, `.separator`, `.nullvalue`) apply globally to the entire
78+
invocation, not incrementally. Real sqlite3 applies them statement by
79+
statement. Pinned in dot-commands / "interleaved mode + query".
80+
- **`.tables` format**: one name per line, not real sqlite3's 3-column
81+
space-padded format. Easier to parse, but a divergence.
82+
83+
## Open work (deferred)
84+
85+
### D8: `.import <file> <table>` — load CSV/TSV into a table
86+
87+
Real sqlite3 uses `.import` to ingest CSV/TSV. Agents currently work around
88+
this by translating CSV → `INSERT` statements via `awk`, which is slow and
89+
brittle. Trace evidence: ~22 invocations of the awk-then-script pattern in
90+
the sample of 200 spans.
91+
92+
**Suggested approach**: implement in `dot-commands.ts`. Parse
93+
`.import [--csv|--ascii] [--skip N] FILE TABLE`. Read FILE via `ctx.fs`,
94+
parse with the existing papaparse dep, translate to `INSERT INTO TABLE
95+
VALUES (...)` statements appended to the SQL stream.
96+
97+
**Test pin**: `sqlite3.dot-commands.test.ts` has `it.todo` under
98+
`describe("D8: .import")`. Flip to `it(...)` once implemented.
99+
100+
### D9: `.dump` — emit schema + INSERTs reproducing the database
101+
102+
Walk `sqlite_master`, emit every `CREATE TABLE/INDEX/VIEW/TRIGGER` with a
103+
trailing `;`, then `SELECT *` from each table to generate `INSERT INTO ...
104+
VALUES (...)` lines. Wrap in `BEGIN; ... COMMIT;`.
105+
106+
**Test pin**: `sqlite3.dot-commands.test.ts` has `it.todo` under
107+
`describe("D9: .dump")`.
108+
109+
### X3: `ATTACH DATABASE 'other.db' AS o` to real-FS file paths
110+
111+
sql.js is a sandboxed WASM build with no real-FS access. `ATTACH` to a path
112+
opens an empty in-memory database, not the file the agent expects.
113+
114+
**Suggested approach**: pre-process `ATTACH DATABASE 'path' AS alias`
115+
read `path` via `ctx.fs`, load it into sql.js as an in-memory DB, register
116+
under `alias`. Write back on exit if modifications detected. Non-trivial:
117+
need a multi-buffer worker protocol.
118+
119+
**No test pin yet** — file under
120+
`sqlite3.invocation-shapes.test.ts` if/when added.
121+
122+
### S2: `file` builtin missing
123+
124+
`which sqlite3 && file $(which sqlite3)``file` is not a just-bash builtin.
125+
This isn't strictly a sqlite3 issue; it's a gap in the command set. Agents
126+
use it to verify the binary type during capability probes.
127+
128+
**Suggested approach**: add `file` as a stub in `src/commands/file/` that
129+
inspects the magic bytes of the target file and reports an extension-based
130+
type. Doesn't need full libmagic — just enough to identify ELF, Mach-O,
131+
PE, ASCII text, JSON, gzip, zip, sqlite, etc.
132+
133+
## When this catalogue should be re-run
134+
135+
Pull a fresh sample any time:
136+
137+
- A `flowglad-pay-agent*` project's prompt set materially changes.
138+
- We bump `sql.js` (regression risk for SQL features).
139+
- An agent reports a new failure mode involving `sqlite3`.
140+
141+
Re-run via the Braintrust SQL-query MCP:
142+
143+
```sql
144+
-- across the four flowglad-pay-agent* projects
145+
WHERE output::text ILIKE '%sqlite3%'
146+
ORDER BY created DESC
147+
LIMIT 200
148+
```
149+
150+
Then update the tables above and add new pinning tests.

0 commit comments

Comments
 (0)