Skip to content

REPL slice 3: table output in TTY, JSON in pipes #989

Closed

Description

@thekevinbot

Slice 3 of 3 under #953. Stacks on #988 — open the PR with base set to slice 2's branch.

Scope

Render query results as a table when stdout is a TTY, keeping the JSON array for pipes.

Why

Slices 1 and 2 reuse the existing JSON array serialization, which is right for dirsql query and wrong for a REPL. SELECT * FROM './' in a 5k-file repo dumps a 5000-element JSON array at a human being.

The two modes have genuinely different consumers: one-shot output is piped to jq, REPL output is read with eyes. sqlite3 prints a table for the same reason. This is a real product decision, which is why it is its own slice rather than a detail of slice 1.

Behavior

  • stdout is a TTY → table
  • stdout is piped/redirected → JSON array, byte-identical to dirsql query today

Note this keys on stdout, not stdin. dirsql > out.json from an interactive terminal should still produce JSON — the destination decides the format, not the input source. Worth confirming that reading against slice 1's stdin-based TTY check, which governs prompting, a separate concern.

Open question — resolve before implementing

Once two formats exist, can the user choose?

.mode is the obvious answer and dot-commands were explicitly ruled out (see #953). Alternatives:

  1. Infer from TTY only — no user control. Simplest, no new surface, but no way to get JSON interactively or a table in a pipe.
  2. A --format {auto,table,json} flag valid in both REPL and one-shot mode, defaulting to auto. Adds one documented flag instead of a meta-command namespace, and benefits dirsql query too.

Option 2 is the better fit if control is wanted at all: it stays in flag-space rather than reopening the dot-command question. Decide before implementing.

Also to settle

  • Column width strategy: truncate, wrap, or terminal-width-aware. Long content values will dominate otherwise.
  • NULL rendering, and how blobs/binary display.
  • Row-count footer, and whether large results need paging (probably out of scope — say so explicitly rather than silently omitting).

Testing

  • Snapshot testing is the natural fit: curtaincall's term.to_snapshot() serializes the screen as a box-drawn string, designed to pair with syrupy. Snapshot the rendered table over a fixed fixture directory.
  • Pipe path: assert the JSON is byte-identical to dirsql query for the same SQL — that is the regression that matters most here.
  • Unit: table-rendering logic is pure, so it unit-tests cleanly with no mocking. Plus unit mutation.

Checklist

  • Open question above resolved and recorded in the PR
  • Red tests pushed as their own commit; CI observed red for the right reason before implementation
  • Changelog fragment under packages/rust/changelog.d/
  • Migration fragment if --format (or equivalent) changes any existing output path
  • Docs: docs/reference/cli.md documents both formats and the selection rule
  • PARITY.md: no drift (CLI is one shared implementation)
  • Re-attest both bindings
  • Manual run — eyeball the table in a real terminal, then pipe it and diff against dirsql query

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions