Skip to content

fix(metadata): isMissingTableError no longer reads Postgres' write-path missing-COLUMN phrase as a missing TABLE (#6347) - #6613

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-6347-missing-table-column-phrase
Aug 8, 2026
Merged

fix(metadata): isMissingTableError no longer reads Postgres' write-path missing-COLUMN phrase as a missing TABLE (#6347)#6613
baozhoutao merged 2 commits into
mainfrom
claude/issue-6347-missing-table-column-phrase

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6347

The premise, re-measured on origin/main (82397b6)

Confirmed, exactly as filed. Driving the real MISSING_TABLE.message regex against the two Postgres phrasings:

true   PG write-path column | column "label" of relation "sys_team" does not exist
false  PG read-path column  | column "bogus" does not exist
true   PG genuine missing table | relation "sys_metadata_history" does not exist
true   SQLite genuine       | no such table: sys_metadata_history
true   MySQL genuine        | Table 'app.sys_metadata_history' doesn't exist

The write-path phrase carries a complete, legal missing-table phrase — relation "sys_team" does not exist — as a substring, so the table-scoped message test matched it. The function's own docblock names column "x" does not exist (42703) as "a real failure that must stay loud … a case where 'start numbering at 1' would be the wrong answer against a table that may be full of rows". This is the restore-invariant: the code did not honour its own documented contract.

Code-first does not rescue it — matchesDriverError is a sequential OR, so an error carrying code: '42703' falls past both code lines and is decided by its message.

One thing the issue did not name, found by the same probe: the superstring family is wider than columns. Postgres phrases every sub-object of a relation the same way, and constraint "uq_x" of relation "sys_team" does not exist (SQLSTATE 42704) matched too — 42704 being the second of the three neighbours the same docblock already declares must stay loud. Closing it is the same invariant, in the same function, so it is in this PR rather than a follow-up.

The repair

A message regex can never exclude a superstring: once a legal phrase for X appears inside a longer phrase meaning NOT-X, no widening of the X regex removes the match — the phrase really is in there. So the repair is a front-exclusion on the signature, evaluated before any positive test. Two channels:

codes42703 (undefined_column), 42704 (undefined_object: constraint/trigger/role/type), 3D000 (invalid_catalog_name). Exactly the three the docblock already names. Postgres-shaped on purpose: measured, neither MySQL (Unknown column 'label' in 'field list') nor SQLite (no such column: bogus, table t has no column named label) phrases a sub-object failure so that a missing-table phrase falls out of it, so there is nothing there to exclude.

message — the "something inside a relation" phrasing, which therefore says the relation itself is present:

