README
¶
spannersh
Yet another interactive tool for Google Cloud Spanner, following cloudspannerecosystem/spanner-cli (the basis of the current official Spanner CLI) and apstndb/spanner-mycli: run SQL from the terminal, print result tables (with column types), and show the query plan.
It stays intentionally simple by reusing existing Go packages for the driver, REPL, plan rendering, and value formatting instead of re-implementing that machinery in this repository.
Because spannersh delegates query execution to go-sql-spanner through Go's database/sql, it can use the Cloud Spanner driver capabilities documented in the official drivers overview for the Go database/sql path. This repository mainly adds the interactive shell, client-side EXPLAIN handling, and result rendering on top of that driver behavior.
Prerequisites
- Go 1.26.2+
- Google Cloud credentials with access to the target instance (for example Application Default Credentials from
gcloud auth application-default login).
The go-sql-spanner driver detects whether the database uses GoogleSQL or PostgreSQL dialect after connect unless you override this via DSN parameters (see --dsn-suffix and the driver documentation). The shell’s --dialect aligns client-side statement splitting with that choice; use --dialect postgresql when working against a PostgreSQL-dialect database so semicolons inside PostgreSQL-specific literals are handled like the driver’s parser.
Install
go install github.com/apstndb/spannersh@latest
You can also install it with mise:
mise use -g github:apstndb/spannersh@latest
# or install from the Go module
mise use -g go:github.com/apstndb/spannersh@latest
For go install module@version, --version prints the module version embedded in the binary (runtime/debug.ReadBuildInfo Main.Version — the tag you asked for, or a pseudo-version for commits without a tag). Local go build / go install from a checkout usually reports dev because Main.Version is (devel) unless you pass -ldflags (see below).
If you clone this repository, build from the module root:
go build -o spannersh .
To force an explicit version string (for example from git describe in CI or release builds), override the link-time default:
go build -o spannersh -ldflags "-X main.version=$(git describe --tags --always --dirty)"
Usage
spannersh -p PROJECT -i INSTANCE -d DATABASE [--verbose] [--dialect DIALECT] [--dsn-suffix PARAMS] [--format FORMAT]
spannersh --help
Usage: spannersh [flags]
Interactive shell for Google Cloud Spanner.
Flags:
-h, --help Show context-sensitive help.
--version Print version and exit.
-p, --project=PROJECT Google Cloud Project ID. Default: SPANNER_PROJECT_ID.
-i, --instance=INSTANCE Spanner instance ID. Default: SPANNER_INSTANCE_ID.
-d, --database=DATABASE Database ID. Default: SPANNER_DATABASE_ID.
-v, --verbose Print the full query_stats block instead of the default summary subset.
--dsn-suffix=PARAMS Extra go-sql-spanner DSN parameters (snake_case; semicolon-separated). See docs.
--dialect=DIALECT Client-side SQL parser dialect: auto, google-standard-sql, or postgresql. PostgreSQL adds dialect=POSTGRESQL to the DSN unless --dsn-suffix already sets dialect=.
--format=FORMAT Output format: table, csv, or jsonl. EXPLAIN plan output is always a text plan tree.
| Flag | Meaning |
|---|---|
--version |
Print version and exit. |
-p, --project |
GCP project ID (required unless set). Default: env SPANNER_PROJECT_ID. |
-i, --instance |
Spanner instance ID (required unless set). Default: env SPANNER_INSTANCE_ID. |
-d, --database |
Database ID (required unless set). Default: env SPANNER_DATABASE_ID. |
-v, --verbose |
Print the full query_stats block instead of the default summary subset. |
--dialect |
SQL dialect for client-side parsing: auto (default), google-standard-sql, or postgresql. |
--dsn-suffix |
Extra go-sql-spanner DSN parameters as ;-separated name=value pairs. See docs/go-sql-spanner-dsn.md and docs/go-sql-spanner-client-side-statements.md. |
--format |
Result output format: table (default), csv, or jsonl. |
End each statement with a semicolon (;) and press Enter to run it. Multiple lines are supported until the line ending is ;.
Leave the shell: type exit or quit (case-insensitive, optional trailing ;) and press Enter—they submit even without a semicolon. Ctrl-D on an empty first line sends EOF and exits cleanly (as in go-multiline-ny examples). Ctrl-J inserts a newline without submitting (same idea as Enter when the line does not end with ;).
Press Ctrl+C to cancel an in-flight query or interrupt the session (see context cancellation below).
Behavior notes
- After connect, the shell warms the session by reading the driver’s
database_dialectproperty (client-sideSHOW, no PROFILEExecOptions), so the first interactive query is less likely to pay full cold-start cost. With--dialect auto, that read runs once as part of dialect detection; with an explicit--dialect, it runs in the background while the REPL starts. Errors are printed to stderr and the REPL still runs. - Statements are executed through
database/sqlQueryContext. The driver routes SELECT, DML, and DDL appropriately (including long-running DDL on the admin API). Multiple statements in one input (semicolon-separated, as supported by go-sql-spanner from v1.22+) are printed one result block per statement—same table /csv/jsonlshape and execution summary as a single query—with a blank line between blocks. - Normal queries use
ExecuteSqlRequest_PROFILEwithReturnResultSetStatsenabled. After the result table (orcsv/jsonloutput), the shell prints an execution summary from Spanner’sResultSetStats.query_statswhen it is present. By default, the detailed block is limited tocpu_time,rows_scanned,deleted_rows_scanned, andoptimizer_version; use-v/--verboseto print the fullquery_statsblock. The pinnedgo-sql-spannerrelease includes googleapis/go-sql-spanner#778 soQueryStatsis forwarded intoResultSetStats(see also googleapis/go-sql-spanner#779). The query plan tree is not printed unless you useEXPLAIN/EXPLAIN ANALYZE. EXPLAIN/EXPLAIN ANALYZEare not sent to Spanner as SQL keywords. The shell strips them client-side and runs the inner statement withQueryMode = PLANorPROFILE.EXPLAIN: plan tree only (via spannerplan) when plan nodes exist—noN row(s) in setline and no stats block, since PLAN typically does not populateQueryStats.EXPLAIN ANALYZE: plan tree first, thenN row(s) in set(andelapsed_timein parentheses whenquery_statsincludes it), then aquery_statskey/value block only whenquery_statsis returned—the shell does not reconstruct stats from the plan tree. Plan output ignores--format. For multi-statement input, the shell batches consecutive statements that share the sameQueryModeinto oneQueryContext(so normalSELECTandEXPLAIN ANALYZE, both PROFILE, can run together);EXPLAIN(PLAN) starts a separate batch from PROFILE. Statements are split with go-sql-spanner’sparser.Split; per-statement display (plan vs table) still follows each statement’s role.
Examples
Timing-related values such as elapsed time and CPU time vary by environment and run.
Default result output and EXPLAIN ANALYZE
spanner> SELECT *
-> FROM Singers
-> JOIN Albums USING (SingerId);
+----------+-----------+----------+------------+------------+---------+-------------------------+-----------------+
| SingerId | FirstName | LastName | SingerInfo | BirthDate | AlbumId | AlbumTitle | MarketingBudget |
| INT64 | STRING | STRING | BYTES | DATE | INT64 | STRING | INT64 |
+----------+-----------+----------+------------+------------+---------+-------------------------+-----------------+
| 1 | Marc | Richards | NULL | 1970-09-03 | 1 | Total Junk | NULL |
| 1 | Marc | Richards | NULL | 1970-09-03 | 2 | Go, Go, Go | NULL |
| 2 | Catalina | Smith | NULL | 1990-08-17 | 1 | Green | NULL |
| 2 | Catalina | Smith | NULL | 1990-08-17 | 2 | Forever Hold Your Peace | NULL |
| 2 | Catalina | Smith | NULL | 1990-08-17 | 3 | Terrified | NULL |
| 3 | Alice | Trentor | NULL | 1991-10-02 | 1 | Nothing To Do With Me | NULL |
| 4 | Lea | Martin | NULL | 1991-11-09 | 1 | Play | NULL |
+----------+-----------+----------+------------+------------+---------+-------------------------+-----------------+
7 rows in set (13.56 msecs)
cpu_time : 11.36 msecs
deleted_rows_scanned: 0
optimizer_version : 7
rows_scanned : 12
spanner> EXPLAIN ANALYZE
-> SELECT *
-> FROM Singers
-> JOIN Albums USING (SingerId);
+-----+-------------------------------------------------------------------------------------+------+-------+---------------+
| ID | Operator | Rows | Exec. | Total Latency |
+-----+-------------------------------------------------------------------------------------+------+-------+---------------+
| 0 | Distributed Union on Singers <Row> (split_ranges_aligned) | 7 | 1 | 10.42 msecs |
| 1 | +- Local Distributed Union <Row> | 7 | 3 | 3.6 msecs |
| 2 | +- Serialize Result <Row> | 7 | 4 | 1.05 msecs |
| 3 | +- Cross Apply <Row> | 7 | 4 | 1.04 msecs |
| 4 | +- [Input] Table Scan on Singers <Row> (Full scan, scan_method: Automatic) | 5 | 4 | 0.95 msecs |
| 10 | +- [Map] Local Distributed Union <Row> | 7 | 5 | 0.08 msecs |
| 11 | +- Filter Scan <Row> (seekable_key_size: 0) | | | |
| *12 | +- Table Scan on Albums <Row> (scan_method: Row) | 7 | 5 | 0.07 msecs |
+-----+-------------------------------------------------------------------------------------+------+-------+---------------+
Predicates(identified by ID):
12: Seek Condition: ($SingerId_1 = $SingerId)
7 rows in set (10.85 msecs)
cpu_time : 8.43 msecs
deleted_rows_scanned: 0
optimizer_version : 7
rows_scanned : 12
--verbose query stats
$ go run ./ -v -p ${SPANNER_PROJECT_ID} -i ${SPANNER_INSTANCE_ID} -d ${SPANNER_DATABASE_ID}
spanner> SELECT * FROM Singers;
+----------+-----------+----------+------------+------------+
| SingerId | FirstName | LastName | SingerInfo | BirthDate |
| INT64 | STRING | STRING | BYTES | DATE |
+----------+-----------+----------+------------+------------+
| 1 | Marc | Richards | NULL | 1970-09-03 |
| 2 | Catalina | Smith | NULL | 1990-08-17 |
| 3 | Alice | Trentor | NULL | 1991-10-02 |
| 4 | Lea | Martin | NULL | 1991-11-09 |
| 5 | David | Lomond | NULL | 1977-01-29 |
+----------+-----------+----------+------------+------------+
5 rows in set (14.47 msecs)
bytes_returned : 116
cpu_time : 18.1 msecs
data_bytes_read : 0
deleted_rows_scanned : 0
elapsed_time : 14.47 msecs
filesystem_delay_seconds : 0.46 msecs
is_graph_query : false
locking_delay : 0 msecs
memory_peak_usage_bytes : 112
memory_usage_percentage : 0.000
optimizer_statistics_package: auto_20260429_12_58_32UTC
optimizer_version : 7
query_plan_creation_time : 1.73 msecs
remote_server_calls : 3/3
rows_returned : 5
rows_scanned : 5
runtime_creation_time : 0.58 msecs
server_queue_delay : 0.05 msecs
statistics_load_time : 0
time_to_first_row : 13.26 msecs
total_memory_peak_usage_byte: 112
More documentation
| Document | Contents |
|---|---|
| docs/go-sql-spanner-dsn.md | Driver properties, DSN shape, and shared SHOW / SET names |
| docs/go-sql-spanner-client-side-statements.md | Client-side statements (SHOW / SET, etc.) |
| docs/generated/connection-properties.generated.md | Generated connection-property table from upstream go-sql-spanner |
Development
Processing is delegated as much as possible to go-sql-spanner and database/sql; there is no layered architecture beyond that. Work specific to this repo is the REPL, client-side EXPLAIN / EXPLAIN ANALYZE rewriting, and result / plan rendering. See docs/design-philosophy.md for intent and boundaries.
Run the default tests with:
go test ./...
Integration tests require Docker and start the Spanner emulator via spanemuboost:
go test -tags=integration ./...
Generated documentation is refreshed from the module root with:
go generate ./...
go generate runs ptyhelp patch through mise using the project-level mise.toml. The generator runs go run . --help in a PTY, writes docs/generated/spannersh-help.txt, and replaces the <!-- spannersh-help begin/end --> block in this file. It also regenerates docs/generated/connection-properties.generated.md after go-sql-spanner updates.
CI runs formatting, vet, build, version, unit, race, vulnerability, and integration checks on pushes and pull requests. Releases are created by pushing a v* tag; the GoReleaser workflow builds archives and checksums for GitHub Releases.
License
spannersh is released under the MIT License.
The go-sql-spanner driver is Apache License 2.0; see the upstream LICENSE.
Documentation
¶
Overview ¶
Spannersh is a small interactive client for Cloud Spanner built on database/sql with github.com/googleapis/go-sql-spanner.
Layout (single package main, one file per concern):
main.go — signal-aware context, exit status on interrupt run.go — CLI flags, DSN open, background warmup, REPL loop dsn.go — DSN composition warmup.go — non-blocking driver database_dialect warm-up after connect (skipped when --dialect auto) explain.go — EXPLAIN / EXPLAIN ANALYZE stripping → QueryMode + display kind query.go — ExecOptions, query execution, driving render/summary render.go — table / CSV / JSONL, plan tree, row helpers summary.go — execution summary from ResultSetStats.query_stats when present format.go — --format parsing