sparrowcli

command module
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: Apache-2.0 Imports: 47 Imported by: 0

README

sparrow

test

A terminal client for any Apache Arrow Flight / Flight SQL server. One static binary: browse the catalog, run SQL, stream Arrow onward. Human-friendly on a TTY, machine-friendly in a pipe, agent-friendly by design.

Most Flight servers ship SDKs. Sometimes you just want to inspect one from a terminal. And the pipe is first-class: a table when you're reading, raw Arrow IPC when you're piping — the same command does both.

Status   ✔ works against five independent Flight SQL servers  ·  ✔ binaries for Linux · macOS · Windows Validated against   ✔ GizmoSQL (DuckDB)   ✔ Sparrow Flight   ✔ ROAPI (DataFusion)   ✔ Dremio OSS   ✔ InfluxDB 3 Core

Quick start — four commands

# a live 136-million-row Flight SQL server, open for exactly this:
sparrow connect grpc+tls://flight.sparrowflight.io:443 --basic demo:demo

sparrow ls
sparrow info series_data
sparrow sql "SELECT series_id, COUNT(*) FROM series_data GROUP BY 1 LIMIT 5"

Commands

command does wire calls
sparrow connect <uri> verify + save a profile vendor probe via GetSqlInfo, SELECT 1 fallback
sparrow orient one-shot markdown map: vendor, every table, every schema GetSqlInfo + GetTables w/ schemas
sparrow ls [pattern] list tables; the pattern is a server-side SQL LIKE (%, _, case-sensitive) GetTables — the one discovery RPC that works everywhere
sparrow info <table> schema, catalog, row count GetTables w/ schema; LIMIT 0 fallback
sparrow sql "<query>" run a statement (- = stdin, -f query.sql = file; --stats / --ipc stream anatomy; --schema = columns+types only; --cost estimates result size without streaming; --budget 10MB|5000rows|30s aborts a stream past a ceiling (exit 1); --receipt r.json writes a verifiable receipt of the result; --bigint-as-string for JS precision; --substrait plan.pb executes a Substrait plan) CommandStatementQueryGetFlightInfoDoGet
sparrow query <table> build the one-liner SELECT for you: --cols --where --order --limit; everything else works like sql same as sql
sparrow head <table> [n] preview the first n rows (default 10) — the SELECT * … LIMIT n you keep typing ExecuteDoGet
sparrow pull '<ticket>' Direct Pull (1-RTT): a ready ticket straight to the server — no GetFlightInfo, no SQL (doget is a hidden alias). Flight SQL reads are two round trips by design; servers that accept client-made tickets (Sparrow: JSON {"series": [...]} or {"sql": "…"}) serve known pulls in one — measured 143 vs 224 ms for the same 10k-row series over the public internet, the 81 ms gap being exactly the saved round trip (the win is one RTT, so it shrinks to nothing on a LAN). --accept-compression lz4 (the default) asks a negotiating server for a compressed wire — decoded transparently; --dry-run prints the final composed ticket (after that injection) and sends nothing; doctor --server probes which kind a server is DoGet only
sparrow ticket '<sql>' | --series a,b emit a reusable pull ticket (JSON) — save it, replay it forever with pull @file. A client ticket is stateless (re-run fresh each pull); a GetFlightInfo handle is single-use none (client-side)
sparrow profile <table> per-column nulls %, approx-distinct, min, max — one server-side pass one aggregate query
sparrow doctor layered connection diagnosis — names the layer that breaks (--server: Flight SQL conformance card — 10 surface probes incl. IPC compression) staged: DNS → TCP → TLS/ALPN → auth → GetTablesSELECT 1
sparrow check <table> data doctor: nulls, duplicate keys, staleness, frozen series, outliers. --strict fails on warnings · --fail-on keys,nulls gates the exit on named checks only (the rest still report) · --show-violations emits offending keys+values · --approx = memory-safe (HLL) uniqueness · --explain echoes each stage's SQL · --baseline prior.json gates on regressions server-side SQL aggregates — the table is never downloaded
sparrow expect '<sql>' --eq N | --rows 0 | --cols a,b assert something about a query and exit 1 if it fails — an agent's self-authored data contract. Scalar (--eq/--ne/--gt/--lt/--ge/--le, numeric-aware), row-count (--rows/--rows-min/--rows-max/--empty/--nonempty, wrapped in COUNT(*)), or shape (--cols, names in order); any combination, all must hold one query (counts never materialize)
sparrow verify <receipt.json> re-run a receipt's query and confirm the result fingerprint still matches (sql --receipt r.json writes one) — provable provenance: exit 0 = the number is real, exit 1 = changed/tampered. -s verifies the same query against another server one server-side fingerprint aggregate
sparrow replay <session.jsonl> re-run a recorded investigation and confirm every step reproduces (set SPARROW_SESSION=file and each read appends a fingerprinted step) — exit 1 if any drifted, or if nothing in the file was verifiable. -s replays the whole thing against another server one fingerprint aggregate per step
sparrow mcp [-s profile] serve orient / sql / pull / expect / verify, the diagnostics (doctor / check / ping, plus sql stats-benchmark mode) and client-side version / whatsnew / feedback over MCP stdio — chat agents without a shell (Claude Desktop, claude.ai) drive the bound Flight server through schema-validated tool calls on ONE warm connection dialed lazily, kept warm
sparrow diff <table> --against <b> drift gate: schema, COUNT(*), --time bounds, numeric fingerprint vs a second server — exit 1 on drift conservative aggregates on both sides; nothing downloaded
sparrow dataset compare <A> <B> [C...] drift gate for the FILES: folder-shaped sources (dirs, \shares, s3://, s3+http(s)://host/bucket, https) — per-file table (verdicts incl. DIFFERS when direction is unknowable), --deep per-series max(period), exit 1 on drift · 2 unreachable sparrow dataset push <master> <replica...> heals writable replicas (mtime-honest; _INDEX.json regenerated)
sparrow audit security surface: what client SQL can reach beyond queries — file reads, dir listing, writes, SSRF, config tamper, catalog writes (CREATE/DROP). Exit 1 if exposed benign probes (incl. a create-then-drop round-trip); run against a server you operate
sparrow ping separate network latency from server latency, as percentiles bare TCP connect vs a no-match GetTables on the warm channel
sparrow feedback "msg" send feedback to the sparrow maintainers HTTPS to sparrowflight.io — independent of whichever server you use
sparrow completion bash|zsh|fish shell tab-completion script
sparrow profiles list saved connections (use <name> / rm <name>)

Auth — the two adapters that cover the whole tested landscape:

--basic user:pass                          # GizmoSQL, Dremio, Sparrow (API key as user
                                           # works; Bearer handoff adopted automatically)
--bearer TOKEN --header database=mydb      # InfluxDB 3 style: token + per-call metadata

TLS: grpc:// plain, grpc+tls:// verified, --tls-skip-verify for self-signed. The CLI identifies the server by trying GetSqlInfo first, then SELECT version() as a fallback — Dremio answers the second, InfluxDB the first; between them, every server identifies itself.

Direct Pull (1 RTT) — pull

A Flight SQL read is two round trips by design: GetFlightInfo to plan, then DoGet to stream. A Direct Pull sends a client-made ticket straight to the server and skips the plan — one round trip. The same WTI crude series, fetched both ways — an ordinary query the server plans, and a Direct Pull:

# the ordinary way — a query the server plans (2 round trips)
sparrow sql "SELECT series_id, period, value FROM series_data WHERE series_id='PET.RWTC.D' ORDER BY period" -o table --max-rows 5

# a Direct Pull — a ready ticket straight to the server (1 round trip)
sparrow pull '{"series":["PET.RWTC.D"]}' -o table --max-rows 5

Both print the identical rows:

series_id   period    value
PET.RWTC.D  19860102  25.56
PET.RWTC.D  19860103  26
PET.RWTC.D  19860106  26.53
PET.RWTC.D  19860107  25.85
PET.RWTC.D  19860108  25.87

A ticket comes in two dialects — {"series":[…]} by key, or {"sql":"…"} for an arbitrary read-only query:

sparrow pull '{"sql":"SELECT series_id, COUNT(*) AS n FROM series_data GROUP BY 1 ORDER BY n DESC LIMIT 3"}' -o table
series_id                      n
FRED.DFF                       13340
PET.EER_EPD2F_PE3_Y35NY_DPG.D  11117
PET.EER_EPD2F_PE1_Y35NY_DPG.D  11113

Default output is raw Arrow IPC when piped — so it composes (| duckdb, > series.arrows) — and an aligned table on a TTY; -o picks explicitly. --stats shows the round trip you skipped (plan (skipped: 1-RTT)) and the codec you negotiated (see Measure). pull is the same operation as the Flight DoGet RPC — which is why doget still works as a hidden alias. Opaque-handle vendors — most Flight SQL servers — don't accept client tickets; use sql there, and sparrow doctor --server probes which kind a server is.

A client ticket is a reusable, durable artifact — the server re-runs it fresh on every pull, so save one and replay it forever (it survives restarts; a GetFlightInfo statement handle does not — that's single-use, consumed on its first DoGet). sparrow ticket writes one for you, JSON-escaped:

sparrow ticket "SELECT period, value FROM series_data WHERE series_id='PET.RWTC.D'" > wti.ticket
sparrow pull @wti.ticket -o md          # replay it, 1 RTT each, as often as you like
sparrow ticket --series PET.RWTC.D,FRED.DFF --start 2020-01-01 > two.ticket

Output — pick your consumer

sparrow sql "..."                    # TTY: aligned table · pipe: raw Arrow IPC
sparrow sql "..." -o md              # markdown table
sparrow sql "..." -o csv             # CSV (empty cell = NULL)
sparrow sql "..." -o jsonl           # one JSON object per row
sparrow sql "..." -o json            # JSON array
sparrow sql "..." -o data.parquet    # file sink: .parquet .csv .json .jsonl .arrow .md

JSON note: 64-bit integers are emitted as JSON numbers at full precision. JavaScript's JSON.parse silently loses precision above 2^53 — use a bigint-aware parser, or cast to text in SQL.

In a pipe, the default is a raw Arrow IPC stream — columnar data stays columnar all the way:

sparrow sql "SELECT period, value FROM series_data WHERE series_id='PET.RWTC.D'" \
  | duckdb -c "LOAD arrow; SELECT COUNT(*), MIN(value), MAX(value) FROM read_arrow('/dev/stdin')"
# → 10217 · -36.98 · 145.31 — forty years of WTI without leaving Arrow
# (one-time setup: duckdb -c "INSTALL arrow FROM community" — read_arrow is a
#  community extension, so the explicit LOAD is required; it never autoloads)

Security

# private CA + mTLS — for Flight deployments that require client certificates
sparrow connect grpc+tls://flight.corp:443 \
  --tls-ca ca.crt --tls-cert client.crt --tls-key client.key

# sealed exports — in-spec Parquet Modular Encryption (AES-GCM)
sparrow sql "SELECT ..." -o data.parquet --encrypt-key env:SPARROW_KEY

The encryption key is hex (16/24/32 bytes), env:VAR, or file:path. DuckDB, Spark and pyarrow read the file back with the key — and refuse it without. The exact DuckDB recipe (key handed over as base64 of the same bytes):

PRAGMA add_parquet_key('k', '<base64 of the key bytes>');
SELECT * FROM read_parquet('data.parquet', encryption_config = {footer_key: 'k'});

Prefer 32-byte keys (64 hex digits). DuckDB guesses whether the key string is raw bytes or base64 by its length — and base64 of a 16/24-byte key is exactly 24/32 characters, a valid raw-key length that DuckDB tries first, ending in a spurious "AES tag differs" error. A 32-byte key encodes to 44 characters and is unambiguous. (Found by an external tester driving this CLI — thanks.) mTLS verified against an Envoy that requires client certificates: no cert → refused (exit 2); cert → query runs.

Doctor — when the connection doesn't work

Every connection failure looks the same from a client: "connection error." sparrow doctor walks the stack — config → DNS → TCP → TLS → auth → Flight SQL → round trip — and names the layer that breaks, with evidence:

$ sparrow doctor -s grpc+tls://fixture:31337 --basic user:pass
 ✓ config    profile "(ad-hoc)" · auth basic · TLS system roots
 ✓ dns       fixture → 192.168.132.91 (18 ms)
 ✓ tcp       connected to 192.168.132.91:31337 (2 ms)
 ✗ tls       tls: failed to verify certificate: x509: certificate signed by unknown authority
             wire presented: subject "localhost" · issuer "Norton Web/Mail Shield Untrusted Root"
             hint: if that issuer is not your server's CA, something between you and the
             server is intercepting TLS (antivirus HTTPS scanning, corporate proxy)
 · auth      not reached

3 ok · 0 warn · 1 fail — first failure at tls

That capture is real — a self-signed fixture that "wouldn't verify" turned out to be local antivirus re-signing the connection, and doctor shows the swapped certificate straight off the wire. On a healthy endpoint it reports the TLS version, ALPN (gRPC needs h2; doctor says so when a proxy won't negotiate it), certificate issuer and expiry, the auth handshake, the vendor, and a timed round trip. -o json for scripts; exit 2 if any layer fails.

Measure — is it the network or the server?

sparrow ping runs two round trips per round — a bare TCP connect (pure network) and a lightweight RPC on the warm, authenticated channel (network + server) — and summarizes the percentiles. The gap between the two is the server:

$ sparrow ping -n 5 -s grpc+tls://flight.sparrowflight.io:443 --basic demo:demo
round  1   tcp    75.9 ms   rpc    86.1 ms
...
            min     p50     p95     max
tcp        61.3    63.4    75.9    75.9  ms
rpc        75.4    81.4    86.1    86.1  ms   (5/5 ok)

≈ network 63.4 ms + server 18.0 ms (medians)

sparrow sql --stats breaks a query's wall clock into its anatomy — plan, first byte, stream — plus rows, bytes that actually crossed the wire (counted at the gRPC layer), throughput, pacing, and the per-column type/encoding breakdown. Half a million rows over the public internet:

$ sparrow sql "SELECT * FROM series_data LIMIT 500000" --stats > /dev/null
── query stats ─────────────────────────
plan (GetFlightInfo)      79 ms
first byte               404 ms
stream (DoGet)          2304 ms
total                   2384 ms
rows       500,000 in 245 batches · rows/batch p50 2,048 (min 288 · max 2,048)
wire       22.5 MB received · decodes to 22.4 MB (1.0×) · no body compression declared
speed      78 Mbit/s over the stream
pacing     gaps p50 4.2 ms · p95 22.7 ms · max 164.7 ms — 82% of the stream is
           waiting (paced upstream: sender or network stalls between batches)
column     type     encoding  nulls  decoded
series_id  utf8     plain     0      13.2 MB (59%)
period     utf8     plain     0      5.2 MB (23%)
value      float64  plain     0      4.0 MB (18%)

That's the stream's full anatomy: the server's batch signature (2,048-row chunks — DuckDB's vector size showing through), whether the wire is actually compressed — read from the IPC message header's declared codec, so a compressed stream prints codec lz4_frame (or zstd) with the ratio corroborating it — what every column arrives as (type, arrow-level encoding, nulls, share of the bytes), and whether the stream is paced by the wire or by gaps upstream — measured excluding the client's own write time, so a slow local sink doesn't skew the verdict. Run the same pull against two servers and the differences name themselves.

Prefer the raw view? sql --ipc prints the message-by-message IPC manifest instead: type, rows, body bytes, declared codec, custom metadata.

The example above reads no body compression declared because a sql query is a 2-RTT read — the opaque statement ticket can't negotiate a codec. To request a compressed wire, use a Direct Pull (Sparrow serving nodes accept JSON tickets): sparrow pull '{"series":["…"]}' --accept-compression lz4 (lz4 is on by default; --accept-compression "" opts out). A negotiating server compresses only for a codec the client lists, arrow-go decodes it transparently, and the same --stats/--ipc view then prints codec lz4_frame with the ratio. sparrow doctor --server probes whether a server offers it at all.

$ sparrow pull '{"series":["PET.RWTC.D"]}' --stats > /dev/null
plan (skipped: 1-RTT)      0 ms
rows       10,217 in 5 batches · rows/batch p50 2,048
wire       172.5 KB received · decodes to 347.4 KB (2.0×) · codec lz4_frame

Same 10k-row series, half the bytes on the wire — the server compressed it because the client (arrow-go here) advertised lz4, and the ratio comes back in the same stats line. Compression trades CPU for bandwidth: a clear win over a wide-area link, a wash or worse on a fast LAN where the wire was never the bottleneck — so it is negotiated, never forced.

Stats go to stderr, so they compose with every output format and pipe. Every benchmark number this project publishes is reproducible with this flag. ping -o json for scripts; both work against any Flight SQL server.

Check — a doctor for the data itself

sparrow check <table> runs a statistical health screen server-side — every check is one conservative SQL aggregate, the table is never downloaded, and anything a dialect rejects degrades to a skip instead of aborting:

$ sparrow check checkme --key k --max-age 7d
 ✓ table     3 columns · key k · time t (auto-detected)
 ✓ rows      60
 ✓ nulls     v 8.3%
 ✗ keys      1 duplicated (k, t) groups
             e.g. DUP
 ⚠ time      t spans 2026-06-01 → 2026-06-30 · newest point 13.7 days old
             hint: older than --max-age 7d — is the feed still running?
 ✓ coverage  5 entities · rows per entity: min 1 · avg 12 · max 30
 ⚠ frozen    1 entities have a constant v across ≥10 observations
             e.g. FLAT
 ✓ numeric   v: min 1 · max 900000 · avg 16432.25

5 ok · 2 warn · 1 fail — checked in 0.0 s (10 queries, server-side)

--key names the entity key (uniqueness is checked on (key, time) when --time is set, and temporal columns are auto-detected). Duplicate keys and NULLs in key columns are failures and exit 1sparrow check t --key id && deploy works as a data gate in CI (--strict widens the gate to warnings, and a sub-check the server couldn't execute is an ! error, never a silent pass). Staleness, frozen series (a constant value across ≥10 observations — a stuck feed's signature), dead columns and σ-outliers are warnings. -o json for pipelines and agents.

Prove it — verifiable receipts

When an agent (or a person) reports a number from the data, a receipt makes it checkable instead of taken on faith:

sparrow sql "SELECT count(*) FROM series_data WHERE value < 0" -o md --receipt neg.json
sparrow verify neg.json
#  ✓ server    EnergyScope 1.0
#  ✓ rows       10,217
#  ✓ digest     matches

--receipt writes a manifest — the query, the server's identity, a timestamp, and an order-independent content fingerprint of the result (a row count plus two independent digests, computed server-side via hash() aggregates, so nothing extra is downloaded and row order doesn't matter). sparrow verify re-runs the query and confirms the fingerprint, the result's column shape, and — against the receipt's own endpoint — that the server still identifies as the one recorded: exit 0 = the number is real, exit 1 = the data/shape/server changed or the receipt was tampered (a fabricated row count, digest, columns, or vendor is caught), exit 2 = unreachable/auth, exit 3 = malformed receipt. verify -s <other> runs the same query against a different server — a clean way to ask "do two engines agree?" (they do here: flight and flight2 return an identical digest over the same 136M-row snapshot). A query that isn't deterministic server-side (now(), random()) won't verify — which is the honest result.

Compose it with expect: assert a contract, receipt the result, let a third party verify it. Analysis you can check, not just trust.

Reproducible investigations — record & replay

A receipt proves one number. A session proves a whole exploration. Set SPARROW_SESSION=<file> and every read appends a replayable step — the query, the endpoint, the row count, and (for SQL) the same content fingerprint:

export SPARROW_SESSION=probe.jsonl
sparrow sql "SELECT count(*) FROM series_data WHERE value < 0"
sparrow sql "SELECT max(value) AS wti_high FROM series_data WHERE series_id='PET.RWTC.D'"
unset SPARROW_SESSION

sparrow replay probe.jsonl
#  ✓ step 1  sql   reproduces
#  ✓ step 2  sql   reproduces
#  2/2 verifiable steps reproduce

sparrow replay re-runs each SQL step against its endpoint and diffs the fingerprint — exit 0 if the investigation still holds, exit 1 if any step drifted (it names which, and whether the row count, the content, or the column names changed) — or if the file has no verifiable steps at all, so CI gating on replay && … can't be fooled by a pull-only session. replay -s <other> runs the entire investigation against a different server — "how I arrived at this" turned into a regression test you (or a reviewer, or CI) can re-run. Pull steps are recorded for the narrative but marked "not checked" (no server-side fingerprint). It's the reusable-ticket idea lifted from one query to a whole exploration. One caveat: the SQL text is recorded verbatim (replay must re-run it) — credential flags are redacted, but a secret inside a query literal is not. Keep secrets out of SQL you record.

Install

Download a binary from the releases page (Linux, macOS and Windows; amd64 + arm64), unpack it and put sparrow on your PATH. Checksums included. Or install with Go, or build from source:

go install github.com/balicat/sparrowcli@latest   # installs as `sparrowcli` — rename if you like

go build -o sparrow .        # Go ≥ 1.25; pure Go, no cgo — trivially cross-compiles
GOOS=windows go build -o sparrow.exe .

For AI agents (Claude Code, etc.)

sparrow agent prints a complete, self-contained manual (markdown) for driving the CLI — every command, the sql-vs-pull decision, the output and exit-code conventions, how to call server-advertised macros like full-text search. Point an agent at it once (sparrow agent > SPARROW.md) and it can operate the tool with no other docs. sparrow agent --json emits the same surface as a machine-readable capability catalog (commands, flags, exit codes, ticket dialects) for programmatic bootstrap against an unknown version. The rest of this section is the summary.

No shell? sparrow mcp. Chat agents without a terminal — Claude Desktop, claude.ai integrations, Slack — get the same verbs over the Model Context Protocol: sparrow mcp -s <profile> serves orient / sql / pull / expect / verify as MCP tools (stdio) against the one server it's bound to — plus the diagnostic layer (doctor with its conformance card, check, ping, and sql's stats-benchmark mode) and version, whatsnew (release notes, live from the feed), and feedback (file a bug/idea to the maintainers; needs no Flight server at all — reports name the MCP client that filed them). What the shape adds over shelling out: a warm connection (one dial + auth held across calls — a CLI invocation re-dials every time), schema-validated calls (SQL rides as a JSON field; the shell-quoting failure class is gone), and results row-capped by default so a query can't flood a model's context. It's a first-class MCP citizen, not CLI verbs in a trenchcoat: every tool carries honest read-only annotations (hosts can auto-approve; feedback is marked outward because it really does send), the server map and per-table schemas are resources (sparrow://orient, sparrow://table/<name>) a host can load as ambient context, two starter prompts ship in-box, and every table/JSON result carries typed structuredContent so agents never re-parse markdown. Point sparrow at any Flight SQL server and that server now speaks MCP:

