← First Pair Library

17 LakeCat as the QueryGraph Catalog Boundary

LakeCat is where QueryGraph stops pretending that catalog state is background plumbing. A QueryGraph import is only useful if the catalog can prove what table, view, policy, lineage, and receipt state it accepted. LakeCat gives that proof while staying a thin Iceberg-compatible catalog: normal clients still see standard table access, while QueryGraph receives derived control-plane evidence.

The current QGLake handoff has four durable artifacts:

The important new rule is the view-chain rule. Active accepted views must carry an acceptedReceiptChainHash that appears in namespace receiptChains[].chainHashes. That prevents a handoff from pairing a valid view receipt with unrelated namespace chain evidence. Tombstoned accepted views are different: their accepted chain can be a prefix of the later tombstone chain, so the proof must instead include tombstone receipt evidence whose expectedViewVersion preserves the accepted view version.

Diagram 19

The local integration test is intentionally concrete:

cd ../lakecat
scripts/qglake-handoff-local.sh

That command starts LakeCat, creates the local QGLake fixture, drains lineage, asks QueryGraph to verify and import the saved bundle, writes querygraph-import-plan.json, and verifies handoff-summary.json. The current passing handoff proves one table, one view, 26 outbox/lineage events, and 53 catalog graph events. QueryGraph rejects the handoff if the saved bundle, saved drain, captured output, compact summary, graph envelope, view receipt evidence, or QueryGraph import plan drift from one another.

The same boundary shows up in code:

cargo run -- lakecat-verify \
  --bundle ../lakecat/target/qglake-handoff/lakecat-bootstrap.json

cargo run -- lakecat-import \
  --bundle ../lakecat/target/qglake-handoff/lakecat-bootstrap.json \
  --output ../lakecat/target/qglake-handoff/querygraph-import-plan.json

lakecat-verify recomputes the LakeCat manifest hashes for tables, views, OpenLineage, graph, and the outer bundle. It also validates the QueryGraph import compatibility contract, including receipt-chain-hash in view receipt evidence. lakecat-import then creates the import plan only after that proof has survived round-trip JSON parsing. QueryGraph does not invent catalog truth; it accepts LakeCat proof, validates the Grust graph shape, and builds the next agent context from the smallest verified scope.

QueryGraph tracks LakeCat 0.3.0, the “Ocelot” release. Lynx put the catalog spine on Turso MVCC, so commits to different tables run truly concurrently and a same-table race converges to exactly one winner through a pointer compare-and- swap — no global write lock. That matters to the handoff: the audit event, the lineage outbox row, and the idempotency record that this chapter relies on are written in the same transaction as the table change, which is what lets a QueryGraph import accept catalog state as proof rather than as a best-effort side effect.

Lynx also tightened this boundary in a way the importer feels directly. LakeCat extracted the bootstrap-bundle wire format and its verification into a small shared qglake-bundle crate, so QueryGraph no longer keeps a hand-written copy of those types: it deserializes the canonical QueryGraphBootstrap and runs LakeCat’s own verify_manifest, then layers its Cypher import plan on top. The producer and the consumer now validate the handoff with one set of types — the bundle can no longer mean two slightly different things on the two sides of the boundary.

Ocelot proves the other side of the same boundary: stock-client Iceberg REST conformance, demonstrated by a PyIceberg round-trip against the running catalog — spec-correct error types (403 on authorization denial, 409 on a duplicate namespace, 404 on a missing one), listTables, and fail-closed commit-requirement validation — over dependencies moved to the Grust “Lobster” and TypeSec “Torcello” line. The catalog QueryGraph accepts proof from is the same catalog an ordinary Iceberg client simply uses.