Skip to content

sqlite3: pin agent invocation shapes; add dot-commands + -init/-batch#5

Merged
subsetpark merged 9 commits into
flowglad-mainfrom
sqlite3-invocation-shape-tests-and-dot-commands
Apr 30, 2026
Merged

sqlite3: pin agent invocation shapes; add dot-commands + -init/-batch#5
subsetpark merged 9 commits into
flowglad-mainfrom
sqlite3-invocation-shape-tests-and-dot-commands

Conversation

@subsetpark

@subsetpark subsetpark commented Apr 30, 2026

Copy link
Copy Markdown

Summary

  • Adds a 66-test pinning suite that mirrors how reasoning agents actually invoke sqlite3 in production, sourced from a Braintrust review of ~924 spans across the four flowglad-pay-agent* projects.
  • Lands two patches that close the most common gaps surfaced by that review: dot-command support (.tables, .schema, .headers, .mode, .separator, .nullvalue, .read) and the -init <file> + -batch flags.
  • Captures the catalogue + remaining deferred work in packages/just-bash/docs/sqlite3-invocation-shapes.md since GitHub issues are disabled on this fork.

What's in the test suite

Four new files under packages/just-bash/src/commands/sqlite3/:

  • sqlite3.invocation-shapes.test.ts (14 tests) — S1–S11: :memory:, multi-statement single arg, < script.sql, | stdin, $(cat …), -header -separator $'\t', output redirect, sequential calls, relative-path db.
  • sqlite3.sql-features.test.ts (17 tests) — F1–F7 + X4: CREATE TABLE … AS SELECT, bulk INSERT scripts, window functions, CTEs incl. WITH RECURSIVE, strftime, sqlite_master, CASE WHEN, scalar subqueries, PRAGMA table_info, indices/views, transactions.
  • sqlite3.dot-commands.test.ts (28 + 2 todo) — D1–D7 plus the rejection set. D8 .import and D9 .dump are kept as it.todo and tracked in docs.
  • sqlite3.flags.test.ts (7 tests) — X1 -init <file>, X2 -batch.

What's in the patch

dot-commands.ts (new): pure-function preprocessor that walks the SQL line by line and either translates a dot-command to SQL (.tablesSELECT name FROM sqlite_master WHERE type='table' …) or accumulates a formatter mutation (.mode csv{ mode: "csv", separator: "," }). .read is recursive (max depth 8). Explicitly unsupported commands (.import, .dump, .shell, .system, .open, …) are rejected with a clear error.

sqlite3.ts: adds -init <file> (reads via ctx.fs, prepends to SQL) and -batch (no-op since just-bash is never interactive). Wires the dot-command preprocessor in before the worker call, applies any formatter mutation to FormatOptions, and surfaces dot-command errors to stderr with -bail short-circuit.

Limitations (encoded in tests)

  • Last-mode-wins: dot-commands that mutate formatter state apply globally to the entire invocation, not incrementally. Real sqlite3 applies them statement by statement. Pinned in dot-commands / "interleaved mode + query".
  • .tables format: one name per line, not real sqlite3's 3-column space-padded format.
  • Float precision: pre-existing sql.js quirk — ROUND(x, 2) does not emit clean two-decimal output. 999.99 round-trips as 999.99000000000001. Documented in docs/sqlite3-invocation-shapes.md.

Deferred work

Tracked in packages/just-bash/docs/sqlite3-invocation-shapes.md:

  • D8 .import <file> <table> — could collapse the dominant awk → sql → < script ETL pattern into one line.
  • D9 .dump — schema + INSERT serialization.
  • X3 ATTACH DATABASE 'other.db' to real-FS files — blocked by sql.js sandbox; needs a multi-buffer worker protocol.
  • S2 — file builtin missing (agents use it for capability probes).

Test plan

  • pnpm typecheck — clean
  • pnpm knip — clean
  • pnpm lint — only flags a pre-existing python3/worker.ts violation on main (verified by git stash + re-run)
  • pnpm test:wasm src/commands/sqlite3/ — 204 passed + 2 todo across 13 files
  • pnpm test:run — 13,197 passed | 97 skipped (388 files)
  • CI green on this PR

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Adds a 66-test pinning suite that mirrors how reasoning agents actually
invoke `sqlite3` in production (sourced from a Braintrust review of ~924
spans across the four flowglad-pay-agent* projects), plus two patches to
close the most common gaps.

Tests (`packages/just-bash/src/commands/sqlite3/`):
- sqlite3.invocation-shapes.test.ts (S1-S11): :memory:, multi-statement,
  `< script.sql`, `| stdin`, `$(cat ...)`, `-header -separator $'\t'`,
  output redirect, sequential calls, relative-path db.