{ "mcpServers": { "sparrow": { "command": "sparrow", "args": ["mcp", "-s", "sparrow"] } } }

AI agents don't need a Flight client library — they can just call the CLI. One command maps a Flight server — vendor, tables, schemas, as markdown:

sparrow orient

Then query with results the agent reads natively:

sparrow info series_data                          # row count for one table
sparrow sql "SELECT ... LIMIT 20" -o md           # arbitrary query, readable results
echo "SELECT ..." | sparrow sql - -o md           # SQL via stdin — no shell-quoting battles
sparrow pull '{"series":["PET.RWTC.D"]}' -o md     # a known series in ONE round trip

Conventions agents can rely on:

  • Two ways to read the same data. sparrow sql "<query>" plans an arbitrary query — two round trips (GetFlightInfo then DoGet). When you already know exactly what you want, sparrow pull '<ticket>' sends a ready ticket straight to the server in one round trip: a {"series":[…]} key or a {"sql":"…"} string, lz4-compressed by default. sparrow help pull prints the ticket dialects; the server also advertises them in-band via GetSqlInfo. Opaque-handle vendors reject client tickets — use sql there (sparrow doctor --server says which kind a server is). Rule of thumb: exploring → sql; a known series or a fixed query you'll repeat → pull.

  • -o md / -o jsonl / -o csv are stable, parseable stdout formats — no ANSI, no decoration; row-count and timing summaries go to stderr.

  • Exit codes: 0 ok · 1 query error · 2 connection/auth · 3 usage — branch on "server down" vs "my SQL was wrong".

  • On exit 2, run sparrow doctor -o json — a layer-by-layer diagnosis (DNS, TCP, TLS/ALPN, auth) as structured JSON, instead of guessing.

  • sparrow ping -o json (latency percentiles, network-vs-server split) and sql --stats (timing/throughput anatomy on stderr) make measurements scriptable too.

  • sparrow check <table> --key id -o json screens a dataset's health without downloading it — exit 1 means findings, so it gates pipelines.

  • sql --ipc reveals the stream's raw IPC manifest — every message's type (schema / dictionary / record batch), rows, body bytes, declared codec and custom-metadata count — wire-level introspection without a packet capture.

  • Found a bug or have an idea? sparrow feedback "..." --from your-name delivers it to the sparrow maintainers directly — independent of whichever Flight server you're connected to, so it works even when the server is the problem. Agents are explicitly welcome to use it.

  • -o md to stdout caps at 1,000 rows by default so a careless SELECT * can't flood a context window (the true total reports on stderr; --max-rows overrides). File sinks and data formats (csv/jsonl/json/arrow/parquet) always emit everything.

  • Prefer LIMIT in SQL — --max-rows still downloads the full result.

  • Profiles live in ~/.sparrow/config.json; sparrow profiles use <name> switches the default, -s grpc+tls://host:port --basic u:p works ad-hoc.

The landscape (as of July 2026)

Other CLIs can reach a Flight SQL server — none preserve Arrow end-to-end:

scope Arrow stays Arrow?
flight_sql_client (apache/arrow-rs) any Flight SQL server ✗ — "basic" example binary, one-shot RPC commands, text out
timvw/arrow-flight-sql-client any Flight SQL server ✗ — small RPC-mirror CLI, text out
usql 40+ databases; Flight SQL as one driver ✗ — excellent universal shell, but results flatten through database/sql to rows and text
bendsql (né "Arrow CLI") Databend only — what happens when an Arrow CLI grows up inside one vendor
sparrow any Flight SQL server ✓ — columnar from Flight server to downstream process: raw IPC pipe, parquet sinks, typed formats

The gap sparrow fills isn't "a CLI exists" — it's Arrow-native ergonomics: catalog discovery over the Flight RPCs, profiles, orient, output that follows the consumer, and conventions agents can script against.

The Sparrow family

One transport, many clients: Sparrow (the Flight server) · sparrowJS (the browser) · sparrowXL (Excel) · sparrowMCP (AI agents) · sparrowCLI (the terminal).


Dialect note: your SQL passes through untouched — quirks are the server's business (e.g. Dremio rejects aliases on FROM-less SELECTs; the CLI reports the server's error verbatim and exits 1).

License

Apache-2.0

Documentation

Overview

agent — `sparrow agent` prints a complete, self-contained operator's manual for an AI agent (Claude Code et al.) driving the CLI. One command, one markdown document, no server required: everything an agent needs to discover a Flight server, read data the fast way, parse the output, and recover from errors. Point an agent at `sparrow agent` once and it can operate the tool.

audit — a security-surface probe for a Flight SQL server you operate.

A Flight SQL client sends arbitrary SQL, and a DuckDB-backed server runs it on a connection with DuckDB's full default powers: reading host files, listing directories, writing files, fetching URLs (SSRF), and changing server-wide configuration (raise memory_limit to OOM the node, or re-enable any of the above). This command probes each of those with a BENIGN version — read /etc/hostname, list /, write /dev/null, connect to a dead loopback port, flip an inert setting — and reports which the server permits.

Run it against a server you operate or are explicitly authorized to test. It is a defender's tool: it verifies the hardening (enable_external_access=false · allowed_directories · lock_configuration), it does not exploit. Note: on an unhardened DuckDB server the net probe makes the server autoload the httpfs extension (a one-time download).

check — the data doctor: a statistical health screen for one table, computed SERVER-SIDE with conservative SQL (COUNT / COUNT DISTINCT / MIN / MAX / AVG / STDDEV / GROUP BY ... HAVING — the subset that ran on every vendor we validated). The table itself is never downloaded.

