← All decision records

EDR 0039 Status: accepted Implementation: none

The checkable grammar is parsed by PostgreSQL's own parser

Every component that parses a statement uses libpg_query — PostgreSQL's real grammar — not a re-implementation. cgo is accepted in every binary, including the CLI, rather than maintain a second grammar that can disagree with the server.

What exists: The consequences are staged for it — the Makefile exports CGO_ENABLED=1 for every target, CI builds on four platforms, and the conformance corpus at testdata/conformance/ has its format, its validator and its subset_version field, though it holds no vectors yet. Nothing that decides anything exists: libpg_query is not linked in, there is no internal/grammar, and nothing reads a statement.

Authors
Theo Zourzouvillys
Date
2026-08-16
Tags
architecture security

TL;DR

The checkable statement grammar of EDR-0007 is parsed by libpg_query — PostgreSQL's own parser, extracted from the server sources — through its Go binding, in every component that parses a statement: Harbourmaster, Pilot, CLI, and the loopback proxy. There is no second parser.

This makes CGO_ENABLED=1 a property of every Marque binary, including the one that ships to laptops. That cost is accepted, and the release process changes to suit it: builds run on a runner per operating system and architecture rather than cross-compiling from one.

The parse tree is not the decision. A single package walks that tree and returns one of three verdicts — in subset with the extracted scope, out of grammar, or unsupported — and the subset is defined by an allowlist over node types, never by pattern-matching text.

Context

Seven records lean on "the checkable grammar" without saying what implements it. It is the input to the static containment proof, the source of the predicate the fence is built from, the gate on standing-order parameters, and the thing the proxy consults before brokering a statement. It is the most load-bearing unnamed component in the design, so it needs deciding before Phase 1 sequences anything around it.

A re-implemented SQL grammar is a soundness risk in a way most re-implementations are not. PostgreSQL's grammar is large and full of cases that a derived or hand-written parser gets subtly different: operator precedence in the presence of user-defined operators, AT TIME ZONE, the several forms of UPDATE … FROM, dollar-quoting, escape-string behaviour that depends on standard_conforming_strings, and a grammar that changes between major versions. If the parser believes a statement is one shape and the server executes another, the containment proof is proving something about a statement that does not exist.

But it is worth being precise about where that risk actually lives, because overstating it would justify the wrong thing. The CLI and the proxy run on the operator's machine and are not trusted: everything they decide is re-checked by the Harbourmaster at submission and by the Pilot's fence at execution (EDR-0034), and EDR-0036 binds the statement digest so the server always re-parses the same bytes. A client-side parser that wrongly says "in scope" produces a refusal, not a breach. Soundness genuinely requires the real grammar only in the Harbourmaster and the Pilot.

So the honest argument for using it on the client too is not soundness. It is that a client which says in scope and a server which says out of scope is a confusing product and an expensive bug to chase, that the subset allowlist would otherwise have to be written and widened twice in step, and that one person maintaining two SQL grammars as a side activity is not a realistic plan.

ZFN-31 is the standing position, and it points the other way here on purpose: owning a component is justified when you understand the domain better than the alternative. Nobody understands PostgreSQL's grammar better than PostgreSQL, and the alternative is free.

The rejected option was libpg_query server-side with a pure-Go advisory parse in the CLI, which is sound and buys a trivial cross-compile. It was rejected on the maintenance argument above, not on a security one; if the cgo cost proves worse in practice than it looks here, this is the record to supersede, and the reasoning to revisit is that paragraph rather than this one.

Decision

One parser, one package. internal/grammar wraps pg_query_go and is the only place a statement is parsed. Its interface is engine-shaped, taking and returning types that name no PostgreSQL concept, so the second engine of EDR-0026 is an implementation of it rather than a fork of everything above it.

Three verdicts, allowlisted. The subset walk enumerates the node types it accepts and rejects everything else by default. The distinction EDR-0022 draws is preserved in the return type:

Verdict Meaning What happens
in_subset Provable shape; scope and predicate extracted May match a delegation or standing order
out_of_grammar Valid SQL the subset cannot prove anything about Escalates to a human
unsupported Marque will not broker it at all (DDL, implicit commit, multi-statement) Refused with the reason

Never a boolean, and never a fourth verdict meaning "probably fine".

The subset is versioned, and the version is recorded on every delegation and marque, which EDR-0007 already requires so that widening the grammar cannot retroactively change what an old delegation permits. This record names the mechanism that makes it enforceable: see the obligation on parser upgrades below.

A conformance corpus is the specification. A language-neutral file of (statement, verdict, extracted scope) cases is executed by the test suite. Widening the subset is a diff to that file, reviewed as such. A statement that changes verdict without a corresponding corpus change fails the build.

The supported PostgreSQL major range is declared, and checked at connection. The parser is pinned to a major version; a target running a newer major may parse a statement the pinned grammar does not have. The Pilot reads server_version_num when it connects and, outside the declared range, marks the target as unable to support a proved scope — so statements against it take the human path rather than a fast path. This is the same principle EDR-0026 applies to engines, applied to a version rather than an engine; it introduces no new verification case, only a narrower one.

cgo everywhere. Every binary sets CGO_ENABLED=1. Release builds run on a native runner per operating system and architecture. go install continues to work and requires a C toolchain, which is stated in the installation instructions rather than discovered.

Consequences

Easier.

  • There is exactly one answer to "what does this statement do", and it is the server's answer. Dollar quoting, escape-string behaviour, precedence and every other grammar subtlety are correct without anyone reasoning about them.
  • Widening the checkable subset is one allowlist and one corpus, not two kept in step.
  • A statement rejected by the CLI is rejected by the Harbourmaster for the identical reason, so the error the operator reads is the real one.

Harder.

  • Cross-compilation is gone. The release matrix needs a native runner per platform, or a cross-toolchain shim; either is more moving parts than GOOS=… go build, and this is the single largest cost of the decision.
  • Contributing needs a C toolchain, so the barrier to a first patch is higher than a pure-Go project's, and CI is slower.
  • Anyone embedding Marque's grammar package inherits cgo, whether or not they wanted it.
  • The console cannot parse anything. No cgo in a browser, and WebAssembly is not on the table for a CSP-strict static app. The console's statement view is display-only — which EDR-0024 independently requires, so nothing is lost, but it is now also structural.
  • Container images need the parser's static library present, so "scratch plus a static binary" needs care rather than being the default.

New obligations.

  • A libpg_query upgrade is a reviewed change, on the order of a schema migration. A new major can parse a statement the previous one could not, which silently widens the checkable subset and therefore widens what an existing delegation permits — precisely what EDR-0007 forbids. An upgrade re-runs the conformance corpus, and any verdict that changes requires the subset version to be bumped, so old delegations stay pinned to the subset they were signed against.
  • The supported PostgreSQL major range is declared publicly, and each major in it is covered by the integration suite.
  • The installation instructions state the toolchain requirement.

References

  • ZFN-31 — own a component only where you understand the domain; the grammar is the case where that argues for not owning it.
  • EDR-0007 — the containment proof and the versioned subset this record implements.
  • EDR-0022 — the out-of-grammar versus unsupported distinction the verdict type carries.
  • EDR-0026 — engine capability, extended here to a server version outside the parser's range.
  • EDR-0036 — the digest binding that makes a client-side parse advisory rather than load-bearing.
  • libpg_query — PostgreSQL's parser as a library; pg_query_go is the binding.

Changelog

  • 2026-08-16: Accepted.
  • 2026-08-17: The conformance corpus this record relies on now exists as a format and a validator at testdata/conformance/, loaded by internal/conformance, with the vectors and the grammar that runs them still to come in M2. implementation stays none — nothing here parses a statement or decides anything — and the note is corrected, since it said no corpus existed. The decision is unchanged.