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
# 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"| 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) |
CommandStatementQuery → GetFlightInfo → DoGet |
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 |
Execute → DoGet |
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 → GetTables → SELECT 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 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 metadataTLS: 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.
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 5Both 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 tableseries_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.ticketsparrow 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 .mdJSON 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)# 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_KEYThe 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.
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.
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.
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 1 — sparrow 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.
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.
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 reproducesparrow 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.
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 .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 orientThen 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 tripConventions agents can rely on:
-
Two ways to read the same data.
sparrow sql "<query>"plans an arbitrary query — two round trips (GetFlightInfothenDoGet). 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 pullprints the ticket dialects; the server also advertises them in-band viaGetSqlInfo. Opaque-handle vendors reject client tickets — usesqlthere (sparrow doctor --serversays 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 csvare stable, parseable stdout formats — no ANSI, no decoration; row-count and timing summaries go to stderr. -
Exit codes:
0ok ·1query error ·2connection/auth ·3usage — branch on "server down" vs "my SQL was wrong". -
On exit
2, runsparrow 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) andsql --stats(timing/throughput anatomy on stderr) make measurements scriptable too. -
sparrow check <table> --key id -o jsonscreens a dataset's health without downloading it — exit1means findings, so it gates pipelines. -
sql --ipcreveals 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-namedelivers 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 mdto stdout caps at 1,000 rows by default so a carelessSELECT *can't flood a context window (the true total reports on stderr;--max-rowsoverrides). File sinks and data formats (csv/jsonl/json/arrow/parquet) always emit everything. -
Prefer
LIMITin SQL —--max-rowsstill downloads the full result. -
Profiles live in
~/.sparrow/config.json;sparrow profiles use <name>switches the default,-s grpc+tls://host:port --basic u:pworks ad-hoc.
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.
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).