[INIT-6549] Add client-side synchronous snapshot queries to the Flink CLI - #3466
[INIT-6549] Add client-side synchronous snapshot queries to the Flink CLI#3466Yiyu Tian (yiyutian1) wants to merge 6 commits into
Conversation
|
🎉 All Contributor License Agreements have been signed. Ready to merge. |
There was a problem hiding this comment.
Pull request overview
Adds a new client-side “snapshot query” execution path for Flink SQL, including a top-level confluent query command that submits a statement, blocks until it leaves PENDING, drains all result pages, and prints results (table or serialized JSON/YAML), with best-effort stop behavior on interrupts/timeouts.
Changes:
- Introduces
pkg/flink/querywith an await+drain loop for synchronous result collection (including boundedness checks, truncation/incomplete signaling, and wrapped fetch errors). - Adds SQL-type-aware serialization via
StatementResultField.ToSerializedValue()and corresponding unit tests. - Mounts the new
confluent querycommand at the CLI root and tightens the Flink gateway mock to reject malformed stop/update requests consistent with the real gateway.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/test-server/flink_gateway_router.go | Makes the mock gateway stricter by rejecting statement updates that omit SQL text, matching real gateway behavior and catching broken stop paths. |
| pkg/flink/types/statement_traits.go | Adds helpers to read bounded/append-only traits with “known/unknown” signaling. |
| pkg/flink/types/result_fields.go | Extends StatementResultField with ToSerializedValue() for typed JSON/YAML output. |
| pkg/flink/types/result_fields_serialized.go | Implements typed serialization rules (e.g., NULL→null, small ints→numbers, BIGINT/DECIMAL→strings). |
| pkg/flink/types/result_fields_serialized_test.go | Unit tests for typed serialization behavior and JSON round-trip precision expectations. |
| pkg/flink/types/processed_statement.go | Adds STOPPED/DELETING phases for terminal-phase handling. |
| pkg/flink/query/README.md | Documents rationale, drain-loop semantics, and known limitations of the synchronous query path. |
| pkg/flink/query/query.go | Implements query.Run, await, drain, and terminal-phase logic for synchronous snapshot queries. |
| pkg/flink/query/query_test.go | Unit tests for paging, truncation, incomplete detection, boundedness rejection, and error wrapping. |
| internal/query/command.go | Adds the confluent query Cobra command, wiring flags, submit/run/stop flow, and output formatting. |
| internal/command.go | Registers the new root-level query command. |
Suppressed comments (1)
internal/query/command.go:539
- The JSON/YAML envelope is constructed without surfacing
result.Incomplete, so even if the drain loop flags the result as incomplete, serialized output won't include it.
Truncated: result.Truncated,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The stderr warning for a gateway that stopped returning page tokens mid-run was the only signal of a partial result set; a script reading -o json/-o yaml had no way to detect it. Per Copilot review on #3466.
…queries Adds a top-level `confluent query` command that submits a bounded Flink SQL statement, blocks until it finishes, and prints the complete result set, exiting non-zero on failure. Intended for scripts and one-shot queries; the interactive `flink shell` remains the tool for exploring a streaming result. Mounted at the top level rather than under `flink statement`: per discussion with Jim Hughes and Florian Eiden, the verb should not name Flink or the statement resource, since the same one-shot query ergonomics are expected to cover other backends (e.g. Lightning Tables) later without a rename. Output defaults to a self-describing envelope (column schema + rows); `--raw` opts into a bare row array. Values are type-aware — a number serializes as a number and a NULL as null, rather than everything round-tripping as a string. Folded in from post-review fixup commits: - error message now follows repo convention (lowercase, no trailing period, flags in backticks) - ListFlinkComputePools call updated for its current three-arg signature - the JSON/YAML envelope now surfaces Incomplete alongside Truncated - help goldens regenerated for the new command
ee1da79 to
9707001
Compare
|
An automated review pass flagged a few things worth a human glance. These scored moderately-to-highly in initial triage but weren't independently re-verified in a second pass, so please treat them as leads rather than confirmed issues. Suggestion — flag formatting in error/suggestion messages (
This repo's output-formatting convention documents flags as backtick-formatted — these three look like a consistent slip across the new command's error paths. Suggestion — test coverage (
Suggestion — root command registration (
Suggestion — stale doc line (
Suggestion — comment cites the wrong precedent (
Comment generated with the help of an AI agent |
9d2605d to
24468a1
Compare
|
Thanks Jim Hughes (@jnh5y) — addressed most of this in 24468a1:
Root registration in internal/command.go was intentional, same pattern as the other top-level commands. Still open: integration goldens for actual query execution (only unit tests + help golden so far) — holding off until the mount point/output shape are settled with PM, same as noted in the PR description. |
- Backtick-format flag names in error/suggestion strings for consistency with the rest of the repo's output conventions. - Fix the Hidden-gating comment's precedent: unified-stream-manager doesn't exist in this repo; point at the private link ingress endpoint command instead, which is the actual precedent for this pattern. - Correct the README's stale "no token refresh" limitation now that Options.RefreshToken exists, and note that the default 10-minute timeout is on the same order as the dataplane token's lifetime so it rarely matters in practice. - Add internal/query/command_test.go covering buildQueryProperties, printQueryResult, refreshGatewayToken, stopStatement and handleQueryError, raising this package's coverage from 0% to address the SonarQube new-code coverage gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
24468a1 to
bb7ccca
Compare
|
A few more things from a follow-up pass, including one I reproduced live against a staging compute pool. Recommendation — intermittent false "Incomplete" on a query that did finish ( Suggestion — Suggestion — serialized output silently truncates on a field/header-count mismatch ( Comment generated with the help of an AI agent |
|
Follow-up on the dropped-changelog-operation gap from earlier: I think Recommendation —
The command already detects this case at runtime: Put together: Given the command's own docs frame it as being for "scripting and one-shot queries," I'd treat this as something to close off now rather than after ships: either make Comment generated with the help of an AI agent |
b2ae176 to
c7d85dd
Compare
…ef, dead guard - pkg/flink/query/query.go: re-read the statement's phase once before conceding Incomplete on a page with rows and no next token. terminalBeforeFetch only reflects the phase read before GetStatementResults; if the statement finishes during that call, the read was actually complete. Reproduced live against a staging compute pool on a trivial single-row literal query. Added a regression test alongside the existing Incomplete test (which now also covers the still-running re-check). - internal/query/command.go: added AppendOnly *bool to the JSON/YAML envelope (nil when unknown, true/false when the gateway has reported it). Today's non-append-only warning only reaches stderr, so a script reading -o json had no signal that Rows is a changelog rather than a materialized table, and no way to tell an insert from a delete. - internal/query/command.go: stopStatement no longer dereferences a nil Spec if the gateway ever returns a statement without one; falls through to the existing "could not stop" warning path instead of panicking. - internal/query/command.go: removed the `if j < len(headers)` guard in printQueryResult's row-serialization loop. Run() already hard-errors on a row/schema mismatch before this code runs (see TestRunFailsOnRowSchemaMismatch), so the guard was unreachable and silently matched the exact failure class this package exists to avoid; replaced with a comment recording that guarantee. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c7d85dd to
a4889e4
Compare
c46cf1a to
44e4f00
Compare
- sql.snapshot.mode can no longer be silently overridden via --property; the append-only guarantee the append_only envelope field depends on only holds for a snapshot read, so an override now errors instead of quietly producing an ambiguous changelog result. - Add engine: "snapshot" to the JSON/YAML envelope, matching the PRD's illustrative shape and avoiding a breaking envelope change once M2 (Lightning routing) lands. - Add -f/--file to read the SQL statement from a file, the third PRD-specified source alongside --sql and the positional argument. - Add --catalog as an alias for --environment (shares the same flag Value, since --environment is read by ParseFlagsIntoContext before this command's own code runs). - Add --cluster as an alias for --database, kept as an independent flag rather than shared storage: --cluster is a name ParseFlagsIntoContext already treats specially everywhere in the CLI (persists it as the active Kafka cluster context), and --database has never had that side effect. Sharing storage would have leaked it onto every --database invocation; keeping them independent preserves --database's existing request-scoped behavior while --cluster behaves like it does everywhere else. - --database/--cluster now also fall back to the CLI's active Kafka cluster context (`confluent kafka cluster use`) when neither flag is given, matching the same "flag, then CLI context" chain environment and compute pool already follow. Previously neither flag had any context fallback at all, so the PRD's own "everyday call" example silently dropped sql.current-database whenever --database was omitted. --param (FR5, parameter binding) is not included: the gateway's SqlV1StatementSpec has no parameter/prepared-statement field, so it can only be sent as one opaque SQL string. Real client-side binding would need a placeholder syntax and escaping rules the PRD doesn't specify - that's a security design decision, not a quick addition. Also in this commit: - pkg/flink/query and internal/query no longer duplicate ccloudv2's page-token URL parsing; query.go now calls the newly-exported ccloudv2.ExtractPageToken, guarded the same way extractNextPageToken guards it, so behavior is unchanged. - Trimmed comments across this PR's Go files to their essential "why", cutting roughly 125 lines with no behavior change. - handleQueryError now splits the results-fetch 404/408 cases: a 404 means the statement is gone or mistyped, a 408 means the snapshot result window (confirmed as 1 hour from gateway source) has closed. Previously both were folded into one ambiguous 404 suggestion. - README updated with the gateway-source-confirmed explanation of why an empty next-page token is unambiguous at the protocol level, and where the JobManager/Status.Phase race the phase re-check guards against actually comes from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44e4f00 to
08c3d69
Compare
|
Thanks Jim Hughes (@jnh5y) — did both: sql.snapshot.mode now rejects a --property override outright (errors instead of silently producing an ambiguous changelog), and added append_only (nullable bool) to the JSON/YAML envelope so a script has the signal your warning-to-stderr couldn't give it. See 08c3d69 and a4889e4. |
The mock Flink gateway (test/test-server/flink_gateway_router.go) had no handler at all for GetStatementResults, so confluent query's actual row-fetching path — the reason this package exists — had never been exercised by an integration test. The PR description flagged this as deferred pending mount-point/flag stability. - Added the results endpoint route and handler, plus a fixture store keyed by statement name and scoped to names with the "cli-" prefix (only confluent query ever generates one), so it can never affect the fixed-name statements the rest of this file's tests already depend on. Scenarios are selected by matching the submitted SQL text, since confluent query never lets a caller choose the statement name. - test/query_test.go covers each distinct behavior path: table output, positional SQL, multi-page drain, -o json/-o yaml envelope, --raw, --max-rows truncation (and the stop it triggers), the non-append-only changelog warning, an unbounded statement being rejected, a FAILED statement, a DDL statement with no result schema, sql.snapshot.mode rejecting an override, -f/--file, and the --catalog/--cluster aliases. - Confirmed test/fixtures/output/flink/statement/*.golden and the rest of TestFlinkStatement/TestFlinkConnection are unaffected: verified against the pre-existing commit that TestFlinkShell/TestFlinkShellOnPrem fail identically with or without this change (a pre-existing PTY/ environment issue unrelated to the gateway mock). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|




Implements the client-side path for one-shot Flink SQL snapshot queries (INIT-6549 M1, PRD R3): submit → block → drain every page → print → exit non-zero on failure.
Diagram here: here
example use case:
Mounted at the top level (
confluent query), not underflink statement— per discussion with Jim Hughes and Florian Eiden, the same one-shot ergonomics should extend to other backends (e.g. Lightning Tables) later without a rename.-o json/-o yamldefault to a self-describing envelope (schema + rows);--rawgives a bare row array.Verified against real staging. Submit → multi-page drain → exit-code behavior confirmed on a real compute pool. That run found and fixed two defects:
spec.stopped; every stop (interrupt, timeout, unbounded rejection) left the statementRUNNING. Fixed by reading the statement back and flipping the flag on it, likestatement stopdoes. The mock server was more permissive than the real gateway and missed this — it's now strict enough to catch it.INTEGERcame back as"3065"). Now type-aware: numbers as numbers,NULLasnull.Why a separate drain loop instead of the shell's
Store/ResultFetcherpipeline: that pipeline is built for a scrolling viewer and degrades silently in ways that become real bugs for a script reading stdout (row-cap eviction, schema-mismatch rows dropped, "done" inferred from a missing page token without checking phase). This drain loop reports each of those conditions instead of hiding them.Not done: on-prem support, statement cleanup on success,
--unsafe-tracestill dumps row data, no token-refresh-aware retry beyond a best-effort refresh before each call.Open questions: final mount point/verb, output shape (R3 wants a bare array, R10 wants a typed schema — currently reconciled via envelope-by-default +
--raw), timeout behavior (currently stops the statement and exits non-zero; PRD wants graceful degradation).Testing: 14 unit tests over a mocked gateway (multi-page drain, empty-token-while-running,
--max-rowsboundary, unbounded rejection, schema mismatch, cancellation).make lint-cliclean. Integration goldens deferred — the mount point and flags are still in flux pending PM sign-off.🤖 Generated with Claude Code