Repository navigation
REPL slice 1: bare dirsql opens a REPL (core loop, TTY/pipe split, exit, banner) - #991
Merged
Merged
Conversation
Bare `dirsql` currently prints a usage error and exits 2. These tests assert the contract #987 specifies instead: one statement per line from a pipe, no prompt or banner, a failing statement reported and the session continued, clean EOF exiting 0, and an unusable config failing once rather than per line. Pushed on its own so CI is observed red before any implementation lands. Refs #987 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
`dirsql` with no subcommand and no SQL printed a usage error and exited 2. It now reads statements until EOF: a banner and a `dirsql> ` prompt when stdin is a terminal, one statement per line and nothing else when it is a pipe. `sqlite3` sets the expectation -- run the binary bare, get a shell. The loop is a wrapper around the existing pipeline, not a second one: `load_state` and `execute_query` are unchanged, and statements reach `execute_query` through the same `query_body` intake a one-shot query and a `POST /query` do, so a statement typed at the prompt cannot drift from the same statement passed on the command line. Three behaviors are deliberately not `run_query`'s: - a failing statement prints its diagnostic and the session continues, where `dirsql query` exits 1. A REPL that dies on a typo is unusable. - a clean EOF exits 0 regardless of per-statement failures, matching interactive `sqlite3`. `PARTIAL_SCAN_EXIT` (23) has no session-level meaning, so skipped files are named once before the first prompt and the code is not produced. - a degraded index is reported once and exits 1 before the loop, rather than failing identically on every line the user types. The index is built once, ahead of the loop, so statements share one scan and the live watcher keeps it fresh between them. No new dependency (`std::io::IsTerminal` is std) and no new flag: `ConfigArgs` is already flattened onto the top-level `Cli`, so `-c`, `--persist`, `--no-ignore` and `--on-file` apply to the REPL with no parser work. The read runs on a blocking thread: `run_cli` drives everything inside `runtime.block_on`, so an inline read would park the thread the runtime is driven on. `BufReader<Stdin>` rather than `StdinLock`, which is not `Send`. `cli_e2e`'s `no_subcommand_and_no_sql_errors_pointing_at_dirsql_server` did assert the old runtime error, contrary to #987's survey, which found only the parse-level test. Its #662 intent -- bare `dirsql` must not silently start the server -- outlives the usage error, so it is rewritten to assert that invariant against the REPL rather than deleted. The REPL loop takes its statement sink, reader and both output sinks as parameters so `unit lint`'s isolation rule is satisfiable without a TTY or an index; that is why `run_repl` carries an `err` sink #987's sketch did not. Fixes #987 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GU6WPGs4bbkad8QhQfyXDD
This was referenced Aug 20, 2026
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 #987
Slice 1 of 3 under #953. Stack: this PR ← #988 ← #989.
Bare
dirsql(no subcommand, no SQL) printed a usage error and exited 2. It now reads statements until EOF: a banner and adirsql>prompt when stdin is a terminal, one statement per line and nothing else when it is a pipe.sqlite3sets the expectation — run the binary bare, get a shell.What's here
A new
packages/rust/src/cli/repl.rsand a rewrittenrun_defaultNonearm.load_stateandexecute_queryare untouched: statements reach the pipeline through the samequery_bodyintake a one-shot query and aPOST /querydo, so a statement typed at the prompt cannot drift from the same statement passed on the command line. Output is still the JSON array — the TTY table is #989.No new dependency (
std::io::IsTerminalis std since 1.70) and no new flag (ConfigArgsis already flattened onto the top-levelCli, so-c,--persist,--no-ignoreand--on-fileapply with no parser work).Three behaviors are deliberately not
run_query's:dirsql queryexits 1. A REPL that dies on a typo is unusable.sqlite3.PARTIAL_SCAN_EXIT(23) has no session-level meaning, so skipped files are named once before the first prompt and the code is not produced.Two notes on the issue's plan
#987's survey of the old behavior was incomplete. It says only
no_subcommand_and_no_sql_selects_neithertouches it, and that it asserts parsing.packages/rust/tests/cli_e2e.rs::no_subcommand_and_no_sql_errors_pointing_at_dirsql_serveralso asserted the runtime exit-2. Its #662 intent — baredirsqlmust not silently start the server — outlives the usage error, so it is rewritten to assert that invariant against the REPL rather than deleted.curtaincall (real-PTY e2e) is deferred to #988, deliberately. Slice 1's interactive surface is banner / prompt-per-read / trailing newline, all covered by unit tests with injected sinks plus the manual PTY run below. Slice 2's surface — arrow-key recall, Ctrl+R, Ctrl+C mid-line, multi-line continuation — is testable no other way, so that is where the dependency earns its keep. It also keeps a new dependency out of the PR that changes CLI dispatch, which is the same reasoning #988 gives for splitting reedline out. Practical detail for whoever picks up #988: curtaincall declares
requires-python >=3.12against this package's>=3.10floor, so it needs apython_version >= "3.12"marker plus a skip guard — and the local venv must be recreated on 3.12, or the test silently skips instead of running (upstream: thekevinscott/curtaincall#26).One API-shape deviation
run_repltakes anerrsink the issue's sketch did not:(state, input, out, err, interactive). Without it the diagnostics go througheprintln!, and neitherunit lint's isolation rule norunit mutationcan reach them. The loop is likewise generic over a privateStatementstrait (StateStatements<'a>is the real implementation overexecute_query) so its control flow is exercised with no index behind it — that trait is also the seam #988 should swap the reedline reader behind.PARITY
No drift. The CLI is one shared implementation in the Rust core that every SDK's launcher reaches through
run_cli; there is no per-SDK surface to diverge, and no SDK public API changed.PARITY.mdneeds no edit — stated here rather than skipped silently, per AGENTS.md.Manual run (AGENTS.md)
Also observed: the unquoted-path hint (
did you mean "./"?) reaches the REPL unchanged, since it comes from the shared pipeline.E2E Verification
cargo test --features cli --tests, including the newrepl_pipe_e2e(9) and the rewrittencli_e2ecase (44)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— the sandbox runs as uid 0, so achmod 000file is still readable and the NULL-content assertion cannot hold. It is green in CI and unrelated to this change.Changelog / Migrations
packages/rust/changelog.d/2026-08-20-bare-dirsql-repl.mdpackages/rust/migrations.d/2026-08-20-bare-dirsql-repl.md— required: runtime behavior change with no API change (baredirsql: exit-2 → REPL;echo "SELECT 1" | dirsqlnow runs the statement)Red/green
390264epushed the failing suite alone; CI was observed red on Integration tests (run 32412502201) with 8 failures, all of them the new REPL assertions and nothing else in the crate. The implementation follows in5eea7dd.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 the actuals rising against the floors: lines 93.47 (floor 92), regions 92.82 (91), functions 92.19 (91), branch 78.36 (72).unit mutationwas run directly through cargo-mutants under a deliberately stricter scope than CI's (-- --lib, unit targets only), which surfaced three survivors that are now killed:StateStatements::run -> Ok(Default::default())AppState::Unavailable— the real one reports the index's state, a stub cannot|| -> &&in the prompt guardFlushFailssink (writes succeed, flush fails), so the prompt's two failure modes are distinguishable(read > 0)innext_linematch reader.read_line(..)? { 0 => None, _ => Some(line) }— no comparison operator, so a mutant can no longer turn EOF into an endless empty line and hang instead of failingThe two remaining
run_default -> 0/-> 1mutants survive only under that--libscope; CI's cargo-mutants uses the default full-cargo testscope, whererepl_pipe_e2e's exit-code assertions kill them. Same shape as every previous change torun.rs's effectful arms (e.g.b2094a4), which is why the repo carries zero mutation exemptions.