sql-driver: one emission-identity source — loud DDL refusal for redshift/cockroachdb, pgnative joins the Postgres family - #12591
Conversation
… for redshift/cockroachdb, pgnative in the Postgres family Lands the maintainer ruling of 2026-08-25 (option C, plus pgnative joining the Postgres family). The pair `cockroachdb, redshift` is declared once as POSTGRES_WIRE_ONLY_CLIENTS; the connect-timeout table, the wire table and the new DDL refusal all extend the emission sets through it. `pgnative` becomes a member of POSTGRES_EMIT_CLIENTS and reaches the other two tables by derivation. A redshift/cockroachdb datasource reaching schema DDL now gets UnsupportedDialectEmissionError — SQL_DIALECT_EMISSION_UNSUPPORTED / 501 — before any statement is issued, with the supported clients and the skipSchemaSync posture named in the message. mariadb stays out of scope: neither recognised nor refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
📓 Docs Drift CheckThis PR changes 2 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fbb2923bd24f556fd2050922376ed79e336d8a3b && git checkout fbb2923bd24f556fd2050922376ed79e336d8a3b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f93df4dbe314d5133f4a8c395b255ba0e2aeeaff 78586928e22ad42c435c09b62169b4f95719c4e9 && git checkout -B drift-repro f93df4dbe314d5133f4a8c395b255ba0e2aeeaff && git merge --no-ff 78586928e22ad42c435c09b62169b4f95719c4e9
node scripts/docs-audit/affected-docs.mjs --json f93df4dbe314d5133f4a8c395b255ba0e2aeeaff
|
PM review — accepted, flipped ready, auto-merge armed. One protocol failure of mine first.⛔ I never posted the claim comment on #11991The dev's report says the card claimed one was posted and none exists. They are right. I ran the assign, and the comment I wrote in that same turn went to #5499 — a different card. Verified now: before the dev's own reports, #11991 carried exactly one comment, the triage note. Under this repo's rule a claim is assign plus a claim comment carrying the session ID and branch, precisely because all agents share one GitHub identity and the assignee field cannot tell anyone whose claim it is. So for the whole of this dispatch the card was assigned to an identity with no evidence of which session held it. Another seat re-reading the comments — the check that is supposed to catch a double-dispatch — would have found nothing and been entitled to take it. The dev did the right thing: looked for a competing session ID, found none, proceeded, and reported the gap rather than assuming. The claim record is now posted retroactively on #11991, dated honestly. The card was partly stale, and the dev re-measured instead of implementing itThe card's 2026-08-24 drift reading — "identity getter only pg/postgresql, timeout table also cockroachdb, wire table also redshift" — no longer describes the tree. PR #11783 had already converged the three tables onto one source, and #11784 gave redshift its timeout row. What was not converged is the extension: the pair ⭐ The convergence proves itself. The refusal
Two judgement calls I want on the record because both are precedent-driven rather than invented:
AblationPrediction written to a file before the mutation ran, naming five tests individually and naming what must stay green — every
|
…UPPORTED CI's `Type Check · source gates` was red on `check:docs`, which named both files and the exact remedy. The new ErrorCode union member added by this PR's ERROR_CODE_LEDGER registration flows into two GENERATED reference pages. Regenerated with the repo's own tooling (gen:schema then gen:docs); the diff is the two files CI named and nothing else, and `check:docs` now reports '229 generated files in sync with packages/spec'. These pages are generated and carry a do-not-hand-edit banner; nothing here was written by hand.
CI was red on
|
Fixes #11991
Lands the #11756 maintainer ruling (2026-08-25, verbatim 「同意」 on 「C,但 pgnative 归入 Postgres 家族」; record = #11756 comment 5404884704). The ruling's state was re-verified on GitHub before any code was written: #11756 is closed
completedand the comment stands as quoted.Verified at
d215aaa903. Base:b307bfd2ae.Premise re-check first
The card's premise was measured on 2026-08-24 and is partly stale —
sql-driver.tsmoved twice since. Re-measured onorigin/mainatb307bfd2ae(no line numbers quoted below; every anchor is a symbol name):postgres/sqliteclient spellings in the dialect getters #11783 introducedPOSTGRES_EMIT_CLIENTS/SQLITE_EMIT_CLIENTS/MYSQL_EMIT_CLIENTS, the identity getters read them,POSTGRES_WIRE_CLIENTSandDIALECT_CONNECT_TIMEOUTalready extend them, andwithUtcSessionreads the MySQL set. The "identity getter only knows pg/postgresql · timeout table also has cockroachdb · wire table also has redshift" drift the card describes was repaired by fix(driver-sql): recognise knex's canonicalpostgres/sqliteclient spellings in the dialect getters #11783 and driver-sql: aredshiftdatasource gets the pg wire hook but no connect-timeout bound —DIALECT_CONNECT_TIMEOUThas no entry for it, so the 10s dialect bound silently degrades to the 15s pool backstop #11784;cockroachdb, redshiftwas hand-written intoDIALECT_CONNECT_TIMEOUTand again intoPOSTGRES_WIRE_CLIENTS, and the refusal this card adds would have been a third copy.So part 1 of the ruling lands here as naming that extension once, not as a re-convergence of tables that were already converged. The premise otherwise holds in full.
Reproduction, before repairing
Measured on
origin/mainatb307bfd2aewith a throwaway probe (deleted;git statusclean at commit time). Same probe re-run after the change.Before:
…and what knex actually compiles for one
CREATE TABLE, per client:That is the ruling's "silently mis-built table" as a measurement rather than a claim: on Redshift the old path did not fail — it built a different table (
varchar(max)fortext, anot nullid, the primary key moved into a second statement) and the deployment would find out on write.After:
What changed
One declared extension.
POSTGRES_WIRE_ONLY_CLIENTS = { cockroachdb, redshift }is new and is the single place those two names are written.POSTGRES_WIRE_CLIENTSand the pg arm ofDIALECT_CONNECT_TIMEOUTare nowPOSTGRES_EMIT_CLIENTS ∪ POSTGRES_WIRE_ONLY_CLIENTS, and the refusal reads the same set — so "which clients get the connect bound", "which get the #11389 parser" and "which are refused for DDL" cannot drift apart again. The derivation direction is unchanged and documented as load-bearing: emission is the source, never derived from a union of the tables.pgnativejoins the emission family. One name added toPOSTGRES_EMIT_CLIENTS; its wire-table and connect-timeout memberships arrive by derivation with no edit to either table — which is also the proof that the derivation is real. It gains the #11389 calendar-day parser pin it never had, and loses the bareCURRENT_TIMESTAMPDATEdefault (the exact #11550 defect) it inherited by being lumped in with Redshift.The refusal.
SqlDriver.assertDialectEmitsis called fromassertSchemaMutable— the existing single DDL choke point (initObjects/syncSchema,dropTable,rotateShards,reconcileManagedSchema) — after the unchangedschemaModecheck, so it fires only where DDL would previously have proceeded. It throwsUnsupportedDialectEmissionError(packages/drivers/driver-sql/src/dialect-emission-refusal.ts): codeSQL_DIALECT_EMISSION_UNSUPPORTED, status501, structuredclient/operation/supportedClients, and a message rendered from the driver's own tables that names the client, every client the driver does emit for, and theskipSchemaSync/OS_SKIP_SCHEMA_SYNC=1posture that keeps the database usable. It runs beforeensureDatabaseExistsand before the firsthasTableprobe, so nothing is half-built — pinned with a real round-trip.mariadbkeeps its third state. The refusal is keyed on the named set, never on "the getters do not recognise this". A gate keyed ondialectName === 'unknown'would have swept inmariadb(explicitly out of the ruling's scope) and knex's bring-your-own-Client-constructor hatch. Both are pinned as unrecognised and unrefused.Ledger.
SQL_DIALECT_EMISSION_UNSUPPORTEDis registered under a new@objectstack/driver-sqlkey inERROR_CODE_LEDGER(ADR-0112 D3). Registered rather than left driver-local because it is wire-reachable — publishing a drafted object callsengine.syncObjectSchema→SqlDriver.syncSchema→ the DDL gate on a server already serving HTTP. That is the exact test #8035 applied when it unregisteredMONGODB_MULTI_TENANT_UNSUPPORTED(a boot refusal the CLI rethrows pre-HTTP, which no envelope could carry). No standard-catalog member covers the condition:NOT_IMPLEMENTEDmeans "not yet", and the content of the ruling is that this is decided.Part 4 — the rewrite is the ruling landing, done explicitly
sql-driver-11550-dialect-client-spellings.test.tscarried a block whose stated job was to be consciously rewritten when #11756 was decided. It is rewritten here as its own act, with the ruling named on the line:redshift and cockroachdb parse pg wire but do NOT emit Postgres DDL→… and are REFUSED for DDL: the silent fall-through assertions are replaced by the refusal, with the wire and connect-timeout memberships pinned as unchanged;pgnativeas the Postgres family, asserted off a live knex client (dialect === 'postgresql',driverName === 'pgnative') rather than from knex's docs;mariadbcase gains thenot.toThrow()half;pgnative, and the connect-timeout membership case now states that the extension is declared once.The refusal's own behaviour is pinned in a new
sql-driver-11991-emission-identity-refusal.test.ts(11 cases).Clause ② — yes
git diff --stat b307bfd2ae..d215aaa903:Accept/reject behaviour moves in both directions, and the public surface widens:
redshift/cockroachdbdatasource that previously completed schema sync now throws at the DDL gate (packages/drivers/driver-sql/src/sql-driver.ts,assertSchemaMutable→assertDialectEmits);pgnativedatasource gains emission identity, a differentDATEdefault, a connect bound and a wire parser it did not have;packages/spec/src/api/error-code-ledger.zod.tsgains a member of the generatedErrorCodeunion, and@objectstack/driver-sql's index exports a new error class, code and renderer.needs:contract-reviewis already on the card and the measurement agrees with it.Verification
Every exit code captured before any pipe (
cmd > log 2>&1; rc=$?). Heavy commands ran throughscripts/pm/os-verify-lock.sh; where a wrapper script held the lock, the wrapper's own exit is not the verdict — per-command exits are quoted below.Union re-derived on the real changeset after the final commit:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(answer sourced from this checkout ofobjectstack-ai/objectstack; 6 paths; 176 families discovered).Green (per-command exit 0, verdict lines quoted where the gate prints one):
@objectstack/driver-sqlfull suiteTest Files 138 passed | 8 skipped (146)·Tests 2125 passed | 127 skipped (2252)@objectstack/driver-sqltypechecktsc --noEmit, exit 0@objectstack/spectypecheckcheck:test-typecheck: OK — @objectstack/spec's test layer compiles … 55 file(s) / 263 error(s) held(unchanged)@objectstack/specsrc/api+type-alias-convention.pinTest Files 40 passed (40)·Tests 1257 passed (1257)@objectstack/driver-sqlite-wasmTest Files 25 passed (25)·Tests 395 passed (395)@objectstack/driver-tursoTests 1001 passed (1001); one file (date-bucket-parity.test.ts) first failed to collect —Failed to resolve entry for package "@objectstack/verify", an unbuilt closure, not a finding. After--filter '@objectstack/driver-turso^...' buildit passesTest Files 1 passed (1)pnpm lint(whole repo,eslint . --no-inline-config)check:dispatcher-error-vocabularycheck-dispatcher-error-vocabulary: OK — 21 unregistered code-stamping site(s), all classified(unchanged count: the new code is registered, so it is not a site)check:error-code-casingcheck:error-status-conformance✓ every derivable runtime status is documented, and every documented status is reachable.check-adr-0087-registration✓ … 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition—not-required (no-migration-prescription)check:nul-bytescheck-nul-bytes: OK (scanned 6939 text file(s) … no raw ASCII control bytes)check:driver-conformance·check:engine-double-contract·check:where-matcher·check:objectql-double-limit·check:query-options-erasure·check:test-source-alias·check:cross-package-test-inputs(+scripts/check-cross-package-test-inputs.mjs) ·check:type-source-resolution·check:published-files·check:page-declaration-shape·check:slot-lookup·check:type-check-coverage·check-comment-mask-adoption·check-plugin-teardown-shape·check-ci-filter-parity·check:authorable-surface·check:liveness·check:empty-state·check:strictness-ledger·check:variant-docs·check:spec-parsed-alias·check:doc-authoring·check:doc-formula-expressions·check:merge-driver·check:changeset-gate-self-tests·check:objectui-changeset·check-changeset-no-major·check-empty-changeset·release-rehearsal-clone --self-test·docs-audit/check-affected-docsRed: none.
Not measured — declared, not counted as green:
check:type-check-debt(--re-measure) — needs the whole workspace built; its structural half (check:type-check-coverage) is green. CI owns it.docs-audit/check-drift-comment— needs PR context that does not exist locally.driver-sql, and 3 skipped insql-driver-11389-date-tz-skew.test.ts(set OS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URL). No PostgreSQL or MySQL server exists in this container; CI'sTemporal Conformance (live PG + MySQL)job runs them.pgnative's new wire pin is asserted through the recording-connection probe, not against a live server.driver-sqlwere not swept in full — narrowed, deliberately, to the two packages that actually subclassSqlDriverand therefore inherit the changed gate (driver-sqlite-wasm,driver-turso). Both resolveSqlDriverthrough packageexports→dist, sodriver-sqlwas rebuilt first and the reach proved on disk before the runs:SQL_DIALECT_EMISSION_UNSUPPORTED×1,POSTGRES_WIRE_ONLY_CLIENTS×11,'pgnative'×1 indist/index.js.Ablation — one leg, prediction written first
Prediction recorded before the mutation (scratchpad
ablation-prediction.txt): direction red, baseline 0 failures → exactly 5, named test by test, with the pgnative cases, the wire/timeout cases and bothnot.toThrow()cases predicted to stay green (an absent refusal cannot redden an assertion that nothing is thrown).Mutation:
assertDialectEmitsreturns unconditionally. Proved on disk before any result was read —grep -cFon the original predicate1 → 0, on the injected marker0 → 1,git diff --stat1 file / 1 insertion / 1 deletion. Restored undertrap … EXIT INT TERM; restore verified by an emptygit diff --stat, cleangit status, predicate back at 1 and marker at 0.Result — 5 failed | 24 passed, and the five are the five predicted, by name:
No rebuild was needed for this leg and the reason was checked rather than assumed: both suites import
./sql-driver.jsrelative, inside the same package, so vitest resolves the mutated source with noexports→disthop. The lock wrapper reportedcommand-exit 0for that hold while the inner vitest exit was1— the per-command exit is the verdict, and it is the one quoted here.Not done, on purpose
mariadb— out of the ruling's scope; unchanged, and now pinned as neither recognised nor refused.content/docs/mentionsredshift,cockroachdborpgnative, so nothing published went stale.docs-audit/check-affected-docsis green.dialect-emission-refusal.ts's docblock.Generated by Claude Code
Generated by Claude Code