- sqlite3.sql-features.test.ts (F1-F7, X4): CREATE TABLE AS SELECT, bulk
  INSERT scripts, window functions, CTEs incl. WITH RECURSIVE, strftime,
  sqlite_master, CASE WHEN aggregates, scalar subqueries, PRAGMA
  table_info, indices/views, transactions.
- sqlite3.dot-commands.test.ts (D1-D7 + rejection set): .tables, .schema,
  .headers, .mode csv|tabs|json, .separator, .nullvalue, .read recursive.
  D8 .import / D9 .dump are kept as `it.todo` and tracked in docs.
- sqlite3.flags.test.ts (X1-X2): -init <file>, -batch (accepted no-op).

Patches (ship in dist via existing build):
- dot-commands.ts (new): preprocessor that translates dot-commands to SQL
  or formatter mutations before the worker sees the script. Supports
  .tables/.schema/.indexes/.databases, .headers/.header, .mode,
  .separator, .nullvalue, .read (recursive, max depth 8), .quit/.exit;
  accepts no-op for .help/.show/.timer/etc; rejects .import/.dump/.shell/
  .system/.open and friends with a clear error.
- sqlite3.ts: adds -init <file> (reads + prepends script via ctx.fs) and
  -batch (no-op). Wires the dot-command preprocessor before the worker
  call; dot-errors surface to stderr with -bail short-circuit.

Triage: GitHub issues are disabled on this fork, so deferred work is
tracked in `packages/just-bash/docs/sqlite3-invocation-shapes.md` (D8
.import, D9 .dump, X3 ATTACH-to-file, S2 missing `file` builtin).

Verified: 204 sqlite3 tests pass + 2 todo across 13 files; 13,197 tests
pass in `test:run`; `tsc --noEmit` clean; `pnpm knip` clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 3 comments

Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts
Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts Outdated
Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

The dot-command preprocessor and -init/-batch flag support are well-structured and the 66-test pinning suite gives solid coverage. Three issues are worth addressing:

  1. must-fix — dead sub-expression makes the final exitCode logic misleading (sqlite3.ts line 706): hadError && options.bail is always false at that point (if bail+error, the function returns early inside the loop). The simpler expression dotError !== undefined ? 1 : 0 matches the actual intent.

  2. should-fix — misleading comment about -init/-cmd prepend order (sqlite3.ts line 537): The comment says "prepend -init then -cmd" but the code prepends them in the reverse source order. The net execution order is correct, but the comment will mislead future readers.

  3. should-fix — dot-command scanner fires on SQL comment lines containing . (dot-commands.ts line 282): A SQL comment like -- .tables is a dot-command starts with -- after trim, not ., so it's actually safe in that specific case. But any line that starts with . after trimming (e.g. inside a .read-expanded file with unusual SQL) will hit the scanner. Adding a startsWith("--") guard before the dot-check defends the most common accidental trigger.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 1 |
    | Should-fix | 2 |
    | Note | 0 |


3 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 118520 in / 7010 out · Cache: 46299 created / 395091 read · Review mode: agentic · Turns: 15 · Tool calls: 16 · Forced submit: yes · Tool mix: read_file=11, search_codebase=5

- Drop dead `hadError && options.bail` from final exitCode (the in-loop
  bail return makes that branch unreachable). Remove now-unused
  `hadError` and clarify the comment.
- Fix misleading "-init then -cmd" comment; the code prepends -cmd
  first, then -init, so execution is init -> cmd -> SQL.
- Skip lines starting with `--` in the dot-command preprocessor so SQL
  comments aren't tokenized.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 3 comments

Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts Outdated
Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts
Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

Three issues found in the new dot-commands preprocessor and its integration into sqlite3.ts:

  1. must-fix (sqlite3.ts:572): After dot-command preprocessing, pre.sql is not trimmed before being used. When the input consists solely of formatter mutations (no SQL), pre.sql is whitespace-only (truthy) and bypasses the no-SQL guard, sending whitespace to the worker unnecessarily. Fix: trim pre.sql and return early with exitCode 0 when empty and no error.

  2. should-fix (dot-commands.ts:148): The .tables filter uses ESCAPE '\\\\' in a JS template literal which produces ESCAPE '\\' in the SQL string at runtime — an unterminated SQL string literal that will cause a SQLite parse error. Fix: use ~ as the escape character instead, eliminating the multi-level escaping hazard.

  3. should-fix (dot-commands.ts:298): When .read is expanded, the entire multi-line sub.sql string is pushed as a single element into outLines. The subsequent outLines.join('\\n') then inserts a spurious blank line before the sub-file's content. Fix: split sub.sql on \\n and push each line individually.
    | Severity | Count |
    |----------|-------|
    | Must-fix | 1 |
    | Should-fix | 2 |
    | Note | 0 |


