Uh oh!
There was an error while loading. Please reload this page.
fix(types): teach the internal-leak predicate MySQL's three error templates (#8739) - #9030
Merged
Merged
Conversation
…plates (#8739) `looksLikeInternalErrorLeak` covered the SQLite family and Postgres; on a MySQL deployment it returned false for every one of the same conditions — silent, not clearing. Under the maintainer's 2026-08-15 ruling on #8739 MySQL is a supported deployment target, so its driver text reaches these boundaries in production and its templates belong in the list. Adds three limbs, each anchored on MySQL's own errmsg template rather than a bare substring: ER_NO_SUCH_TABLE (1146), ER_BAD_FIELD_ERROR (1054) and ER_DUP_ENTRY (1062) — the last being the only one of the three whose text embeds a caller's value rather than an identifier. MySQL's ACL family is deliberately left uncovered and the reason is recorded: nothing here has raised one off a live server, and `Access denied` collides with this platform's own security prose. The four toBe(false) pins PR #8824 planted as a tripwire went red as designed and are rewritten, not deleted — same three measured messages, opposite verdict — and a second block keeps the original false-means-UNCOVERED shape pointed at MSSQL and Oracle so that distinction keeps a live subject. Two cross-package measurements of this predicate moved with it: rest's #6250 pin (rewritten to assert the invariant it was always for — the 409 is independent of the leak verdict, which is now demonstrated more strongly than before) and metadata-protocol's dialect matrix row and count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
qq9340100
marked this pull request as ready for review
August 16, 2026 07:23
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8739 — ruling item 1 only. Deliberately not a closing keyword: three of the ruling's four consequences live elsewhere (see "What this PR does not do"), so the card should survive this merge.
The ruling
Maintainer, 2026-08-15, comment
5302926756, provenance quoted verbatim and untranslated:Option A — MySQL is a supported deployment target, not merely a tested dialect. The reasoning the dev report and the re-grade reached independently: the docs are the promise already made —
OS_DATABASE_DRIVER=mysqlis a published deployment knob,MysqlConfigis authorable datasource config,types.mdxspecifies per-field MySQL DDL, and MySQL-specific defects are accepted and fixed rather than closed as unsupported.Item 1 of the four consequences: teach
DIALECT_LEAK_PHRASINGSMySQL's phrasings.What changed
looksLikeInternalErrorLeakdecides whether a message is a driver dump that must not reach an API client. It is applied at three HTTP boundaries (rest'smapDataError,runtime's dispatcher-plugin and endpoint-executor, the hono adapter) and gatesobjectql's log redactor. Its dialect list covered the SQLite family and Postgres; on a MySQL deployment it answeredfalseto every one of the same conditions — silent, not clearing.Three limbs added, one per condition the other two dialects were already covered for, each anchored on MySQL's own errmsg template rather than a bare substring:
Table 'app.t' doesn't existUnknown column 'c' in 'field list'Duplicate entry 'x' for key 'i'Anchoring detail, since over-matching is the expensive direction here: the unknown-column pattern requires both quoted parts — the second is MySQL's clause name (
field list,where clause,order clause,on clause) and is what separates the driver's template from a sentence that merely calls a column unknown. The duplicate-entry pattern anchors on thefor keytail plus a quoted index, and matches the value half loosely because it is the caller's own text and MySQL does not escape a quote inside it.Deliberately still NOT recognised, on the record rather than inferred:
Access denied for user 'u'@'h' to database 'd'(1044),SELECT command denied to user … for table 't'(1142) — the counterpart of the Postgrespermission denied for tablelimb. Nothing here has raised one off a live server, and the standing rule in this neighbourhood (unique-violation.ts) is that a dialect's spelling is added once measured off a thrown error, never from a reading of the manual.Access deniedalso collides with this platform's own security prose ([Security] Access denied: …, a pinned negative case), so a guessed pattern would over-match — and over-matching suppresses diagnostics an operator needs.false.duplicate entry in the uploaded file, a mapping message sayingUnknown column in the uploaded CSV header,The table you selected does not exist. All pinned as new negative cases.The four pins went red by design, and are rewritten rather than deleted
PR #8824 planted four
toBe(false)pins as a deliberate tripwire for exactly this decision. They fired. Measured first, before any test was touched — source edited alone, landed pins run against it:Zero collateral inside
packages/types, which is the evidence that the three limbs match the templates and nothing else in that suite.The rewrite keeps them equally capable of going red, which was the point of building them:
true, plus three more (MySQL'swhere clausespelling, a qualifiedtable.columnkey, and a value containing its own quote). A future change that silently drops MySQL coverage fails here, on templates measured off real MySQL text.includes(...), they go red before a deployment starts answering "Internal server error" to real questions.false-means-UNCOVERED lesson is not retired with them. It was never about MySQL: it is the reading a reviewer on PR fix(objectql): refuse undeclared insert fields at the schema, and keep bound values out of the write-path logs (#8682) #8737 got wrong while sizing a disclosure residual, which is what produced this card. A second block keeps the originaltoBe(false)shape pointed at MSSQL and Oracle, so the distinction keeps a live subject instead of becoming a paragraph.Reverse verification, direction predicted in writing first
Both legs were predicted before running, and the prediction file recorded the inverse direction this card requires.
Leg A — the original pins must fail against the change. Predicted 4 red, zero collateral. Observed exactly that (above).
Leg B — the rewritten pins must go red against pre-ruling behaviour. Fix committed first, then
git checkout origin/main -- packages/types/src/error-leak.tsalone:One missed prediction, kept rather than tidied away: leg B was predicted at 3 red and came back 5. The direction was right; the count was stale because two positive cases were added after the prediction was written. Recording it because the useful half is the two that stayed green: the knex-prefixed case is green in both directions (it reaches
truethrough the pre-existinginsert intolimb, independently of the MySQL limbs — which is exactly why it was added), and the MSSQL/Oracle block is green in both directions by design, making it a guard rather than evidence.Restored by
git checkoutof the branch, byte-identity proved rather than assumed: worktree blob4e2d8a49ab83996acddb646e4a75cff6ebfb302fequalsgit rev-parse HEAD:packages/types/src/error-leak.ts.The card's four measurements, re-measured through the shipped functions
Not inherited from the round-8 report. Driven through the real
redactStatementFromMessage, resolving@objectstack/typesfrom its built dist:Byte-identical to the recorded values. Three keep an identifier; one keeps a caller's value, and it is the dialect the false claim excluded. Postgres' own value was confirmed to live on
error.detail—Key (email)=(acme@example.com) already exists.— whichLoggerdoes not serialize, and which the redactor's rebuiltErrordrops as well; so Postgres escapes for a reason unrelated to the cut, as recorded.No status mapping moves, and the log does not move either
Measured, not reasoned:
409 UNIQUE_VIOLATIONand a MySQL unknown-column error is still400 INVALID_FIELD— both decided above the leak branch, byisUniqueViolationErrorand the INVALID_FIELD limb respectively. 唯一约束冲突没有单一判别谓词:仓内四套各自为政的方言词表,REST 的 409 映射漏掉 MySQL(Duplicate entry落成 500 INTERNAL_ERROR) #6250's separation is what makes the widening safe.trueto the gate but carries no knex-separator, solastIndexOfreturns -1 and the message comes back byte-identical. Measured for all seven dialect shapes —unchanged=truefor every one.Consumption-radius sweep, not a package-scoped one
The predicate has consumers in seven packages, so the sweep followed the rule's consumers rather than the edited package. Full suites run:
typesobjectqlrestruntimemetadata-protocolservice-analyticsservice-packagehonoExactly two verdicts moved outside
packages/types, and both are files that measure this predicate directly rather than files that merely use it:rest/src/rest-unique-violation-dialects.test.ts— the#6250block pinned "the fix did not widen the internal-leak classifier". Rewritten to assert the invariant it was always for: the 409 does not depend on the leak verdict. The rewrite is a stronger demonstration than the original — the message now IS classified as a leak and the 409 is returned anyway, which can only be true if the conflict branch runs above and independently of the leak branch.metadata-protocol/src/protocol.driver-text-disclosure.test.ts— theDIALECTSmatrix row and its uncovered-count. That file's own header instructs the next author to flip rather than delete, and that is what was done. Every withhold case in it passed before and after the flip, which is the cleanest available demonstration of its thesis: a producer that withholds by DECLARATION is unaffected when the phrasing list moves underneath it.Two neighbouring comments that state the old verdict as history were given a dated qualifier so the next reader cannot re-derive a false present-tense claim from them — which is the exact defect class this card exists for.
What this PR does not do
Duplicate entry 'acme@example.com' for key …survives the cut, measured through the shipped function #8823 is not addressed here. The value-bearingDuplicate entryresidual in the server log is a separate card in the engine-core lane, and this change provably does not touch it (the log output is byte-identical in both directions, measured above).nobody here runsnarrations PR docs(types): correct error-leak.ts's false MySQL dialect claim, and pin the uncovered dialect #8824 left as a tripwire are deliberately preserved, not cleaned up.Gates
Derived after the final commit against the actual changed paths, and run at that HEAD —
05e85a759.The derivation named more families than the dispatch prompt predicted: it expected
check:nul-bytes,check:query-options-erasure,check:type-check-coverage,check:type-check-debt, and the re-derivation addedcheck:changeset-gate-self-tests,check:cross-package-test-inputs,check:durability-log-level,check:objectui-changeset,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-cross-package-test-inputs, plus convention-triggeredcheck:engine-double-contractandcheck:where-matcher(this PR edits three test files). The union was run, output redirected to files, never piped.14 of 14 EXIT=0.
check:type-check-debt --re-measureran on the built workspace closure — 33 ledger entries re-measured in 294.7s, 1926 raw errors, none above its recorded number — so it is MEASURED, not the unbuilt-worktree refusal. Nothing was NOT MEASURED.Generated by Claude Code