Checks: rows, null census, duplicate keys, time span + staleness, per-entity coverage, constant ("frozen") series, numeric ranges with a crude sigma-based outlier flag. Anything a dialect rejects degrades to "skip" with the server's error — one exotic server must not kill the whole checkup.

completion — static shell completion scripts (bash, zsh, fish).

The command/flag tables below are maintained BY HAND next to the flag definitions they mirror. That drift risk is accepted: the flag surface is small and changes rarely, and static tables keep the scripts dependency- free and instant. When adding a command or flag, update this file.

conform — the Flight SQL conformance card (sparrow doctor --server).

doctor asks "can I reach this server?"; the card asks "which parts of the Flight SQL surface does it actually implement?" — the answer differs per vendor (GizmoSQL, InfluxDB 3, Dremio, EnergyScope all diverge somewhere) and knowing WHICH RPCs work is what makes one client portable. The card is informational: unsupported surfaces are warns, not failures, and the exit code is always 0 once the dial succeeds.

dataset — compare and sync FOLDER-SHAPED dataset sources (the Sparrow dataset contract: PREFIX.vortex / PREFIX.parquet + _META.parquet + _ADAPTERS.json + _INDEX.json), across plain folders, network shares, s3-compatible stores (MinIO, R2, AWS) and read-only https publishers.

The sibling of `sparrow diff`: diff gates SERVER replicas, dataset gates the FILES those servers (and Excel's folder tier) are built from.

sparrow dataset compare <A> <B> [C...]     drift table, exit 1 on drift
sparrow dataset push <MASTER> <replica...> heal replicas from a master

Source forms:

C:\data\energy · \\NAS\share\energy        local / UNC folder
https://host/path                          read-only (needs _INDEX.json)
s3://bucket/prefix                         endpoint from SPARROW_S3_ENDPOINT
                                           or AWS_ENDPOINT_URL; creds from
                                           AWS_ACCESS_KEY_ID/SECRET
s3+http://host:9000/bucket/prefix          explicit endpoint (MinIO)
s3+https://acct.r2.cloudflarestorage.com/bucket/prefix   (R2)
s3+https://KEY:SECRET@host/bucket/prefix   inline creds (scripts; prefer env)

diff — compare one table across two servers (the drift gate).

Made for the "is the replica current?" question: schema, row count, time bounds, and a numeric fingerprint, side by side. Identical → exit 0; any difference → exit 1, so a cron line can gate on it. The B side is a profile name or an ANONYMOUS URI — for an authenticated ad-hoc B, save a profile first (sparrow connect ... --name b).

expect — `sparrow expect "<sql>" --eq N | --rows 0 | --cols a,b …` turns any query into a self-verifying assertion: exit 0 if it holds, 1 if it doesn't.

Where check/diff gate over FIXED dimensions (nulls, dup keys, drift), expect gates over an ARBITRARY query — the primitive an agent uses to pin a finding as a durable, replayable data contract:

sparrow expect "SELECT count(*) FROM series_data" --eq 136052269
sparrow expect "SELECT * FROM series_data WHERE value IS NULL" --empty
sparrow expect "SELECT * FROM search_meta('brent', lim:=5)" --cols series_id,name,description,score,total_matches

Any combination of assertions may be given; ALL must hold. Row-count checks wrap the query in COUNT(*) (never materialized); scalar checks read the first cell of the first row; --cols reads the schema via a LIMIT 0 probe.

feedback — send a message to the sparrow maintainers. Deliberately INDEPENDENT of whatever Flight server you're connected to: it POSTs to the fixed receiver at sparrowflight.io, so it works from any server, or with no working server at all — which is exactly when you need it. Built so AI agents driving this CLI have a way to file what they hit: `sparrow feedback "orient chokes on catalog X" --category bug --from claude`.

sparrow — a terminal client for any Arrow Flight / Flight SQL server. M0: connect · ls · sql · TTY table / Arrow IPC pipe output.

mcp — `sparrow mcp` serves the CLI's core verbs over the Model Context Protocol (JSON-RPC 2.0 on stdio), so chat agents WITHOUT a shell — Claude Desktop, claude.ai integrations, Slack — can drive any Flight SQL server. The generic play, one hop further: point sparrow at a server and that server now speaks MCP.

What the shape buys over shelling out to the CLI:

  • a WARM connection: one dial + auth held across calls (a CLI invocation re-dials every time — ~150 ms that swamps an 8 ms pull)
  • schema-validated calls: SQL rides as a JSON string field — the entire shell-quoting failure class is gone
  • reach: hosts with no terminal at all

Five tools in this first slice — orient, sql, pull, expect, verify — each a thin wrapper over the SAME internals the CLI commands use (orientMarkdown, queryRows, withAcceptCompression, compareScalar, fingerprint). Tool descriptions embed cmdDesc from completion.go so the catalog can't drift from the CLI's own vocabulary (mcp_test.go pins the correspondence).

Protocol notes: newline-delimited JSON-RPC per the MCP stdio transport; stdout carries ONLY protocol frames (all logging goes to stderr); tool failures are results with isError=true (protocol errors are reserved for malformed requests / unknown methods / unknown tools).

preflight — result-size awareness and enforcement, so an agent can size a pull BEFORE it floods its own context or hammers a server.

--cost   (sql/query): estimate rows + decoded bytes WITHOUT streaming the
         result — count(*) for rows, a first-batch bytes/row extrapolation
         for size. The "how much" sibling of --schema's "what shape".
--budget (sql/query/pull/head): a hard ceiling — "10MB" | "5000rows" |
         "30s". The stream is aborted the instant it crosses the ceiling,
         with a clean error (exit 1). estimate -> decide -> enforce.

profile — a one-pass column profiler: per-column null %, distinct estimate, min and max, computed SERVER-SIDE in a single query. `check` screens a table for problems; `profile` describes its columns' distributions.

query — sugar over sql: build the one-liner SELECT for you. Agents write SQL; humans at 11pm write `sparrow query t --where "x>3"`.

receipt — verifiable data provenance. `sparrow sql "..." --receipt r.json` writes a manifest: the query, the server's identity, a timestamp, and an order-independent content fingerprint of the result. `sparrow verify r.json` re-runs the query against the server and confirms the fingerprint still matches — proof that a number REALLY came from that query against that server, and wasn't invented.

The fingerprint is computed server-side and is order-independent: count(*) plus sum(hash(all cols)) and bit_xor(hash(all cols)). Two independent digests over the row multiset — no download, no ORDER BY needed, and duplicate rows don't cancel (sum adds where xor would xor to zero). A query that isn't deterministic server-side (now(), random()) won't verify — which is correct: a receipt proves reproducibility.

session — reproducible investigations. Set SPARROW_SESSION=path and every read (sql/query/pull/head) appends a JSONL record: the command, the endpoint, the query/ticket, the row count, and — for SQL — the same order-independent content fingerprint receipts use. `sparrow replay <session.jsonl>` re-runs the recorded reads and diffs each against its fingerprint, so "here's how I got this number" becomes "…and it still reproduces." Investigation-as-regression- test — the reusable-ticket idea lifted from one query to a whole exploration.

ticket — `sparrow ticket` emits a REUSABLE Sparrow pull ticket (JSON) to stdout: the inverse of `sparrow pull`. Save it once, replay it any number of times with `sparrow pull @file`, each a 1-RTT Direct Pull.

Why this is the reusable artifact (and a GetFlightInfo handle is not): a Flight SQL statement ticket from GetFlightInfo is a server-minted handle, consumed on the first DoGet (single-use). A client-constructed ticket is stateless — the server re-runs it fresh every DoGet, so it works forever and survives restarts (it's just text). This command only formats the JSON; no server round trip, so it works offline.

whatsnew — what changed in recent sparrow releases. The content is the GitHub releases feed (whose notes goreleaser generates from commit subjects), fetched live — no hand-written changelog to drift. Works with no Flight server configured; shared by the CLI command and the MCP tool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL