Skip to content

cleanup(engine): make process_command stdout-clean (drop REPL-only prints)#76

Merged
joaoh82 merged 1 commit into
mainfrom
cleanup/engine-stdout-pollution
May 3, 2026
Merged

cleanup(engine): make process_command stdout-clean (drop REPL-only prints)#76
joaoh82 merged 1 commit into
mainfrom
cleanup/engine-stdout-pollution

Conversation

@joaoh82

@joaoh82 joaoh82 commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

The engine's process_command had four direct stdout writes that made sense in a REPL but corrupted any other channel an embedder might be using stdout for:

File:line Wrote to stdout
src/sql/mod.rs:150 table.print_table_schema() after CREATE TABLE
src/sql/mod.rs:208 db_table.print_table_data() after INSERT (dumps the whole table after every insert — bad UX even before the protocol-channel concern)
src/sql/mod.rs:224 print!("{rendered}") after SELECT
src/sql/parser/create.rs:190 println!("{constraint:?}") — debug print left in for unhandled TableConstraint variants

This is the issue PR #73 papered over with the dup2(2,1) dance in sqlrite-mcp/src/stdio_redirect.rs. The dance is still in place (belt-and-suspenders against future regressions), but the engine now behaves correctly without it. Every other embedder benefits — Tauri desktop, Python/Node/Go/WASM SDKs, FFI consumers, and any future host all get clean stdout.

How

Refactored process_command to return a richer struct, with a backwards-compat wrapper:

pub struct CommandOutput {
    pub status:   String,           // "INSERT Statement executed.", etc.
    pub rendered: Option<String>,   // SELECT prettytable; None otherwise
}

pub fn process_command(query, db) -> Result<String>;
// backwards-compat: returns just .status

pub fn process_command_with_render(query, db) -> Result<CommandOutput>;
// new: rich return, never writes to stdout

The REPL (src/main.rs) calls _with_render and prints rendered above status — same UX as before. The .ask meta-command does the same and concatenates the two pieces into the single String it bubbles up to the REPL's outer dispatch loop.

The CREATE-TABLE schema dump and INSERT row dump are not preserved — they were small UX niceties the interactive REPL user rarely needs (the schema you just CREATEd, the row you just INSERTed). The INSERT case was actively bad UX on any non-trivial table — it dumped the entire table after every insert.

sqlrite::process_command keeps its existing signature for backwards compatibility — every existing call site (Connection::execute, the SDK FFI shims, the .ask handler's inline runner, the engine's own tests) keeps working unchanged.

REPL smoke (UX preserved for what matters)

sqlrite-engine - 0.1.25
…
sqlrite> CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);
CREATE TABLE Statement executed.
sqlrite> INSERT INTO users (name, age) VALUES ('alice', 30);
INSERT Statement executed.
sqlrite> SELECT id, name, age FROM users ORDER BY age DESC;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
| 1  | alice | 30  |
+----+-------+-----+
SELECT Statement executed. 1 row returned.

CREATE / INSERT now show just the status line (no schema dump after CREATE, no row dump after INSERT). SELECT still renders the table above the status — same as before.

MCP smoke (the original motivating bug)

Before PR #73, MCP saw prettytable garbage on stdout corrupting JSON-RPC. PR #73's stdio_redirect papered over it. With this engine cleanup, the MCP server's stderr is now completely empty during the same round-trip — confirming the engine no longer produces stray stdout writes.

Tests

  • cargo build --workspace — clean
  • cargo test --workspace --exclude sqlrite-{python,nodejs,desktop} — 330+ tests across engine + sqlrite-mcp + sqlrite-ask + sqlrite-ffi, all passing (the engine's 251-test integration suite directly exercises the process_command paths)
  • REPL smoke: CREATE / INSERT / SELECT round-trip — table renders above status as expected
  • MCP smoke: end-to-end JSON-RPC round-trip with stderr captured — empty

Docs updated

  • docs/architecture.md — module-map entries for src/lib.rs and src/sql/mod.rs now mention the new types + the "never writes to stdout" guarantee
  • docs/sql-engine.md — entry-point section shows both functions + the CommandOutput shape; explains the rendered-vs-status split
  • docs/mcp.md — security-notes section reframes stdio_redirect as defense-in-depth now that the engine is clean
  • sqlrite-mcp/src/stdio_redirect.rs — module-level comment rewritten to call out the engine fix + the concrete future-regression scenarios the redirect still protects against

Test plan

  • cargo build --workspace clean
  • all engine + MCP integration tests pass
  • REPL UX preserved for SELECT (rendered table still appears)
  • MCP server stderr is empty during normal operation
  • post-merge: visual review that the desktop app still works (Tauri webview never depended on stdout, so this is mostly a non-event — but worth a quick sanity check)
  • post-merge: no SDK regression — Python/Node/Go SDKs never showed the engine's stdout output to their callers anyway, but the underlying tests should confirm

🤖 Generated with Claude Code

…ints)

The engine's `process_command` had three direct stdout writes that
made sense in a REPL but corrupted any other channel an embedder
might be using stdout for:

  src/sql/mod.rs:150 — let _ = table.print_table_schema();   (CREATE)
  src/sql/mod.rs:208 — db_table.print_table_data();          (INSERT)
  src/sql/mod.rs:224 — print!("{rendered}");                 (SELECT)

Plus a leftover debug print in src/sql/parser/create.rs:190 dumping
unhandled `sqlparser::ast::TableConstraint` debug reprs to stdout.

This is the issue PR #73 papered over with the dup2(2,1) dance in
sqlrite-mcp/src/stdio_redirect.rs. That fix is still in place
(belt-and-suspenders against future regressions), but the engine now
behaves correctly without it — the SDKs (Python, Node, Go, WASM,
FFI), the Tauri desktop, and any future embedder all benefit.

## How

Refactored process_command to return a richer struct, with a
backwards-compat wrapper:

  pub struct CommandOutput {
      pub status:   String,           // "INSERT Statement executed.", etc.
      pub rendered: Option<String>,   // SELECT prettytable; None otherwise
  }

  pub fn process_command(query, db) -> Result<String>
  // backwards-compat: returns just .status

  pub fn process_command_with_render(query, db) -> Result<CommandOutput>
  // new: rich return, never writes to stdout

The REPL (`src/main.rs`) calls `_with_render` and prints rendered
above status — same UX as before. The .ask meta-command does the
same and concatenates the two pieces into the single String it
bubbles up to the REPL's outer dispatch loop.

The CREATE-TABLE schema dump and INSERT row dump aren't preserved —
they were small UX niceties that interactive REPL users rarely need
(the schema you just CREATEd, the row you just INSERTed). The INSERT
case was actively bad UX on any non-trivial table — it dumped the
entire table after every insert. Both gone.

`sqlrite::process_command` keeps its existing signature for
backwards compatibility — every existing call site (Connection::execute,
the SDK FFI shims, the .ask handler's inline runner, the engine's
own tests) keeps working unchanged.

## Verified

- `cargo build --workspace` clean
- `cargo test --workspace --exclude sqlrite-{python,nodejs,desktop}` —
  330+ tests across engine + sqlrite-mcp + sqlrite-ask + sqlrite-ffi,
  all passing (engine's 251-test integration suite covers the
  process_command paths most directly affected)
- REPL smoke: CREATE / INSERT / SELECT round-trip prints rendered
  table above status, no surprises
- MCP smoke: same round-trip via JSON-RPC over stdio — `cat 2>/dev/null`
  filtered stderr is now empty (was prettytable noise before;
  stdio_redirect was the only thing keeping it off the protocol channel)

## Docs

- `docs/architecture.md` — updates the engine module map entries for
  src/lib.rs and src/sql/mod.rs to mention the new types + the
  "never writes to stdout" guarantee
- `docs/sql-engine.md` — entry-point section now shows both functions
  + the CommandOutput shape; explains the rendered-vs-status split
- `docs/mcp.md` — security-notes section reframes stdio_redirect as
  defense-in-depth now that the engine is clean
- `sqlrite-mcp/src/stdio_redirect.rs` — module-level comment rewritten
  to call out the engine fix + the three concrete future-regression
  scenarios the redirect protects against

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit 51e2e56 into main May 3, 2026
15 of 16 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