3 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 116659 in / 5875 out · Cache: 69590 created / 376651 read · Review mode: agentic · Turns: 15 · Tool calls: 18 · Forced submit: yes · Tool mix: read_file=12, search_codebase=6

- Trim sql after dot-command preprocessing and short-circuit when the
  input was pure formatter mutations / dot-commands with no remaining
  SQL. Avoids sending whitespace to the worker; matches real sqlite3.
- Switch the .tables ESCAPE character from `\` to `~` to avoid the
  JS-template-literal -> SQL double-escape ambiguity.
- In preprocessDotCommandsInternal, split multi-line dot-command output
  (notably from .read) into individual outLines entries so the final
  outLines.join("\n") behaves uniformly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

One new should-fix finding in this incremental diff. The .mode handler in translateDotCommand mutates mutation.separator as a side-effect whenever the mode is csv, tabs, or list. This can silently overwrite an explicit .separator command that appeared before the .mode line in the script, because both mutations share the same FormatterMutation object and later writes win. Real sqlite3 keeps .mode and .separator orthogonal — changing the mode does not reset the separator. Removing the separator side-effect from the .mode handler fixes the ordering ambiguity without breaking the documented "last-write-wins" contract. All three outstanding Turn-1/Turn-2 findings remain unaddressed and are not re-raised here.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 114049 in / 3919 out · Cache: 74431 created / 379880 read · Review mode: agentic · Turns: 15 · Tool calls: 15 · Forced submit: yes · Tool mix: read_file=15

Real sqlite3 treats .mode and .separator as orthogonal: csv/tabs modes
hardcode their separators in the output formatter, and list mode reads
from the configured separator. The previous code mutated
mutation.separator inside the .mode handler, which clobbered an
explicit .separator that ran *before* the .mode (a script like
`.separator ;` then `.mode list` would silently revert to `|`).

Drop the separator side-effect from the .mode handler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 2 comments

Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts Outdated
Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

The incremental diff lands the dot-command preprocessor (dot-commands.ts) and wires it into sqlite3.ts, along with -init/-batch flag support and a 66-test pinning suite. The previously-flagged issues (backslash escape bug, -- SQL comment guard, .read multi-line split, .mode/.separator independence) have been resolved in this commit. Two new issues:\n\n1. should-fix (sqlite3.ts:557): An empty -init file with no main SQL causes a spurious \"no SQL provided\" error (exit 1) instead of clean exit 0. The existing !sql guard needs to gate on whether any SQL source was explicitly provided, not just whether the assembled string is empty.\n\n2. should-fix (dot-commands.ts:286): The trimmed.startsWith(\".\") guard can misidentify SQL lines that begin with a non-alphabetic . (e.g. .5 as a decimal literal) as dot-commands. All real sqlite3 dot-commands have a letter after the dot, so adding !/^\\.\\[a-zA-Z]/.test(trimmed) to the guard closes this with a one-line change.

Severity Count
Must-fix 0
Should-fix 2
Note 0

2 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 131465 in / 8480 out · Cache: 64670 created / 414821 read · Review mode: agentic · Turns: 15 · Tool calls: 15 · Forced submit: yes · Tool mix: read_file=13, search_codebase=2

- The "no SQL provided" guard now only fires when no SQL source was
  given at all (no -init, no positional SQL arg, no stdin). An empty
  -init file with nothing else falls through to the dot-command
  preprocessor's clean short-circuit, matching real sqlite3.
- The dot-command scanner now requires `.` to be followed by an
  alphabetic character before treating a line as a dot-command. SQL
  fragments like `.5` (numeric continuation) no longer get tokenized
  and rejected as unknown commands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

The incremental diff correctly addresses most prior-round comments: the \\~ ESCAPE fix in .tables, the /^\.[a-zA-Z]/ dot-command guard, sql = pre.sql.trim() plus the short-circuit for empty-SQL-after-preprocessing, removal of the dead hadError && options.bail sub-expression, and .mode no longer mutating separator. One new issue: the updated no-SQL guard at line 560 uses !sqlArg (falsy) instead of sqlArg === null (presence) to test whether a SQL positional argument was provided. Since sqlArg is string | null, an explicit empty string argument (sqlite3 :memory: \"\") evaluates !sqlArg as true and incorrectly returns exit 1 with "no SQL provided" instead of the correct exit 0.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 172590 in / 5963 out · Cache: 65213 created / 392289 read · Review mode: agentic · Turns: 15 · Tool calls: 14 · Tool mix: read_file=14

`sqlArg` and `options.init` are typed `string | null`, so use `=== null`
to test absence rather than falsiness. With falsy checks, an explicit
empty SQL argument (`sqlite3 :memory: ""`) was treated the same as a
missing argument and produced "no SQL provided" + exit 1, but real
sqlite3 silently exits 0 in that case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

One new correctness issue found in this incremental diff: .quit/.exit dot-commands in dot-commands.ts return an empty SQL string and comment "stop processing" but the preprocessDotCommandsInternal loop has no mechanism to honor that intent — all subsequent lines still execute. All outstanding comments from prior turns are not re-raised.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 122121 in / 3836 out · Cache: 63450 created / 420514 read · Review mode: agentic · Turns: 15 · Tool calls: 15 · Forced submit: yes · Tool mix: read_file=14, search_codebase=1

Previously .quit and .exit emitted empty SQL and the loop kept
iterating, so SQL on lines after .quit was still sent to the worker
and executed. A script like ".quit\nDROP TABLE t" would still drop
the table.

Add a quit signal to the translateDotCommand return type and propagate
it through PreprocessResult so that:

- the main loop returns immediately on .quit, dropping everything below;
- a .quit inside a .read'd file also stops the outer parent (matches
  real sqlite3, where .quit exits the process regardless of nesting).

Add regression tests for .quit, .exit, and nested .quit-via-.read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread packages/just-bash/src/commands/sqlite3/sqlite3.ts Outdated
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

The current diff addresses several previously outstanding issues (dead hadError && options.bail code, misleading comment order, .quit sentinel, empty-SQL short-circuit, === null vs falsy check, -- comment guard, /^\.[a-zA-Z]/ guard, and .mode not clobbering .separator). One new in-scope inconsistency remains: the -init processing block at line 543 uses if (options.init) (truthy), while the co-located comment and the no-SQL guard at line 562 both use strict === null / !== null checks. An empty string filename (-init "") would silently skip the init block but still pass the no-SQL guard, leaving the two checks inconsistent with each other and with the stated design.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 122038 in / 2805 out · Cache: 66624 created / 433816 read · Review mode: agentic · Turns: 15 · Tool calls: 15 · Forced submit: yes · Tool mix: read_file=12, search_codebase=3

`options.init` is `string | null`. Using `!== null` (instead of a
truthy check) means `-init ""` actually attempts the read and surfaces
a clear "cannot open" error, matching the no-SQL guard below which
already treats an explicit empty string as "provided".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@flowglad-review-service flowglad-review-service Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: 1 comment

Comment thread packages/just-bash/src/commands/sqlite3/dot-commands.ts
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

All previously-outstanding issues from turns 1–7 appear to have been addressed in this diff: the hadError dead code is removed, the dotError && options.bail early-return is present, sql = pre.sql.trim() with the subsequent empty-SQL short-circuit is in place, .quit/.exit propagates a quit sentinel that breaks the outer loop, the ~ ESCAPE character replaces the broken \\ in .tables, .read content is split line-by-line before being pushed to outLines, the /^\.[a-zA-Z]/ guard prevents non-command .-prefixed SQL from being routed to the translator, sqlArg === null is used instead of falsiness in the no-SQL guard, options.init !== null guards the init-file block, and .mode no longer clobbers .separator. One new minor defensive gap is identified: translateDotCommand's head is silently typed as string rather than string | undefined and would produce a confusing error message if tokens were somehow empty.

Severity Count
Must-fix 0
Should-fix 1
Note 0

1 comment posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 178745 in / 6292 out · Cache: 71678 created / 329091 read · Review mode: agentic · Turns: 13 · Tool calls: 11 · Tool mix: read_file=11

The destructured `head` is typed `string` but is actually `string |
undefined` if `tokens` is empty (TypeScript's index-signature inference
hides this). The caller's `/^\.[a-zA-Z]/` guard means tokens is always
non-empty in practice, but add an explicit check so a future caller
change can't surface as a confusing "unknown command: undefined"
error from the switch's default branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@flowglad-review-service

Copy link
Copy Markdown

Review Summary

No new findings in this incremental diff. The dot-command preprocessor (dot-commands.ts) and its wiring into sqlite3.ts are implemented correctly: the withSemi/split path for .read-produced multi-line SQL works correctly, null-prototype is applied to the mutation accumulator, Set-based lookup tables are not subject to prototype pollution, and all new guard conditions use the right semantics (!== null for presence checks). The four new test files provide solid coverage of the catalogued invocation shapes. All prior review comments remain live; none of the issues they describe are re-introduced by this incremental change.

Severity Count
Must-fix 0
Should-fix 0
Note 0

0 comments posted · Model: claude-sonnet-4-6 (sonnet) · Tokens: 177456 in / 4165 out · Cache: 69686 created / 375689 read · Review mode: agentic · Turns: 14 · Tool calls: 12 · Tool mix: read_file=10, search_codebase=2

@subsetpark subsetpark merged commit 03dc476 into flowglad-main Apr 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant