Skip to content

fix(driver-sql): upsert with nothing left to merge is a plain INSERT, not merge-ALL - #26015

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-slwx8p
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-slwx8p

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #8740

Clause-②: no (narrowing)
Metadata-protocol: none

What changed

SqlDriver.upsert merges every payload column that is not insert-only, or, when there is none, the conflict-target columns the payload carries (a no-op). When the payload carried neither, the statement fell back to knex's bare merge(), which compiles to merge-ALL on every dialect (do update set / on duplicate key update over every inserted column, insert-only ones included).

That fallback is removed. With nothing to merge, the statement is a plain INSERT with no conflict clause, so a collision surfaces as the driver's own unique-violation error and the stored row is left as it was. It is not insert ignore / do nothing, which would hide the collision. The reachable no-op merge on the conflict-target columns is unchanged.

  • packages/drivers/driver-sql/src/upsert-conflict-clause.ts (new, not exported from the package): withUpsertConflictClause(insertion, mergeKeys, columnsToMerge) returns the plain insertion when columnsToMerge is empty, else onConflict(mergeKeys).merge(columnsToMerge). Kept free of the driver so its SQL is pinned per dialect with no server.
  • sql-driver.ts: the upsert statement goes through it; the in-statement merge predicate is applied only when the statement merges.
  • .changeset/8740-upsert-nothing-to-merge.md: patch, BREAKING banner (a served-set narrowing), ADR-0087 not-required (no-migration-prescription).

Tests

New src/sql-driver-upsert-empty-merge-set.test.ts:

  • compiled SQL with nothing to merge, via toSQL() with no server, exact strings for better-sqlite3, pg and mysql2: no on conflict, do update set, on duplicate key update or insert ignore; plus the contrast (something to merge keeps the clause and merges only the named column);
  • per dialect cell (SQLite always; PostgreSQL and MySQL where OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL are provisioned): a collision on a conflict-target column the payload omits (non-null default) is refused with a unique violation and rewrites nothing; a primary-key collision whose payload carries only insert-only columns is refused and rewrites nothing; controls: a payload that collides with nothing still inserts and answers the row, and a payload carrying its conflict target still upserts onto the stored row. The refusal is asserted by isUniqueViolationError AND the dialect's native code (SQLITE_CONSTRAINT_*, 23505, ER_DUP_ENTRY).

Red first, on the unfixed driver (commit 19a401b): 4 failed / 12 passed, the non-null-default collision red on all three cells, the primary-key collision red on the MySQL-dialect cell. Green after the fix (commit fc2ee27), with the four neighbouring upsert suites: 5 files, 119 tests passed, all three cells live.

Local cells: SQLite in-process; PostgreSQL 16.15 local; the MySQL cell against MariaDB 10.11.14 (no MySQL 8 server in the container). CI's Temporal Conformance (live PG + MySQL) job runs this package's whole suite, so the new file's MySQL cell runs there on MySQL 8.

Gates

  • pnpm --filter @objectstack/driver-sql test: 236 files passed, 12 skipped; 3795 tests passed (SQLite cells; live cells via CI). typecheck: exit 0.
  • With local live URLs the package run shows 49 failures in 24 files, all live PG/MariaDB cells and none in an upsert file; the same 24 files on the base commit 05f7758f05 show the same 49 failures (local MariaDB / PG environment, not this diff).
  • eslint --no-inline-config on the three touched source files: exit 0.
  • dispatch-gates --commands: 71 derived, 70 run, every exit 0; 1 NOT MEASURED: check:dual-build-cjs-loads (exit 3, needs the whole tree built; CI runs it). --ran reconciliation: 71 accounted, 0 unrun.

Acceptance notes

  • An unbacked conflict target is still refused VALIDATION_ERROR / 400 on the nothing-to-merge path on every dialect, checked against the table keys before the plain INSERT (pinned per dialect cell).
  • isUniqueViolationError reads a non-unique error as a unique violation when the error message echoes an on duplicate key update statement (driver-level repro: a MySQL ER_TRUNCATED_WRONG_VALUE whose message carries the statement answers true; the same code without the statement text answers false). Not fixed here; the new tests pin the native code alongside it for that reason. No public door measured.
  • driver-turso's remote transport is a separate statement builder (DO NOTHING on an empty merge set, SQLite dialect only) and is untouched; its local face inherits this fix through super.upsert.
  • Full dogfood battery (owed by the (narrowing) arm) is run on the final head and reported on drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 6 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/queries.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/permissions/tenant-audit-census.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/types.mdx (via SqlDriver (symbol, a top-level class))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-6.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/releases/v18/18-1.mdx (via SqlDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 39a78b151d691a538856c0f0d57304f8faa69f96 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ef0dc19bb6fe8b137ace72021d0678dff7ef30d — the merge of head b4fc36f35e265f6c25471504842838d88f58fe2a into base 39a78b151d691a538856c0f0d57304f8faa69f96, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1ef0dc19bb6fe8b137ace72021d0678dff7ef30d && git checkout 1ef0dc19bb6fe8b137ace72021d0678dff7ef30d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 39a78b151d691a538856c0f0d57304f8faa69f96 b4fc36f35e265f6c25471504842838d88f58fe2a && git checkout -B drift-repro 39a78b151d691a538856c0f0d57304f8faa69f96 && git merge --no-ff b4fc36f35e265f6c25471504842838d88f58fe2a

node scripts/docs-audit/affected-docs.mjs --json 39a78b151d691a538856c0f0d57304f8faa69f96

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 39a78b151d691a538856c0f0d57304f8faa69f96 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…t to merge (red on head)

Claude-Session: https://claude.ai/code/session_01T6USnvRkjp4BPytcNttAuF
Co-authored-by: Claude <noreply@anthropic.com>
…RT path, on every dialect

Claude-Session: https://claude.ai/code/session_01T6USnvRkjp4BPytcNttAuF
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b4fc36f35e265f6c25471504842838d88f58fe2a
Local-runs: none
Scope: packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/upsert-conflict-clause.ts, packages/drivers/driver-sql/src/sql-driver-upsert-empty-merge-set.test.ts + .changeset/8740-upsert-nothing-to-merge.md (prose face, read for ②), packages/drivers/driver-sql/src/schema-drift.ts (PhysicalIndex, introspectIndexes: partial and expression key parts), packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts (cell declaration, skip-vs-fail rule), packages/types/src/unique-violation.ts (isUniqueViolationError), packages/drivers/driver-turso/src/remote-transport.ts and packages/drivers/driver-turso/src/turso-driver.ts (the PR body's claim about the remote twin), .github/workflows/ci.yml (the Temporal Conformance (live PG + MySQL) job's if:), docs/adr/0112-error-code-vocabulary-and-ledger.md

Inputs: card #8740 (body and every comment, the seat's ruling B in the round-2 dev report included), PR #26015 body, file list and the net diff origin/main...b4fc36f3 (4 files, +389/-26), and the 54 check-runs on the head. Disclosure rule of the card observed: this record describes the change in abstract terms only.

① Derived judgments

Read of the diff. withUpsertConflictClause (new, not exported from index.ts) returns the bare insertion when columnsToMerge is empty and onConflict(mergeKeys).merge(columnsToMerge) otherwise; the bare merge() fallback is gone from upsert. On the empty path assertConflictTargetBackedWithoutClause runs before the statement on every dialect, reading the same cached introspectKeyIndexes the MySQL pre-flight reads, with the pre-flight's certainty rule (failed read or no keys at all: proceed; a refusal is preceded by a fresh re-read) and the pre-flight's cover predicate, both now shared helpers (indexCoversConflictTarget, unbackedConflictTargetCause). The in-statement scope predicate is applied only when the statement has a merge leg.

  1. Collision on the empty-merge path, backed target: merge-every-column becomes a unique violation and no column is rewritten. RIGHT, and the card's ruled shape. This is the narrowing the changeset declares. The MySQL after-statement fence and the drivers(sql): on MySQL an upsert with no conflictKeys — or one naming the primary key — still merges on a unique key the caller never named #8807 identity check still run on that path and are satisfied by the inserted row (read back by id); a collision throws before either, inside the same unit of work. The read-back of a non-colliding call is unchanged (target absent from the payload, so it reads by id, as before).

  2. Unbacked target on the empty-merge path, table carrying at least one PRIMARY KEY or UNIQUE index: refused VALIDATION_ERROR / 400 on all three dialects, nothing written. RIGHT (seat ruling B). On SQLite and PostgreSQL the refusal's cause is now the introspection sentence (schema identifiers only) where it used to be the server's own sentence; message, code and status are the shared wording. Judged a diagnostic-only change.

  3. Unbacked target on the empty-merge path, table with NO key at all, or an introspection read that fails: the plain INSERT proceeds. This is a WIDENING on SQLite and PostgreSQL that the diff implies and no text names: with a conflict clause the server refused the call (no arbiter index) and the driver answered VALIDATION_ERROR / 400; now the uncertain-answer legs of the pre-flight's certainty rule let the statement run, and on a keyless table it inserts. On MySQL the same call already proceeded (the pre-flight has always let it through, and ON DUPLICATE KEY UPDATE on a keyless table inserts), so MySQL is unchanged. Judged ACCEPTABLE as behaviour: the rule exists because an empty catalog answer is indistinguishable from a mistyped table name, and nothing can collide on a keyless table so nothing is overwritten. Judged WRONG as public text: the changeset sentence "A conflict target that no PRIMARY KEY or UNIQUE index backs is still refused on that path" is unqualified, and the PR body's Acceptance note repeats it. See ③ for the action.

  4. A target the caller did NOT name is not exempt from the no-clause refusal. assertConflictTargetHonoured returns without refusing when callerNamed is false (drivers(sql): on MySQL an upsert with no conflictKeys — or one naming the primary key — still merges on a unique key the caller never named #8807: the driver's own ['id'] default is never refused); assertConflictTargetBackedWithoutClause takes no such argument. The default target reaches the empty path only when id is absent from formatted, which applyWriteColumnMap can do for an external object whose columnMap remaps id. On that shape (default target, payload of insert-only columns only, remapped id) the pre-fix statement already failed on SQLite and PostgreSQL (the conflict clause named a column the table does not have) and merged every column on MySQL; now all three refuse before the statement, with no collision needed. Judged a divergence from the drivers(sql): on MySQL an upsert with no conflictKeys — or one naming the primary key — still merges on a unique key the caller never named #8807 rule on a degenerate shape, not a defect in the ruling: naming it here so the seat decides whether to thread callerNamed through (passing it would skip the check and let the plain INSERT answer, which loses nothing). See ③.

  5. A scope-fenced collision on the empty-merge path. Before and after, the call is refused and writes nothing; the error envelope moves from the driver's fence refusal to the dialect's own unique-violation error, the same error a create() collision already raises on these tables. Judged RIGHT, no new surface.

  6. Everything with a column to merge, or with the target in the payload, is byte-identical. RIGHT: the compile pins in the new test show the clause and the exact merged column on all three clients, and noopMergeColumns is untouched (drivers(sql): an upsert that merges on a non-PK conflict key silently REWRITES the existing row's primary key — measured on SQLite and MySQL alike #8622's reachable no-op stays).

  7. Seat question (b), the shared cover helper. Exact set equality over the index's columns, order-insensitive, which is how both ON CONFLICT dialects infer an arbiter and the only reading a composite key admits: RIGHT. Absent cache: a read is made and cached; failed read (null) and empty key set: proceed; a refusal is preceded by a fresh read: all RIGHT and identical to the pre-flight (the helper is the pre-flight's own covers, now hoisted). Partial indexes are filtered out of the key set by introspectKeyIndexes (partial !== true), so a target backed only by a partial unique index reads as unbacked, which is what the server with a clause answered too: RIGHT. Expression key parts: an attributable part lands in columns, an unattributable one is omitted from columns, so an index over (col, expr) can read as covering (col); the error direction is "proceed", the safe one under the certainty rule, and it is the pre-flight's existing reading: RIGHT. Case and quoting: identifiers are compared verbatim against the target as emitted (knex quotes the conflict target; the write column map is applied to the row, never to the target), the pre-flight's documented rule: RIGHT. Scope column: matched like any column when a target names it, and the scope fencing on the no-merge statement was read against each dialect: RIGHT (detail kept off this public record under the card's disclosure rule).

  8. Seat question (c), public text. PR body, changeset, test file header and titles, code comments, the shared cause text and the new helper's doc read in full. None names a reproduction or a path to one; the vocabulary ("insert-only columns", "merge-ALL", "a collision is refused and rewrites nothing") is the card body's own, public since 2026-08-14, and the 17.6.0 changelog already names the insert-only set. The round-2 dev report on the card says only that step 1 was measured and held. NEUTRAL. The only text defect is accuracy, not disclosure (item 3).

  9. ADR-0112 assertion shape. The unbacked-target cell asserts code === 'VALIDATION_ERROR' and status === 400, both, on every dialect cell. The two unique-violation cells assert isUniqueViolationError plus the dialect's native code; a server error carries no declared status (ADR-0112's status-channel note: the door derives it), so there is no status to assert there. RIGHT. The cells also assert the stored rows and created_at unchanged, which is the pin the card asked for.

  10. The PR body's claim about driver-turso. Its remote transport emits DO NOTHING on an empty merge set (remote-transport.ts, its own statement builder) and its local face returns super.upsert (turso-driver.ts), so the twin is untouched and inherits this change on the local face only. RIGHT.

  11. Test placement. sql-driver-upsert-empty-merge-set.test.ts sits in src/ and imports the driver as ../src/index.js, the style 117 sibling tests in that directory use. The live cells are declared through declareDialectCell, which names a missing URL as a skip and makes it fatal under OS_EXPECT_LIVE_DIALECT_MATRIX=1. RIGHT.

Check-runs on the head, as they stood when read: 54 runs; Lint & Repo Gates, TypeScript Type Check, Test Core (8 shards), Build Core, Dogfood Regression Gate, Governed Surface Queue Guard, Check Changeset, Check PR Size, the three single-writer and card-claim guards, Flag docs affected by code changes, Check Documentation Links: all success. Temporal Conformance (live PG + MySQL) skipped, by its own if: (github.event_name != 'pull_request'): the live PostgreSQL and MySQL cells of the new file have not run in CI on this head. UI and docs lanes skipped (no path). No failure.

② Semver level

@objectstack/driver-sql patch with a BREAKING banner, declared a served-set narrowing. Matches what the diff publishes: no exported symbol, type or schema changes (upsert-conflict-clause.ts is not exported; index.ts is untouched), no metadata key moves (ADR-0087 not-required (no-migration-prescription) is right), and the only accept-set move the diff intends is a collision that used to merge now being refused. Clause-②: no (narrowing) on the changeset and on the PR body, consistent with each other and with the round-2 dev report; the dispatch claim's expected yes was superseded by measurement, as the report says. Item 3 above is the one place the changeset's prose is wider than the diff: the unbacked-target sentence needs the qualifier "on a table that carries at least one PRIMARY KEY or UNIQUE index; a table with none, or one whose keys cannot be read, proceeds to the plain INSERT, as the MySQL pre-flight always has". The level itself is right.

③ Boundary flags

  • open_questions[0] (unbacked target on the empty-merge path, options A/B): seat ruled B; implemented on this head as described; pinned per dialect cell with code and status. ANSWERED. Residue: items 3 and 4 above, which B's wording ("the cached physical key indexes", "the existing refusal") did not reach.
  • Dev flag, round-2 report: the MySQL cell ran locally against MariaDB 10.11.14, not MySQL 8; the PR body says the MySQL 8 pin runs in Temporal Conformance (live PG + MySQL). That job is push-only by design and is skipped on this head, so the live cells are unmeasured in CI until the merge lands on main. ESCALATED to the seat: read that job's run on the merge commit before closing the card; if it is red on the new file, the card reopens.
  • Dev flag, out-of-scope finding 1 (dogfood authz-probe-blind-spot.test.ts red on main after fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) #25956): not this diff; packages/runtime untouched. ANSWERED as main-red, the seat's to route.
  • Dev flag, out-of-scope finding 2 (isUniqueViolationError matches a non-unique MySQL error whose message echoes a statement): not this diff; the new tests pin the native code beside the predicate for that reason. ANSWERED as out of scope; worth its own card.
  • Docs drift advisory on the PR: 9 hand-written pages name SqlDriver as a symbol; none documents the upsert merge set or the conflict clause (read: data-modeling/drivers.mdx names the class only). No doc edit owed by this PR.
  • Seat action 1 (from item 3): the changeset and the PR body's first Acceptance note overstate the refusal. Either amend the changeset sentence with the qualifier in ② on a follow-up commit (a new head, which needs a fresh record) or record the qualification on the card when closing it. The seat decides; this record's PASS covers the code and the level, not that sentence.
  • Seat action 2 (from item 4): decide whether assertConflictTargetBackedWithoutClause should take the pre-flight's callerNamed exemption. Not blocking: the only reachable shape is an external object whose columnMap remaps id, where the pre-fix call already failed on two dialects.
  • Scope breach: none. packages/spec/** untouched; file surface packages/drivers/driver-sql/** plus one changeset, as claimed.

Implemented-by: claude/project-thread-slwx8p
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T10:50Z

Head b4fc36f35e, report 6413879118 (on #8740, round 2 under the seat's ruling B), claim 6411358920, contract review 6414139329 (PASS, narrowing face, by server seat 2).

item reading
Shape draft, base main, Fixes #8740, Clause-②: no (narrowing) (body relay-patched to match the changeset), Metadata-protocol: none; four paths under packages/drivers/driver-sql/** plus one changeset.
Fix an upsert with nothing left to merge no longer falls back to merging every column; it is a plain INSERT, so a collision raises the driver's unique violation. Ruling B: before that INSERT, the conflict target is checked against the table's cached physical key indexes on every dialect (the MySQL pre-flight's cover rule, hoisted into one shared helper), and an unbacked target is refused VALIDATION_ERROR / 400 on all three.
Review qualifier, recorded here on a table with no primary or unique key at all, or when the key read fails, SQLite and PostgreSQL now run the plain INSERT where they used to refuse (MySQL always did). Nothing can collide there, so the seat accepts it; the PR body's "still refused" sentence carries this qualifier by this record, and #8740's landing repeats it.
Evidence red first (the SQLite and PG cells inserted at the round-1 head), 122/122 upsert cells on all three dialects, package 3796 passed, typecheck 0, 71/71 derived gates. The live PG / MySQL 8 cells run push-only: Temporal Conformance is read on the merge commit before #8740 is reported closed.
Noted the clause-less backing check lacks #8807's caller-named exemption; reachable only through a column map that remaps id. Not this card's.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay.

Generated by Claude Code

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