Repository navigation
REPL slice 3: table output when rows are read, JSON when they are piped - #1006
Merged
thekevinscott merged 4 commits intoAug 21, 2026
Merged
thekevinscott merged 4 commits into
thekevinscott merged 4 commits into
Conversation
#989 asks for a table when rows are read with eyes and JSON when they are piped to `jq`, and leaves open whether the user can choose. These tests answer that with a `--format {auto,table,json}` flag -- the issue's own preferred option, and the one that stays in flag-space rather than reopening the dot-command question #953 closed. Nine fail today; three pass as regression guards. The guards are the point of the shape: `dirsql "<sql>" | jq` must keep printing exactly the bytes it prints now, and the server must keep rejecting a rendering flag its transport cannot honour. Every case forces the format or runs through a pipe, so none of it needs a terminal. Refs #989 skip-changelog: test-only commit; the fragment lands with the implementation Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GU6WPGs4bbkad8QhQfyXDD
A JSON array is right for `dirsql "<sql>" | jq` and wrong for a person: `SELECT * FROM './'` over a few thousand files puts a few thousand JSON objects on one line. Rows now follow their destination -- a table when stdout is a terminal, the same JSON array when it is not. #989 left open whether the user gets a say. They do, through `--format {auto,table,json}`, which is the issue's own preferred option and the one that stays in flag-space: `.mode` would mean reintroducing the dot-command namespace #953 closed, and a flag serves `dirsql query` too. `auto` is the default and reads **stdout**, not stdin -- `dirsql > rows.json` typed at a terminal is still headed for a file, and the file wants JSON. `server` does not take the flag; its transport is JSON over HTTP and could not honour it. Piped output is unchanged byte for byte, which is the regression that mattered most here and is asserted directly rather than by eye. The renderer is deliberately modest, in `sqlite3`'s `.mode column` style: aligned columns, a rule under the header, a row count, `NULL` spelled out so it cannot be read as an empty string, and every column any row carries so a ragged result drops nothing. Two things happen to a value on its way into a cell, and a `content` column is the reason for both. Its newlines are escaped, because the manual run showed a single unescaped one collapsing the whole layout -- one row spans as many lines as its longest value and no column lines up with any other. And it is truncated past 60 characters, because a cell holding a whole file otherwise sets the width of every row. Tabs, carriage returns and other control characters are escaped alongside the newlines; a bare `\r` would rewind the cursor over a row already drawn. `--format json` returns values unaltered. Terminal-width-aware layout and paging are out of scope, said so in the docs rather than silently omitted. Slice 2's pty cases now ask for `--format json`: on a terminal they would otherwise read rows through a table layout, making every one of them a rendering test as well as a termination test. One new case covers the default they gave up. Fixes #989 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GU6WPGs4bbkad8QhQfyXDD
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #989
Slice 3 of 3 under #953 — the last one. Stacked on #998, which is stacked on #991, so this PR's diff is slice 3's own. Stack: #991 ← #998 ← this PR.
A JSON array is right for
dirsql "<sql>" | jqand wrong for a person:SELECT * FROM './'over a few thousand files puts a few thousand JSON objects on one line. Rows now follow their destination — a table when stdout is a terminal, the same JSON array when it is not.Piped output is unchanged, byte for byte. That is the regression that mattered most here, and it is asserted directly rather than by eye:
autoin a pipe and--format jsonmust produce identical bytes.The open question, resolved
#989 asked whether the user gets a say once two formats exist, and ruled out
.mode(dot-commands were closed in #953). The answer is the issue's own preferred option:--format {auto,table,json}, defaulting toauto, valid in the REPL anddirsql queryalike. It stays in flag-space rather than reopening a meta-command namespace, and it serves the one-shot query too.autoreads stdout, not stdin, as #989 asked explicitly.dirsql > rows.jsontyped at a terminal is still headed for a file, and the file wants JSON.The flag is declared on
CliandQueryArgs— the same places--on-filelives — and deliberately not onConfigArgs, which is flattened intoServerArgstoo. The server speaks JSON over HTTP and could not honour a rendering flag; a test pins that it still rejects one.Also settled
…. Onecontentcolumn holding a whole file would otherwise set the width of every row.less. Terminal-width-aware layout is out of scope for the same reason — the per-cell cap solves the problem the issue actually named.NULLrather than dropping the column.What the manual run caught that the tests had not
AGENTS.md asks for a manual run because tests prove what they assert and nothing else. This is what the table looked like the first time:
contentvalues end in a newline, so one row spanned two lines and the table stopped being a table. Every unit test passed — none of them had put a newline in a cell. Fixed by escaping control characters on the way into a cell (\n,\t,\r, and\u{…}for the rest); a bare\ris worse than a newline, since it rewinds the cursor over a row already drawn.--format jsonreturns values unaltered.After:
Manual run (AGENTS.md)
Against a directory holding
a.md(6 B) andb.md(10 B):The second is the one that matters: a pipe is not a terminal, so
autopicks JSON and the bytes are what they always were.The terminal half is confirmed from the other side too —
a_terminal_session_renders_a_table_by_defaultdrives a real pty with no--formatand sees a table rather than a JSON array.PARITY
No drift. The CLI is one shared implementation in the Rust core that every SDK's launcher reaches through
run_cli; no SDK public API changed.PARITY.mdneeds no edit — stated here rather than skipped silently, per AGENTS.md.E2E Verification
cargo test --features cli --tests, 45 binaries green, including the newrepl_format_e2e(12) and the updatedrepl_tty_e2e(8)packages/pythone2e attestation refreshed (apackages/rust/srcchange stales both bindings)packages/tse2e attestation refreshedtests/vtab.rs::unreadable_file_yields_null_content_without_erroring_the_row— this sandbox runs as uid 0, so achmod 000file is still readable and the NULL-content assertion cannot hold. Green in CI, unrelated to this change.Changelog / Migrations
packages/rust/changelog.d/2026-08-20-format-flag.mdpackages/rust/migrations.d/2026-08-20-format-flag.md— required: rows printed to a terminal change shape. Piped output is untouched, but a script capturing output through a pty sees a table now and needs--format json.Red/green
6f693fdpushed the failing suite alone — nine red, three passing as regression guards (piped JSON unchanged; the server still rejects the flag) — and CI was observed red onIntegration tests. The implementation follows in060576f.Other gates run locally
unit colocated-testandunit lint(rust) exit 0 ·cargo clippy --workspace -- -D warningsandcargo fmt --all --checkclean ·unit coverage(rust, nightly) exit 0, with actuals above every floor: lines 93.55 (floor 92), regions 92.81 (91), functions 92.52 (91), branch 78.07 (72) ·unit mutationagainst this slice's diff, full-suite scope: 44 mutants, 43 caught, 1 unviable, zero survivors.One
unit lintnote worth recording for whoever writes the next renderer: a unit test may import onlysuper::and purestd, soserde_json::json!is out of bounds.serialize.rsgets away with it because it importsjson!at module level for production use;table.rsdoes not, so its tests build rows withValue::from_iter([(String, Value)])— an object constructor reachable throughsuper::*alone.