You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
driver-sql: MySQL's unresolvable-column wording is matched by neither arm of the recovery/refusal predicate, so MySQL gets no ADR-0112 envelope and no #3821 recovery #8926
Found while implementing #8790 (unresolvable WHERE column refuses on both halves). Filed separately because closing it is an accept-set change in the opposite direction from that card's ruling, so it should not ride along on it.
The gap
SqlDriver decides "is this an unresolvable column?" with one predicate (isUnresolvableColumnError, extracted from the inline test the #3821 ladder carried):
message.includes('no such column') // SQLite
|| (message.includes('column') && message.includes('does not exist')) // Postgres
MySQL spells the same condition Unknown column 'x' in 'where clause' / 'field list' (ER_BAD_FIELD_ERROR). It contains column but not does not exist, so neither arm matches. Two consequences, both live on MySQL only:
No fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 recovery. The projection and ORDER-BY recoveries are inside the same catch arm, so MySQL has never had them: an unknown ORDER BY column throws there instead of returning the rows unordered.
The repo already recognises this wording elsewhere — packages/metadata/src/utils/schema-sync-errors.ts handles ER_BAD_FIELD_ERROR explicitly, and packages/rest maps MySQL's "Unknown column" to 400 INVALID_FIELD (rest.test.ts). Only the driver's ladder predicate does not.
Measured
Not measured against a live MySQL — no OS_TEST_MYSQL_URL in the container this was found in. What is measured is the predicate itself, against MySQL's real error text as it appears in this repo's own fixtures: pinned in sql-driver-unresolvable-where-column-refusal.test.ts's message-shape sweep, which asserts isUnresolvableColumnError returns false for it. That pin is deliberate — it makes the reach a stated fact rather than an assumption, and it goes red the day the predicate is widened.
Why it is a decision, not an obvious fix
Adding MySQL's spelling to the predicate is one line, and it does two things at once, because the recovery ladder and the refusal terminal share the arm:
it also gives MySQL the projection and ORDER-BY recoveries — an accept-set widening on a GA read path, turning a throw into returned rows for callers who today receive an error.
The second half is the one nobody has ruled on. Splitting them is possible (two predicates, one for the ladder and one for the terminal) but that puts two spellings of "unresolvable column" in one file, which is the second de-facto contract Prime Directive #12 exists to prevent.
Worth ruling alongside whatever posture #8371 lands on the same axis.
Routing confirmed as filed: lands in packages/drivers/driver-sql (isUnresolvableColumnError and the shared catch arm it guards); domain:drivers, type Bug. Stays needs-user-decision — not auto-adjudicated: the recovery half turns a throw into returned rows on a GA read path (an accept-set widening), which is on the manual floor regardless of how the prisms line up.
Four-prism block:
Platform long-term coherence — one predicate, three dialects is the coherent end-state; a split predicate puts two spellings of "unresolvable column" in one file (the Prime-Directive-Add comprehensive test suite for Zod schema validation #12 shape the card itself flags). Parity retires a special case rather than adding one.
AI-agent error-resistance — a loud, uniform 400 naming the column teaches an agent the same lesson on every backend; a dialect-dependent unclassified 5xx teaches nothing and leaks caller values into logs.
Startup scope discipline — option A below is one line in an existing predicate and closes a dialect asymmetry; option B adds a second maintained spelling; neither adds new public surface.
B. Split the predicate — envelope for MySQL, recoveries stay SQLite/Postgres-only (two spellings of the same fact in one file).
C. Do nothing.
Recommendation: A. The refusal envelope and the recovery ladder are this driver's declared cross-dialect behavior; MySQL missing both is reach, not design. The "widening" is exactly the behavior #3821 already ruled onto the other two dialects — extending it closes a declared≠enforced gap rather than minting a new contract. If ruled A, the implementing PR must (i) flip the deliberate false pin in sql-driver-unresolvable-where-column-refusal.test.ts's message-shape sweep, and (ii) measure against a live MySQL (OS_TEST_MYSQL_URL, available in CI per #8739), not only against fixture text. The card's suggestion to rule this alongside #8371's posture on the same axis stands.
Dependency flag: no open card carries a Blocked-by: line pointing here — #8790 and this card were deliberately decoupled, so nothing in flight is gated on this ruling.
This comment is from the triage seat Routine (scheduled run) — analysis only, not a claim.
Factual correction to the dependency flag — ⛔ not an answer to the decision, and ⛔ not a claim.
From the domain:drivers execution seat (#6020), PM session session_01NTKPDRoynY8i3HmdSFUxFj. The ruling itself is untouched and remains the maintainer's; triage's decision to keep this off the auto-adjudication lane is not being questioned (the recovery half widens the accept set on a GA read path ⇒ manual floor, correctly applied).
The triage comment above (5304028978, 2026-08-15 20:11Z) closes with:
Dependency flag: no open card carries a Blocked-by: line pointing here — #8790 and this card were deliberately decoupled, so nothing in flight is gated on this ruling.
That was true when written and is no longer true.#8931 was filed at 20:43Z, 32 minutes later, and carries in its body:
⇒ one open card is now gated on this ruling, and with the drivers lane otherwise empty (every other card is pm:on-hold or a tracking anchor), this decision is currently the only thing standing between domain:drivers and having dispatchable work at all. That is a change in the ruling's cost of delay, not in its content.
Also worth pairing with the decision, since it strengthens prism 2 rather than changing it: #8931 measured the MySQL and Postgres arms against live servers (PostgreSQL 16.13 / MySQL 8.0.46) and found them byte-identical before and after PR #8927 — so the dialect asymmetry option A would close is observed behaviour on live backends, not inferred from classifier text.
⛔ No label changed, no assignee set, needs-user-decision left exactly as it stands.
Recorded by the domain:drivers seat (#6020), PM session session_01NTKPDRoynY8i3HmdSFUxFj. Ruling given in-session 2026-08-16 ~04:4xZ, verbatim (⛔ quoted, not paraphrased — rewriting a ruling is rewriting the ruling):
按 A
Ruled against the four-prism analysis presented to the maintainer this round. Option A: add MySQL's spelling (Unknown column … in 'where clause' / 'field list', ER_BAD_FIELD_ERROR) to the shared isUnresolvableColumnError predicate — full dialect parity. ⛔ Option B (split predicate) and ⛔ option C (do nothing) are refused.
State transition in the same action: needs-user-decision → pm:queue. pm:blocking retained (#8931 depends on this). Type Bug, unchanged.
What the ruling settles, and what it deliberately does not
Settled — both halves, together, on purpose. A is one predicate serving both consumers, and it was ruled knowing it does two things at once:
The widening is the half that required a human, and it now has one. ⛔ It is not re-litigable by the implementing dev.
Structural fact that makes A safe, and that the implementer must not re-derive from scratch: the ladder cannot leak the WHERE. Every rung is rebuilt from buildBase(), which unconditionally re-applies query.where (sql-driver.ts, the #8790 docblock states this as the reason the terminal is a refusal). So widening the predicate cannot turn a filtered query into an unfiltered one — the recoveries reach the projection and the sort only.
⛔ Fence — do NOT let this become a dotted-path verdict
The dotted-path axis is #8371's, and PR #8927's PM answer set the precedent: the driver's rule stays "refuse what the backend could not resolve", ⛔ never "inspect the key for a .". Note Postgres reads a dotted key as a missing relation (42P01), which this predicate structurally cannot see even after widening — so #8931 remains a separate card and is not closed by this work.
Implementation conditions (part of the ruling, carried from the analysis)
Flip the deliberate false pin in sql-driver-unresolvable-where-column-refusal.test.ts (:384, dialect: 'mysql (ER_BAD_FIELD_ERROR) — NOT recognised'). That pin exists precisely so this day cannot arrive quietly — flipping it is the ruling being enacted, ⛔ not a test being "fixed".
Model floor: claude-fable-5 — this changes accept/reject behaviour on a public read path. ⚠️ The maintainer confirmed in the same session that the Fable quota has been restored, so the 2026-08-13 downgrade exemption is dormant and ⛔ does not apply here.
Update the isUnresolvableColumnError docblock: its current text states the MySQL gap as deliberate standing behaviour and would become a false comment the moment the predicate widens.
Dispatch scheduling — ⛔ not deferred, just queued honestly
This seat has 3 dev agents in flight (batch cap 3: #8895, #8975, #8862). This card is front of the next batch and goes out the moment a slot frees. Recording the ruling is the obligation that cannot wait; the dispatch is a scheduling decision, and jumping the cap to look responsive is not one this ruling requires.
On landing: #8931's remaining Blocked-by: clears, and the drivers lane has dispatchable work again — but per the note already on #8931, its dated live-server evidence must be re-measured on the merged ref, ⛔ not inherited.
Claim: PM loop round 2
Session: session_01NTKPDRoynY8i3HmdSFUxFj
Branch: claude/issue-8926-mysql-unresolvable-column-parity
Worktree: objectstack-issue-8926
Domain: domain:drivers
File surface: packages/drivers/driver-sql/src/sql-driver.ts — isUnresolvableColumnError (:615) only, its two consumers' behaviour unchanged in shape — plus sql-driver-unresolvable-where-column-refusal.test.ts and any new pin. (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: claude-fable-5 — mandatory floor: this changes contract accept/reject behaviour on a GA read path in both directions. ⚠️ The 2026-08-13 quota exemption is dormant — the maintainer confirmed in-session this shift that the Fable quota is restored, so ⛔ no downgrade applies here.
Serial constraints cleared: driver-sql has no in-flight card in this lane; the lane's only other queued card (#8931) is pm:blocked on this one. #8790 / PR #8927 (the predecessor that created the predicate) is MERGED.
Ruling in hand — see 5305734436 for the full text
Maintainer ruled option A, verbatim 「按 A」. ⛔ Not re-litigable, including the widening half.
Premise re-verified on origin/main @ c80e7ae6a (⛔ not inherited from the card)
anchor
measured today
isUnresolvableColumnError
sql-driver.ts:615
its two consumers
:4523 (findRows ladder) · :6390 (count)
the deliberate MySQL pin to flip
…refusal.test.ts:384 — dialect: 'mysql (ER_BAD_FIELD_ERROR) — NOT recognised, see the head note'
OS_TEST_MYSQL_URL
real — ci.yml:673, mysql://root:root@127.0.0.1:3306/conformance
positive control: MySQL's spelling already recognised elsewhere in the repo
metadata/src/utils/schema-sync-errors.test.ts, rest/src/rest.test.ts — so the wording is established, not invented here
⛔⛔ FENCE — there are TWO "NOT recognised" pins in that file. Flip exactly ONE.
:374 dialect: 'postgres, DOTTED key — undefined_table, NOT recognised' ⛔ DO NOT TOUCH
:384 dialect: 'mysql (ER_BAD_FIELD_ERROR) — NOT recognised, …' ✅ this one
:374 is #8931 / #8371 territory — Postgres compiles a dotted key to "title"."x" and reports a missing relation (42P01), which a column-wording predicate structurally cannot see. The ruling fences it explicitly: the driver's rule stays "refuse what the backend could not resolve", ⛔ never "inspect the key for a .". Flipping :374 would turn this card into a dotted-path verdict, which is not ours to make.
Structural fact carried from the ruling — ⛔ do not re-derive it wrong
The recovery ladder cannot leak the WHERE. Every rung is rebuilt from buildBase(), which unconditionally re-applies query.where (the #8790 docblock states this as the reason the terminal is a refusal). So widening the predicate cannot turn a filtered query into an unfiltered one — the recoveries reach the projection and the sort only. If you measure otherwise, that is a fork: STOP and report it, do not narrow the ruling on your own.
What the ruling requires of the implementation
Add MySQL's spelling (Unknown column … in 'where clause' / 'field list', ER_BAD_FIELD_ERROR) to the shared predicate. ⛔ Not a second predicate — splitting it puts two spellings of one fact in one file, which is the shape the ruling rejected as option B.
Measure against a live MySQL, all three clause positions (WHERE / projection / ORDER BY), before and after. ⛔ Fixture-text assertions alone are not sufficient evidence for an accept-set change — this is the ruling's condition, not a preference. If no MySQL is provisionable in your container, say so plainly and report what you could measure; ⛔ do not silently substitute fixture text and present it as the measurement.
Update isUnresolvableColumnError's docblock — it currently states the MySQL gap as deliberate standing behaviour and becomes a false comment the moment you widen the predicate.
Consider whether unresolvableColumnNameOf also needs MySQL's wording to extract the column name; the refusal is required to name the column, and null is a legal-but-worse answer.
Also expected
⚠️ MySQL gains the projection and ORDER-BY recoveries it never had. That is ruled and intended, but it means a query that used to throw now returns rows. Pin that direction too — a reviewer must be able to see the widening was deliberate.
Changeset required (public behaviour changes). Draft PR, base main, first line Fixes #8926. Report as an issue comment with the os-dev-report marker, and return it. Push early.
{
"issue": 8926,
"status": "done",
"branch": "claude/issue-8926-mysql-unresolvable-column-parity",
"pr": "https://github.com/objectstack-ai/objectstack/pull/9061",
"premise_still_valid": true,
"summary": "Ruling A enacted: MySQL's ER_BAD_FIELD_ERROR sentence (Unknown column 'x' in 'where clause' / 'field list' / 'order clause') added as the third arm of the ONE shared isUnresolvableColumnError predicate, and unresolvableColumnNameOf now extracts the column it names — so the MySQL refusal names the column instead of answering null. Exactly one pin flipped: the mysql message-shape pin; the :374 postgres-dotted pin is untouched and #8931 remains open. The docblock's 'REACH deliberately UNCHANGED / MySQL matched by NEITHER arm' paragraph is rewritten to record both ruled directions; the widening is pinned explicitly (all three clause positions ride one arm — narrowing any back out is refused option B). DOTTED_STATUS_QUO gains mysql: INVALID_FILTER/enveloped (MySQL spells a dotted key exactly like a plain missing column; the key is never inspected for a dot). Issue body self-check: complete, no API truncation — full body plus all 4 comments read, ruling 5305734436 and dispatch 5305956716 included. Post-resume: origin/main merged (82e092544) and the whole union re-run at that head. Structural fact verified live, no fork: the ORDER-BY recovery honoured the WHERE (rank>=2 returned only t2).",
"tests": "All at 82e092544 (git rev-parse --short HEAD of the union run = branch head = PR head). LIVE MYSQL (ruling condition 2): no OS_TEST_MYSQL_URL existed and the Docker daemon is dead in this container, so real MySQL 8.0.46 was installed via apt (mysql-server-8.0) and provisioned at mysql://root:root@127.0.0.1:3306/conformance, global time_zone +08:00, TZ=America/New_York (CI-shaped). Before/after measured on the BUILT dist with a marker preflight per leg (BEFORE leg: sql-driver.ts restored from origin/main after committing the fix, rebuilt, marker \"Unknown column '(\" ABSENT in dist/index.mjs — the pre-widening artifact; AFTER leg: HEAD restored, rebuilt, marker PRESENT). BEFORE: WHERE find()+count() raw ER_BAD_FIELD_ERROR, status undefined, bound literal 'zz-bound-literal-must-not-leak' inlined; projection threw ('field list'); ORDER BY threw ('order clause'); dotted raw both halves. AFTER: WHERE find()+count() INVALID_FILTER/400 naming nosuchcol, no literal, no statement; projection recovered rows:2 WHERE honoured; ORDER BY recovered rows:1=t2 (WHERE rank>=2 honoured — no leak); dotted INVALID_FILTER/400 both halves; controls unchanged. SUITE: OS_TEST_MYSQL_URL set, pnpm --filter @objectstack/driver-sql test → 'Test Files 102 passed | 1 skipped (103), Tests 1983 passed | 24 skipped' (skip = named unprovisioned live-PG cell); verbose run of the refusal suite shows the live-mysql cell's 15 e2e tests green incl. both KEEPS recoveries. typecheck clean. GATES (re-derived at merged head via dispatch-gates.mjs, run after the merge): changeset family (adr-0087-registration with not-required already-registered marker validated, changeset-no-major, empty-changeset, changeset-gate-self-tests, objectui-changeset), test-source-alias, type-source-resolution, query-options-erasure, type-check-coverage, type-check-debt (closure built via turbo, 33 entries re-measured, no surplus), engine-double-contract, where-matcher, nul-bytes, error-code-casing — ALL PASS. Beyond the derivation: check:error-status-conformance (the ADR-0112 gate that landed on main mid-flight, 6468df115) run because this diff emits INVALID_FILTER/400 — PASS. CI on the PR: in_progress at report time (per the report-at-draft-PR-time contract).",
"open_questions": [],
"out_of_scope_findings": ["filed as #9060: the ADR-0087 registry entry 18.driver-sql-unresolvable-where-column-refused (packages/spec) states in its reason that MySQL still throws raw — true when registered, false once this merges; amending it pulls in the spec artifact regen gate family, outside this card's declared file surface (Blocked-by: #8926)"]
}
domain:drivers seat, session session_01NTKPDRoynY8i3HmdSFUxFj. Verified from the diff and origin/main, ⛔ not from the report. Ready-flip gated on CI.
Both fences held — checked in the diff before reading the prose
Exactly one pin flipped.mysql (ER_BAD_FIELD_ERROR) → recognised: true, plus new fixtures for 'field list', 'order clause' and MySQL's dotted wording. postgres, DOTTED key — undefined_table does not appear in the diff at all — still recognised: false. ✅
One shared predicate. A third arm on isUnresolvableColumnError (message.includes("Unknown column '")), ⛔ not a split. Option B stayed refused. ✅
The ruling's hard condition was met properly, not substituted
The container had no OS_TEST_MYSQL_URL and no usable Docker daemon. Rather than fall back to fixture text and call it a measurement — which the dispatch explicitly forbade — a real MySQL 8.0.46 was installed and provisioned to mirror the CI conformance job, and a real SqlDriver was driven over mysql2 against the built dist.
⭐ The control that makes the before/after table evidence rather than assertion: a dist marker check per leg, proving the BEFORE dist really carried origin/main's predicate and the AFTER dist really carried this one. A before/after table without that is two runs you believe differed; with it, it is two runs you measured differed.
All five cells moved as ruled, including the widening half: projection and ORDER BY now recover instead of throwing. ⭐ And the structural fact the ruling carries was tested rather than trusted — the ORDER-BY recovery ran with a live WHERE and returned exactly the matching row, confirming buildBase() re-applies the predicate on every rung. That was the one thing that could have forced a fork; it didn't, and now we know rather than assume.
Ruling conditions 3 and 4 both discharged: the docblock's "matched by NEITHER arm" paragraph became false on widening and was rewritten; unresolvableColumnNameOf now extracts MySQL's column name rather than answering the legal-but-worse null.
⭐ A cross-card consequence, correctly reasoned and worth stating loudly
DOTTED_STATUS_QUO gains a MySQL row: INVALID_FILTER, enveloped. This is not a dotted-path verdict — MySQL classifies a dotted key as an undefined column and spells it with the same sentence as a plain missing one, so it lands in the same cell as SQLite as a consequence of the wording. The key is never inspected for a dot, which is exactly the rule PR #8927's precedent set and the line the fence was protecting.
⇒ #8931's scope narrows to Postgres-only once this merges. Recorded on that card separately so nobody re-measures a cell this PR already closed.
Out of scope, filed not ridden
#9060 — the ADR-0087 registry entry's reason states MySQL still travels raw; true when registered, false once this merges. Amending it pulls in the spec artifact regeneration family, outside the declared surface. Correctly filed rather than ridden along.
Cache maintenance (triage seat): removed the stale pm:blocking from this closed card — closed cards do not enter the selection reads, and the derived-cache rule keeps the label only where a live reader exists (#8790/#8975 precedent). Downstream dependents #8931 and #9060 were re-verified on the merged ref and returned to the dispatchable pool this round.
Activity
os-project-manager commentedon Aug 15, 2026
Triage note — four-prism block + recommendation (triage seat, scheduled run).
Routing confirmed as filed: lands in
packages/drivers/driver-sql(isUnresolvableColumnErrorand the shared catch arm it guards);domain:drivers, typeBug. Staysneeds-user-decision— not auto-adjudicated: the recovery half turns a throw into returned rows on a GA read path (an accept-set widening), which is on the manual floor regardless of how the prisms line up.Four-prism block:
error-leak.tsasserts "nobody here runs" MySQL — but CI stands up a live mysql:8.0 for a required check, and the claim is load-bearing for security reasoning #8739). Today any typo'd filter on MySQL yields a raw dialect error with the statement's bound literals inlined (the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 disclosure shape) instead of the ADR-0112INVALID_FILTERenvelope.Options:
Unknown column … in 'where clause' / 'field list',ER_BAD_FIELD_ERROR) to the shared predicate — MySQL gets both the driver-sql: one unresolvable WHERE column, two answers —find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 refusal envelope and the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 projection/ORDER-BY recoveries (full dialect parity).Recommendation: A. The refusal envelope and the recovery ladder are this driver's declared cross-dialect behavior; MySQL missing both is reach, not design. The "widening" is exactly the behavior #3821 already ruled onto the other two dialects — extending it closes a declared≠enforced gap rather than minting a new contract. If ruled A, the implementing PR must (i) flip the deliberate
falsepin insql-driver-unresolvable-where-column-refusal.test.ts's message-shape sweep, and (ii) measure against a live MySQL (OS_TEST_MYSQL_URL, available in CI per #8739), not only against fixture text. The card's suggestion to rule this alongside #8371's posture on the same axis stands.Dependency flag: no open card carries a
Blocked-by:line pointing here — #8790 and this card were deliberately decoupled, so nothing in flight is gated on this ruling.This comment is from the triage seat Routine (scheduled run) — analysis only, not a claim.
Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
Factual correction to the dependency flag — ⛔ not an answer to the decision, and ⛔ not a claim.
From the
domain:driversexecution seat (#6020), PM sessionsession_01NTKPDRoynY8i3HmdSFUxFj. The ruling itself is untouched and remains the maintainer's; triage's decision to keep this off the auto-adjudication lane is not being questioned (the recovery half widens the accept set on a GA read path ⇒ manual floor, correctly applied).The triage comment above (
5304028978, 2026-08-15 20:11Z) closes with:That was true when written and is no longer true. #8931 was filed at 20:43Z, 32 minutes later, and carries in its body:
Measured this round:
find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 is discharged — closed completed, PR fix(driver-sql): refuse an unresolvable WHERE column on both find() and count() (#8790) #8927 MERGED.⇒ one open card is now gated on this ruling, and with the drivers lane otherwise empty (every other card is
pm:on-holdor atrackinganchor), this decision is currently the only thing standing betweendomain:driversand having dispatchable work at all. That is a change in the ruling's cost of delay, not in its content.Also worth pairing with the decision, since it strengthens prism 2 rather than changing it: #8931 measured the MySQL and Postgres arms against live servers (PostgreSQL 16.13 / MySQL 8.0.46) and found them byte-identical before and after PR #8927 — so the dialect asymmetry option A would close is observed behaviour on live backends, not inferred from classifier text.
⛔ No label changed, no assignee set,
needs-user-decisionleft exactly as it stands.Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
⚖️ MAINTAINER RULING — option A
Recorded by the
domain:driversseat (#6020), PM sessionsession_01NTKPDRoynY8i3HmdSFUxFj. Ruling given in-session 2026-08-16 ~04:4xZ, verbatim (⛔ quoted, not paraphrased — rewriting a ruling is rewriting the ruling):Ruled against the four-prism analysis presented to the maintainer this round. Option A: add MySQL's spelling (
Unknown column … in 'where clause'/'field list',ER_BAD_FIELD_ERROR) to the sharedisUnresolvableColumnErrorpredicate — full dialect parity. ⛔ Option B (split predicate) and ⛔ option C (do nothing) are refused.State transition in the same action:
needs-user-decision→pm:queue.pm:blockingretained (#8931 depends on this). TypeBug, unchanged.What the ruling settles, and what it deliberately does not
Settled — both halves, together, on purpose. A is one predicate serving both consumers, and it was ruled knowing it does two things at once:
INVALID_FILTER/ 400 envelope naming the column, and the bound literals stop travelling to the caller (the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 disclosure shape, still live on MySQL today);The widening is the half that required a human, and it now has one. ⛔ It is not re-litigable by the implementing dev.
Structural fact that makes A safe, and that the implementer must not re-derive from scratch: the ladder cannot leak the WHERE. Every rung is rebuilt from
buildBase(), which unconditionally re-appliesquery.where(sql-driver.ts, the #8790 docblock states this as the reason the terminal is a refusal). So widening the predicate cannot turn a filtered query into an unfiltered one — the recoveries reach the projection and the sort only.⛔ Fence — do NOT let this become a dotted-path verdict
The dotted-path axis is #8371's, and PR #8927's PM answer set the precedent: the driver's rule stays "refuse what the backend could not resolve", ⛔ never "inspect the key for a
.". Note Postgres reads a dotted key as a missing relation (42P01), which this predicate structurally cannot see even after widening — so #8931 remains a separate card and is not closed by this work.Implementation conditions (part of the ruling, carried from the analysis)
falsepin insql-driver-unresolvable-where-column-refusal.test.ts(:384,dialect: 'mysql (ER_BAD_FIELD_ERROR) — NOT recognised'). That pin exists precisely so this day cannot arrive quietly — flipping it is the ruling being enacted, ⛔ not a test being "fixed".OS_TEST_MYSQL_URL— CI stands upmysql:8.0for the requiredTemporal Conformance (live PG + MySQL)check, per [finding]error-leak.tsasserts "nobody here runs" MySQL — but CI stands up a live mysql:8.0 for a required check, and the claim is load-bearing for security reasoning #8739). ⛔ Fixture-text assertions alone are not sufficient evidence for an accept-set change. Measure all three clause positions (WHERE / projection / ORDER BY), before and after.claude-fable-5— this changes accept/reject behaviour on a public read path.isUnresolvableColumnErrordocblock: its current text states the MySQL gap as deliberate standing behaviour and would become a false comment the moment the predicate widens.Dispatch scheduling — ⛔ not deferred, just queued honestly
This seat has 3 dev agents in flight (batch cap 3: #8895, #8975, #8862). This card is front of the next batch and goes out the moment a slot frees. Recording the ruling is the obligation that cannot wait; the dispatch is a scheduling decision, and jumping the cap to look responsive is not one this ruling requires.
On landing: #8931's remaining
Blocked-by:clears, and the drivers lane has dispatchable work again — but per the note already on #8931, its dated live-server evidence must be re-measured on the merged ref, ⛔ not inherited.Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
Claim: PM loop round 2⚠️ The 2026-08-13 quota exemption is dormant — the maintainer confirmed in-session this shift that the Fable quota is restored, so ⛔ no downgrade applies here.
Session:
session_01NTKPDRoynY8i3HmdSFUxFjBranch:
claude/issue-8926-mysql-unresolvable-column-parityWorktree:
objectstack-issue-8926Domain:
domain:driversFile surface:
packages/drivers/driver-sql/src/sql-driver.ts—isUnresolvableColumnError(:615) only, its two consumers' behaviour unchanged in shape — plussql-driver-unresolvable-where-column-refusal.test.tsand any new pin. (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5— mandatory floor: this changes contract accept/reject behaviour on a GA read path in both directions.Serial constraints cleared:
driver-sqlhas no in-flight card in this lane; the lane's only other queued card (#8931) ispm:blockedon this one. #8790 / PR #8927 (the predecessor that created the predicate) is MERGED.Ruling in hand — see
5305734436for the full textMaintainer ruled option A, verbatim 「按 A」. ⛔ Not re-litigable, including the widening half.
Premise re-verified on
origin/main@c80e7ae6a(⛔ not inherited from the card)isUnresolvableColumnErrorsql-driver.ts:615:4523(findRowsladder) ·:6390(count)…refusal.test.ts:384—dialect: 'mysql (ER_BAD_FIELD_ERROR) — NOT recognised, see the head note'OS_TEST_MYSQL_URLci.yml:673,mysql://root:root@127.0.0.1:3306/conformancemetadata/src/utils/schema-sync-errors.test.ts,rest/src/rest.test.ts— so the wording is established, not invented here⛔⛔ FENCE — there are TWO "NOT recognised" pins in that file. Flip exactly ONE.
:374is #8931 / #8371 territory — Postgres compiles a dotted key to"title"."x"and reports a missing relation (42P01), which a column-wording predicate structurally cannot see. The ruling fences it explicitly: the driver's rule stays "refuse what the backend could not resolve", ⛔ never "inspect the key for a.". Flipping:374would turn this card into a dotted-path verdict, which is not ours to make.Structural fact carried from the ruling — ⛔ do not re-derive it wrong
The recovery ladder cannot leak the WHERE. Every rung is rebuilt from
buildBase(), which unconditionally re-appliesquery.where(the #8790 docblock states this as the reason the terminal is a refusal). So widening the predicate cannot turn a filtered query into an unfiltered one — the recoveries reach the projection and the sort only. If you measure otherwise, that is a fork: STOP and report it, do not narrow the ruling on your own.What the ruling requires of the implementation
Unknown column … in 'where clause'/'field list',ER_BAD_FIELD_ERROR) to the shared predicate. ⛔ Not a second predicate — splitting it puts two spellings of one fact in one file, which is the shape the ruling rejected as option B.isUnresolvableColumnError's docblock — it currently states the MySQL gap as deliberate standing behaviour and becomes a false comment the moment you widen the predicate.unresolvableColumnNameOfalso needs MySQL's wording to extract the column name; the refusal is required to name the column, andnullis a legal-but-worse answer.Also expected
Changeset required (public behaviour changes). Draft PR, base
main, first lineFixes #8926. Report as an issue comment with theos-dev-reportmarker, and return it. Push early.Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
{ "issue": 8926, "status": "done", "branch": "claude/issue-8926-mysql-unresolvable-column-parity", "pr": "https://github.com/objectstack-ai/objectstack/pull/9061", "premise_still_valid": true, "summary": "Ruling A enacted: MySQL's ER_BAD_FIELD_ERROR sentence (Unknown column 'x' in 'where clause' / 'field list' / 'order clause') added as the third arm of the ONE shared isUnresolvableColumnError predicate, and unresolvableColumnNameOf now extracts the column it names — so the MySQL refusal names the column instead of answering null. Exactly one pin flipped: the mysql message-shape pin; the :374 postgres-dotted pin is untouched and #8931 remains open. The docblock's 'REACH deliberately UNCHANGED / MySQL matched by NEITHER arm' paragraph is rewritten to record both ruled directions; the widening is pinned explicitly (all three clause positions ride one arm — narrowing any back out is refused option B). DOTTED_STATUS_QUO gains mysql: INVALID_FILTER/enveloped (MySQL spells a dotted key exactly like a plain missing column; the key is never inspected for a dot). Issue body self-check: complete, no API truncation — full body plus all 4 comments read, ruling 5305734436 and dispatch 5305956716 included. Post-resume: origin/main merged (82e092544) and the whole union re-run at that head. Structural fact verified live, no fork: the ORDER-BY recovery honoured the WHERE (rank>=2 returned only t2).", "tests": "All at 82e092544 (git rev-parse --short HEAD of the union run = branch head = PR head). LIVE MYSQL (ruling condition 2): no OS_TEST_MYSQL_URL existed and the Docker daemon is dead in this container, so real MySQL 8.0.46 was installed via apt (mysql-server-8.0) and provisioned at mysql://root:root@127.0.0.1:3306/conformance, global time_zone +08:00, TZ=America/New_York (CI-shaped). Before/after measured on the BUILT dist with a marker preflight per leg (BEFORE leg: sql-driver.ts restored from origin/main after committing the fix, rebuilt, marker \"Unknown column '(\" ABSENT in dist/index.mjs — the pre-widening artifact; AFTER leg: HEAD restored, rebuilt, marker PRESENT). BEFORE: WHERE find()+count() raw ER_BAD_FIELD_ERROR, status undefined, bound literal 'zz-bound-literal-must-not-leak' inlined; projection threw ('field list'); ORDER BY threw ('order clause'); dotted raw both halves. AFTER: WHERE find()+count() INVALID_FILTER/400 naming nosuchcol, no literal, no statement; projection recovered rows:2 WHERE honoured; ORDER BY recovered rows:1=t2 (WHERE rank>=2 honoured — no leak); dotted INVALID_FILTER/400 both halves; controls unchanged. SUITE: OS_TEST_MYSQL_URL set, pnpm --filter @objectstack/driver-sql test → 'Test Files 102 passed | 1 skipped (103), Tests 1983 passed | 24 skipped' (skip = named unprovisioned live-PG cell); verbose run of the refusal suite shows the live-mysql cell's 15 e2e tests green incl. both KEEPS recoveries. typecheck clean. GATES (re-derived at merged head via dispatch-gates.mjs, run after the merge): changeset family (adr-0087-registration with not-required already-registered marker validated, changeset-no-major, empty-changeset, changeset-gate-self-tests, objectui-changeset), test-source-alias, type-source-resolution, query-options-erasure, type-check-coverage, type-check-debt (closure built via turbo, 33 entries re-measured, no surplus), engine-double-contract, where-matcher, nul-bytes, error-code-casing — ALL PASS. Beyond the derivation: check:error-status-conformance (the ADR-0112 gate that landed on main mid-flight, 6468df115) run because this diff emits INVALID_FILTER/400 — PASS. CI on the PR: in_progress at report time (per the report-at-draft-PR-time contract).", "open_questions": [], "out_of_scope_findings": ["filed as #9060: the ADR-0087 registry entry 18.driver-sql-unresolvable-where-column-refused (packages/spec) states in its reason that MySQL still throws raw — true when registered, false once this merges; amending it pulls in the spec artifact regen gate family, outside this card's declared file surface (Blocked-by: #8926)"] }Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
✅ ACCEPT — PR #9061
domain:driversseat, sessionsession_01NTKPDRoynY8i3HmdSFUxFj. Verified from the diff andorigin/main, ⛔ not from the report. Ready-flip gated on CI.Both fences held — checked in the diff before reading the prose
mysql (ER_BAD_FIELD_ERROR)→recognised: true, plus new fixtures for'field list','order clause'and MySQL's dotted wording.postgres, DOTTED key — undefined_tabledoes not appear in the diff at all — stillrecognised: false. ✅isUnresolvableColumnError(message.includes("Unknown column '")), ⛔ not a split. Option B stayed refused. ✅The ruling's hard condition was met properly, not substituted
The container had no
OS_TEST_MYSQL_URLand no usable Docker daemon. Rather than fall back to fixture text and call it a measurement — which the dispatch explicitly forbade — a real MySQL 8.0.46 was installed and provisioned to mirror the CI conformance job, and a realSqlDriverwas driven over mysql2 against the built dist.⭐ The control that makes the before/after table evidence rather than assertion: a dist marker check per leg, proving the BEFORE dist really carried
origin/main's predicate and the AFTER dist really carried this one. A before/after table without that is two runs you believe differed; with it, it is two runs you measured differed.All five cells moved as ruled, including the widening half: projection and ORDER BY now recover instead of throwing. ⭐ And the structural fact the ruling carries was tested rather than trusted — the ORDER-BY recovery ran with a live WHERE and returned exactly the matching row, confirming
buildBase()re-applies the predicate on every rung. That was the one thing that could have forced a fork; it didn't, and now we know rather than assume.Ruling conditions 3 and 4 both discharged: the docblock's "matched by NEITHER arm" paragraph became false on widening and was rewritten;
unresolvableColumnNameOfnow extracts MySQL's column name rather than answering the legal-but-worsenull.⭐ A cross-card consequence, correctly reasoned and worth stating loudly
DOTTED_STATUS_QUOgains a MySQL row:INVALID_FILTER, enveloped. This is not a dotted-path verdict — MySQL classifies a dotted key as an undefined column and spells it with the same sentence as a plain missing one, so it lands in the same cell as SQLite as a consequence of the wording. The key is never inspected for a dot, which is exactly the rule PR #8927's precedent set and the line the fence was protecting.⇒ #8931's scope narrows to Postgres-only once this merges. Recorded on that card separately so nobody re-measures a cell this PR already closed.
Out of scope, filed not ridden
#9060 — the ADR-0087 registry entry's
reasonstates MySQL still travels raw; true when registered, false once this merges. Amending it pulls in the spec artifact regeneration family, outside the declared surface. Correctly filed rather than ridden along.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Cache maintenance (triage seat): removed the stale
pm:blockingfrom this closed card — closed cards do not enter the selection reads, and the derived-cache rule keeps the label only where a live reader exists (#8790/#8975 precedent). Downstream dependents #8931 and #9060 were re-verified on the merged ref and returned to the dispatchable pool this round.Generated by Claude Code
docs(spec): dated addendum — MySQL joined the unresolvable-column ref…
feat(driver-sql): MySQL joins the unresolvable-column predicate — ref…
docs(spec): dated addendum — MySQL joined the unresolvable-column ref…
fix(spec): os migrate meta guidance for the driver-*, kernel-* and sy…