/["'`][^"'`]+["'`]\s+of relation\s/i

Recognising an exclusion ends the question with false and does not descend into cause: an error that identifies as "a column of an existing relation" is that error whatever it wraps, and stopping can only ever subtract benign verdicts, never add one — the direction this module already errs in.

On the copied regex: the two in-repo siblings are mapDataError (packages/rest, #5352) and MISSING_COLUMN_OF_RELATION (service-analytics, #6035 / PR #6346). Both are read-only references here, and neither is imported — a one-line phrase is not worth a cross-package edge. It is deliberately wider than theirs: they extract the column name to phrase a better error, so a miss costs a vaguer message; this one excludes, so a miss restores the corruption. It therefore drops their column / snake-case-identifier / does not exist anchors.

Tests — a phrase corpus, both directions

packages/metadata/src/utils/schema-sync-errors.test.ts gains 25 corpus rows plus 2 named cases, driving the real isMissingTableError:

  • LOUD (must answer false) — write-path phrase in five shapes (bare message, with 42703, thrown as a string, wrapped as cause, single-quoted identifiers); constraint … of relation … (42704); both read-path forms; 42703 with an opaque message; 42703 whose message is a genuine missing-table phrase; role (42704) and database (3D000); MySQL and SQLite missing-column prose.
  • BENIGN (must still answer true, verbatim) — the whole guarded surface: SQLite/libsql with and without a schema prefix and behind a driver prefix, PG message and PG 42P01-with-opaque-message, MySQL message / ER_NO_SUCH_TABLE / errno 1146, a bare string, and a genuine missing table wrapped as cause. An exclusion is a subtraction; the guarded surface may not shrink.
  • Two named cases: the exclusion is not rescued by a nested 42P01 cause, and it does not leak into isSchemaAlreadyExistsError (column "x" of relation "y" already exists is genuinely benign there — the column IS provisioned — and still matches).

Reverse verification, direction predicted before running: deleting MISSING_TABLE.excludes turns the write-path / sub-object rows red, while every BENIGN row and every read-path row stays green. Measured: 8 failed | 48 passed. The direction held; the count came in two above the six predicted by counting phrases, because two rows go red through the code channel rather than the phrase (42703 whose message is a genuine missing-table phrase, and the cause-rescue case). Both are recorded in the test docblock as measured rather than trimmed to fit the prediction.

The asymmetry question — does this close it for ALL consumers?

Yes, at one point, because there is exactly one point. isMissingTableError is re-exported at packages/metadata/src/errors.ts:50 and every consumer asks the shared predicate — no call site open-codes its own copy (metadata-protocol even pins that it must import rather than re-implement, in sys-metadata-repository.history-counters.test.ts). Grepped, all five:

call site verdict on true cost of the misjudgment
packages/metadata/src/loaders/database-loader.ts:310 (nextEventSeq) return 1 event_seq restarts at 1 against a full history table — the one the issue names
packages/metadata-protocol/src/sys-metadata-repository.ts:1212 (historyCounterVerdict) return 1 same, for event_seq and version
packages/objectql/src/engine.ts:2173 (autonumber seed, #5979) return 0 reseeds autonumbers from 1 against existing rows — forged business identifiers, heavier than the issue's example
packages/metadata/src/loaders/database-loader.ts:716 return (answer empty) an outage served as "nothing declared"
packages/metadata-protocol/src/protocol.ts:3162, 10911 return (benign hydration) same shape

All five get strictly louder; none gets quieter. No consumer needs a follow-up, and no consumer behaviour is edited in this PR — return 1 on a genuine missing table is untouched.

Fixture sweep

Swept the predicate's consumption radius (metadata, metadata-protocol, objectql, metadata-core) for the affected phrases: no fixture anywhere spells … of relation …, and every genuine missing-table fixture is the plain relation "x" does not exist / no such table: form, which is unchanged. Nothing to re-spell.

Verification

  • pnpm --filter @objectstack/metadata test27 files, 568 passed
  • pnpm --filter @objectstack/metadata-protocol exec vitest run on the three predicate-consuming suites — 55 passed
  • pnpm --filter @objectstack/objectql exec vitest run on the two outage suites — 33 passed
  • pnpm lint — clean; all 30 check:* gates in the ESLint job, run one by one from .github/workflows/lint.ymlall PASS (incl. check:nul-bytes, check:engine-double-contract, check:route-envelope, check:error-code-casing, check:durability-log-level, check:startup-registry-verdict)
  • check:type-check-coverage, check:driver-conformance, check:stall-guard, check:skill-frame-sync, check:skill-compatibility, check:empty-changeset, check:adr-0087-registration — PASS
  • tsc --noEmit over packages/metadata: zero errors in the two touched files (the package's ledgered DEBT count is unmoved)

Changeset: .changeset/missing-table-column-of-relation.md, @objectstack/metadata: patch. Not declared-breaking, so no ADR-0087 marker is owed (check:adr-0087-registration green).

Out-of-scope finding filed

#6615 — observation-class (finding, no pm:queue, unassigned): the "x" of relation "y" phrase is now open-coded in three packages, while @objectstack/types is the established home for exactly this class of question (#6250). Nothing broken today; recorded so the consolidation happens once and deliberately rather than as a rider.

claude added 2 commits August 8, 2026 06:57
…e as a missing TABLE (#6347)

`MISSING_TABLE`'s message test demands table/relation next to `does not
exist`, but Postgres' write-path phrasing —
`column "label" of relation "sys_team" does not exist` — carries a
complete legal missing-table phrase as a SUBSTRING, so it matched. The
function's own docblock names 42703 as a failure that must stay loud.

A message regex can never exclude a superstring, so the repair is a
front-exclusion evaluated before any positive test: the column-level
SQLSTATEs the docblock already names (42703 / 42704 / 3D000) and the
`"x" of relation "y"` sub-object phrasing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDU3qAuJyajAQm3GkUXdfA
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 7:10am

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata)
  • content/docs/kernel/cluster.mdx (via packages/metadata)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/releases/v12.mdx (via @objectstack/metadata)
  • content/docs/releases/v9.mdx (via @objectstack/metadata)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants