Documentation
¶
Overview ¶
Package output defines the shared response shape every crdb-sql subcommand emits when --output=json, plus the renderer that switches between text and JSON.
The Envelope describes the analysis context — which tier produced the result, which parser version was used, whether a cluster was reached — and carries a uniform Errors list for agent consumption. Subcommand- specific payload lives in Data so the envelope itself never needs to learn about new commands. The schema is derived from the JSON error-output example in docs/design_doc.md (search "Error output"); fields like statement_type that are subcommand-specific live in Data rather than in the envelope itself.
Index ¶
Constants ¶
const ( // CodeTargetVersionMismatch is emitted by // output.VersionMismatchWarning when the user-declared target // version differs from the bundled parser version at the // MAJOR.MINOR level. CodeTargetVersionMismatch = "target_version_mismatch" // CodeFeatureNotYetIntroduced is emitted when the AST inspector // recognizes a CockroachDB SQL feature whose Introduced version // (per the version registry) is newer than the user's declared // target version. The accompanying Error.Context carries the // feature_tag, feature_name, introduced, and target keys so // agents can branch programmatically without parsing the // human-readable Message. CodeFeatureNotYetIntroduced = "feature_not_yet_introduced" // CodeSafetyViolation is emitted by the Tier 3 safety allowlist // (internal/safety) when a statement is rejected before any // cluster round-trip. The accompanying Error.Context carries the // tag (e.g. "DROP TABLE"), mode (e.g. "read_only"), operation // (e.g. "explain"), and reason keys so agents can branch // programmatically — for example, prompt the user to escalate the // safety mode — without parsing the human-readable Message. CodeSafetyViolation = "safety_violation" // CodeInputPreprocessed is emitted (severity WARNING) by the MCP // and CLI handlers whenever sqlformat.StripShellPromptsWithMap // removed cockroach sql REPL prompt bytes from the caller's input // before parsing. The accompanying Error.Context carries the // bytes_removed integer so agents can confirm the modification // programmatically. CodeInputPreprocessed = "input_preprocessed" )
Stable Error.Code values. Agents key off these strings, so renames are breaking changes; new codes are added by appending here. Codes are namespaced by the producing concept (e.g. "target_version_*" for envelope-level version-handling diagnostics).
const ( // ContextPositionOmittedReason is the Error.Context key set by the // execute handlers when they intentionally drop a cluster error's // Position because no honest translation is possible. The value is // one of the Reason* constants below; agents should branch on the // value, not parse the human-readable Message. ContextPositionOmittedReason = "position_omitted_reason" // ReasonLimitInjectionRewroteSQL is set as the value of // ContextPositionOmittedReason when safety.MaybeInjectLimitParsed // re-serialized the AST through tree.AsStringWithFlags. The // resulting `rewritten` SQL is canonicalized — comments dropped, // whitespace normalized — so pgwire positions index into a buffer // the strip map cannot translate. Position is dropped rather than // reported in a frame the caller cannot reconstruct. ReasonLimitInjectionRewroteSQL = "limit_injection_rewrote_sql" )
Wire-contract Context keys and values. Promoted here so the MCP and CLI sides cannot drift via a typo in one place.
Variables ¶
var ErrRendered = errors.New("output: error already rendered as envelope")
ErrRendered is returned by Renderer.RenderError after it has emitted an envelope describing the failure. cmd/crdb-sql/main.go uses errors.Is to recognize this sentinel and suppress its default "Error: ..." stderr print, while still exiting with a non-zero status. This preserves the JSON-mode contract that stdout is the single source of truth and keeps agents from having to parse stderr.
Functions ¶
func ValidateTargetVersion ¶
ValidateTargetVersion parses a user-supplied CockroachDB target version string and returns its canonical form. The accepted shapes are MAJOR.MINOR and MAJOR.MINOR.PATCH, optionally prefixed with a single "v" (so "v25.4.0" and "25.4.0" are both accepted). The canonical form differs from the input in exactly one way: a leading "v" is stripped. Patch presence/absence and exact digits are preserved verbatim, so "25.4" and "25.4.0" remain distinct strings even though they represent the same minor release.
Surrounding whitespace is trimmed before validation so the CLI and MCP entry points behave the same when callers paste a value.
An empty input (after trimming) returns an error; callers that treat empty as "no target version supplied" should check for the empty string before invoking this helper.
Types ¶
type ConnectionStatus ¶
type ConnectionStatus string
ConnectionStatus reports whether the command reached a live cluster. Commands that never connect (version, parse, format) report ConnectionDisconnected.
const ( ConnectionDisconnected ConnectionStatus = "disconnected" ConnectionConnected ConnectionStatus = "connected" )
ConnectionStatus values.
type Envelope ¶
type Envelope struct {
Tier Tier `json:"tier,omitempty"`
ParserVersion string `json:"parser_version"`
TargetVersion string `json:"target_version,omitempty"`
ConnectionStatus ConnectionStatus `json:"connection_status"`
Errors []Error `json:"errors,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
Envelope is the response contract every crdb-sql subcommand returns under --output=json.
Lifecycle: built fresh by the subcommand's RunE, marshalled once by Renderer.Render, then discarded. No long-lived state.
Field discipline:
- Tier uses omitempty so commands without tier semantics (e.g. version) omit the field rather than emitting a misleading empty string. This relies on TierUnset being the empty-string zero value.
- ParserVersion and ConnectionStatus are always emitted so agents can rely on their presence.
- TargetVersion uses omitempty so the field is absent when the user did not pass --target-version (or the equivalent MCP parameter). This preserves byte-identical output for the unflagged path.
- Errors uses omitempty so a clean run produces no errors key rather than a noisy "errors": null.
- Data is json.RawMessage so each subcommand controls its own payload marshalling and this package never depends on subcommand types.
type Error ¶
type Error struct {
Code string `json:"code"`
Severity Severity `json:"severity"`
Message string `json:"message"`
Position *Position `json:"position,omitempty"`
Category string `json:"category,omitempty"`
Context map[string]any `json:"context,omitempty"`
Suggestions []Suggestion `json:"suggestions,omitempty"`
}
Error is the per-error schema agents consume; the fields are derived from the JSON example in docs/design_doc.md (search "Error output"). Code, Severity, and Message are required (every error has them); Position, Category, Context, and Suggestions are optional so early subcommands can populate only what they have. Richer enrichment lands with later issues.
func VersionMismatchWarning ¶
VersionMismatchWarning compares the parser version compiled into the binary against the user-supplied target version and, when the major or minor components differ, returns a warning Error suitable for appending to Envelope.Errors. The bool return indicates whether a warning was produced; callers should append only when it is true.
targetVersion is expected to already be in canonical form (post-ValidateTargetVersion). The silent-skip path is meant to tolerate an unresolvable parserVersion; a parse failure on targetVersion would indicate a caller contract violation but is also tolerated rather than panicked on, since it never makes sense to fail a SQL operation just because the version-mismatch check could not run.
Comparison is on MAJOR.MINOR only — patch-level differences are noise for users who care about feature compatibility, not bug-fix releases. If either input cannot be parsed (e.g. parserVersion resolved to "unknown" in a development build), no warning is produced; callers should not punish users for an unresolvable bundled version.
Known limitation (tracked separately): parserVersion today is the cockroachdb-parser Go-module version (e.g. "v0.26.2"), not the CockroachDB release version it implements. Until the parser ships a CRDB-version mapping, a user supplying --target-version 25.4.0 will see "parser is v0.26 but target is v25.4" — technically correct under the implemented contract, but semantically a false positive. Stage 2/3 (#83/#85) will revisit how versions compare.
type Format ¶
type Format string
Format selects how a subcommand serializes its result.
text — human-readable, subcommand-defined layout (default). json — Envelope marshalled as indented JSON.
func ParseFormat ¶
ParseFormat validates a user-supplied --output value. Only "text" and "json" are accepted; any other value (including empty) produces an error that names the valid choices, so cobra's surfaced message is actionable.
type Position ¶
type Position struct {
Line int `json:"line"`
Column int `json:"column"`
ByteOffset int `json:"byte_offset"`
}
Position is a 1-based line/column with a 0-based byte offset, matching the convention used by the CockroachDB parser.
type Renderer ¶
Renderer writes a subcommand's result in the selected format.
For FormatText, textFn owns the output: it receives the configured writer and produces whatever lines the subcommand wants (the envelope is ignored). For FormatJSON, the envelope is marshalled as indented JSON so piped output stays human-readable; agents that want compact form can pipe through `jq -c`. A trailing newline is appended so the output is a well-formed line on terminals.
EPIPE handling: write failures with errno EPIPE are suppressed (return nil) because they are the normal outcome when a downstream consumer closes stdout early (e.g. piping into `head -n1`). Surfacing them as a non-zero exit with a "broken pipe" message is hostile for a tool meant to be piped, and partial output is acceptable. Marshal errors and the unsupported-format error are NOT subject to EPIPE filtering — they are propagated unchanged. On Windows the native broken-pipe error has a different value (ERROR_BROKEN_PIPE), so this check would not fire there; Windows is not a supported target, so we don't add a separate branch.
func (Renderer) Render ¶
Render dispatches on r.Format. textFn must be non-nil for FormatText; it is ignored for FormatJSON.
textFn contract (FormatText only): Render passes textFn's returned error through suppressEPIPE, so any error that wraps syscall.EPIPE will be silently dropped — even if it did not originate from a write to r.Out. To avoid accidental suppression, textFn should perform only writes (fmt.Fprintf, w.Write, etc.) and return their errors directly. Pre-write work (formatting, lookups) should happen before calling Render so that textFn's error space is limited to write failures.
func (Renderer) RenderError ¶
RenderError is the JSON-mode error path. It appends a single Error describing failure to env, renders the envelope, and returns ErrRendered so the caller (and cmd/crdb-sql/main.go) know the failure has already been surfaced as structured output and should not be reprinted to stderr. In text mode it returns failure unchanged so cobra's existing "Error: ..." path runs.
The envelope's Data field is cleared because the partial result that motivated env (parser version, connection status) is still meaningful context, but any subcommand payload would be misleading on the error path. ParserVersion and ConnectionStatus may be empty if the failure happened before they were resolved; that is acceptable, since the errors entry is the load-bearing field for agents in this case.
If marshalling the error envelope itself fails, both the original failure and the marshal/write error are joined (via errors.Join) so cmd/crdb-sql/main.go's stderr fallback shows both causes. The joined error is NOT wrapped in ErrRendered so cmd/crdb-sql/main.go prints it.
EPIPE edge case: if the downstream consumer has already closed the pipe, Render succeeds (EPIPE is suppressed) and RenderError returns ErrRendered even though nothing reached stdout. This is intentional: the consumer abandoned the pipe, so no output channel remains and the best we can do is exit non-zero.
func (Renderer) RenderErrorEntry ¶
RenderErrorEntry is the structured-error variant of RenderError for callers that have already constructed an Error themselves. The supplied entry is appended verbatim to env.Errors instead of the generic internal_error shape RenderError synthesizes; this lets callers preserve enriched fields (e.g. SQLSTATE Code, Category, Position) that the generic synthesis would not populate.
failure is still required so the text-mode return path stays consistent: text mode bypasses the envelope and returns failure unchanged. entry.Message and failure.Error() are not required to match — JSON consumers see entry, text consumers see failure. All other contract points (ErrRendered sentinel, Data clearing, append semantics on pre-existing errors, errors.Join on render failure) match RenderError.
type Severity ¶
type Severity string
Severity is the severity level of a structured Error. Values use the PostgreSQL frontend/backend protocol severity strings (uppercase) so wire output matches what agents see from a live cluster. Defining the set as named constants prevents subcommands from drifting between "ERROR"/"error"/"Error" as more error producers come online.
type Suggestion ¶
type Suggestion struct {
Replacement string `json:"replacement"`
Range Range `json:"range"`
Confidence float64 `json:"confidence"`
Reason string `json:"reason"`
}
Suggestion is a structured fix proposal: replace bytes [Range.Start, Range.End) with Replacement. Confidence is in [0, 1]; Reason is a machine-readable label (e.g. "levenshtein_distance_1").
type Tier ¶
type Tier string
Tier identifies which analysis tier produced a response. The names and order match the "Three analysis tiers" section in docs/design_doc.md:
zero_config — Tier 1, expression type checking (zero-config). schema_file — Tier 2, name resolution (requires schema files). connected — Tier 3, connected validation.
TierUnset is the zero value and indicates the command has no tier semantics (e.g. version).