Skip to content

Split query plan and row output into separate sinks - #95

Merged
apstndb merged 2 commits into
mainfrom
feat/split-plan-output
Sep 8, 2026
Merged

Split query plan and row output into separate sinks#95
apstndb merged 2 commits into
mainfrom
feat/split-plan-output

Conversation

@apstndb

@apstndb apstndb commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add --output/-o, --plan-output, --plan-format, and --discard-results so one query can emit rows and a plan artifact to different destinations.
  • Setting --plan-output enables split mode: the primary document is the current ResultSet with stats.queryPlan removed (CSV stays rows-only); the plan artifact is a rows-less ResultSet envelope (metadata plus full stats).
  • Regular-file destinations write through a sibling temp file (mode 0600) and rename into place after the query/transaction succeeds. A failed second publish reports which outputs completed and never replays SQL.

CLI contract

Flag Default Meaning
--output PATH (-o) - Primary document destination.
--plan-output PATH unset Enables split mode and writes the plan artifact.
--plan-format json|yaml follows --format when that is json/yaml, else json Plan artifact encoding.
--discard-results off Write only the plan artifact. Requires --plan-output.

Destination spellings: - is stdout; /dev/stdout and /dev/stderr are mapped in-process (Windows-safe, included in collision checks); anything else is a regular file.

Validation before a client is created:

  • --plan-output requires --query-mode=PLAN, PROFILE, or WITH_PLAN_AND_STATS.
  • --plan-format and --discard-results require --plan-output.
  • --plan-output is incompatible with --try-partition-query and --enable-partitioned-dml.
  • Both sinks on stdout (any spelling), the same file (cleaned abs path and os.SameFile when the target exists), or an output that aliases --sql-file/--param-file/--filter-file is rejected.

A compatible mode that still returns nil/empty planNodes is an artifact-generation error after execution; the statement is not retried. Split mode disables jq early stop and drains remaining rows without retaining them. --redact-rows is unchanged and independent of --discard-results. After committed DML, an output/render failure is reported as not a rollback and SQL is not replayed.

What stays unchanged

  • Default run (none of the new flags): combined document on stdout, existing golden fixtures untouched.
  • jq flags still apply to the primary document only.
  • Tracing (--experimental-trace-stdout) still writes to stderr.

Tests run

Command Exit
go build ./... 0
go test ./params/... ./jqresult/... ./resultset/... 0
golangci-lint run 2 (host go1.27 vs linter go1.26)
GOTOOLCHAIN=go1.25.13 golangci-lint run 0 (0 issues)
go run . --help 0 (new flags render)
DOCKER_HOST=unix://$HOME/.colima/default/docker.sock TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock go test ./... 0

Known limitations

  • Renderer plan formats (text, dot, svg, ...) are out of scope; --plan-format is json/yaml only.
  • Cloud Spanner emulator 1.5.55 (spanemuboost default) returns queryStats for PROFILE but no QueryPlan. Emulator split runs therefore hit the documented missing-plan error after publishing CSV. Happy-path plan envelopes (non-empty planNodes, zero-row/PLAN metadata, discard-results, lazy drain) are covered by the existing fake ExecuteStreamingSql server.
  • Overwrite of existing file targets is allowed by default (no --overwrite/--no-clobber).

--plan-output writes a rows-less ResultSet envelope and strips
stats.queryPlan from the primary document. Default stdout output is
unchanged. Files publish via temp+rename; a missing plan or failed
sink does not replay SQL.
@apstndb apstndb added the enhancement New feature or request label Sep 8, 2026
Two review findings on the split-output head 9c7ebdd:

- sinks.Finish errors (temp-file rename of the primary or plan artifact)
  were returned unwrapped from runCLI. After a read-write transaction
  committed or a partitioned DML finished, a bare "failed to publish plan
  output" invites replaying the statement. runCLI now wraps a Finish error
  with the committed-DML wording whenever the statement itself succeeded in
  a committing mode.
- The eager JSON/YAML path with --discard-results materialized every row
  before skipping jq. Materialize now drops row values whenever the run
  will not emit rows (--redact-rows or --discard-results), matching the CSV
  and lazy paths which already drain.

Adds unit tests for the mode classification, the materialize decision, and
the wrapped publish failure.
@apstndb
apstndb merged commit 82616bc into main Sep 8, 2026
2 checks passed
apstndb added a commit that referenced this pull request Sep 8, 2026
Add --reauth=off|auto (env EXECSPANSQL_REAUTH, default off) so Google Workspace
reauthentication failures on user Application Default Credentials are
classified before any Spanner RPC and, with explicit consent, repaired by one
interactive `gcloud auth application-default login`.

Classification is structural: the typed token error (auth.Error body or
oauth2.RetrieveError) must carry error=invalid_grant with error_subtype
invalid_rapt or rapt_required; message-only matches receive the google-auth
hint text but never trigger a login. With `off` client construction is
unchanged and only the hint is added. With `auto` a preflight token fetch runs
before client creation, gcloud is launched once (stdout to stderr, fixed
argv), the well-known ADC file is re-read and verified, a second token fetch
must succeed, and the client is created with option.WithAuthCredentials.
Applicability requires user ADC at the well-known path, no
GOOGLE_APPLICATION_CREDENTIALS or CLOUDSDK_CONFIG, no emulator, a terminal on
stdin and stderr, gcloud on PATH, and no injected client options. SQL and
parameters are frozen before the login; --timeout starts after it; the first
SIGINT cancels and the second terminates. DML and in-flight statements are
never replayed.

Verification: go build, vet, GOTOOLCHAIN=go1.25.13 golangci-lint (0 issues),
full go test ./... with the Colima emulator on a038e5d; classifier,
orchestration, applicability, fake-gcloud, and transport tests added.
Independent review: grok GO at r1 (570b6c4), r2 (c9932d8, params frozen before
preflight), and r3 (a038e5d, resolution against main after #95); reports
~/tmp/apstndb/execspansql/2026-09-08-feature-notes/merge-reviews/feature-merge-93-r{1,2,3}.md.
Known limits: Windows gcloud.cmd launch untested in CI; mid-run RAPT expiry
after the preflight is hint-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant