Filed out of #8567, which measured Postgres and generalised the unbacked-conflict-target refusal to it. Unassigned. Observation-class and deliberately unmeasured — the first deliverable of this card is the measurement, not a fix.
What #8567 established, without a server
SqlDriver.upsert passes conflictKeys to knex's onConflict(...).merge(...). On the mysql2 dialect knex compiles that to ON DUPLICATE KEY UPDATE, which takes no conflict target:
mysql2 -> insert into `plain` (`email`, `id`, `title`) values (?, ?, ?)
on duplicate key update `title` = values(`title`) <- named keys are gone
pg -> insert into "plain" (...) values ($1, $2, $3)
on conflict ("email") do update set "title" = excluded."title"
The caller's keys never leave the process, so the server is never asked to find an index for them. The unbacked-conflict-target condition cannot arise on MySQL, and isUnbackedConflictTargetError correctly carries no MySQL limb.
The open question this leaves
If the target is compiled away, MySQL does not refuse — it merges on whichever unique key the row happens to collide with. A caller who wrote conflictKeys: ['email'] on a table whose only unique index is on tax_id gets a silent merge against tax_id on MySQL, where SQLite and Postgres both refuse. That is a worse failure than the one #8445 and #8567 fixed: those were legible-versus-illegible errors, this is a wrong write with no error at all.
⚠️The paragraph above is an inference from the compiled SQL, not an observation. Nobody has watched a MySQL server do it. #8445's dispatch ruled that describing dialect behaviour from memory or inference is not acceptable evidence, and #8567's dev correctly declined to file this card on that basis — the container has no mysqld/mariadbd and its docker daemon is unreachable.
The measurement channel exists — in CI, not on a dev box
That premise is resolvable. CI already provisions a live MySQL for the conformance job:
# .github/workflows/ci.yml:583image: mysql:8.0# .github/workflows/ci.yml:673OS_TEST_MYSQL_URL: mysql://root:root@127.0.0.1:3306/conformance
and packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts already reads OS_TEST_MYSQL_URL, reporting an un-run cell rather than passing vacuously. sql-driver-upsert-conflict-target-dialects.test.ts (landing with #8567) currently declares its MySQL cell un-run for exactly this reason — the file runs under Test Core, which has no such service.
What this card is, in order
- Measure. On a runner that has the MySQL service: create an object whose unique index is on a column other than the one named in
conflictKeys, upsert with the mismatched key, and record what actually happens — silent merge on the other key, an error, or something else. ⛔ Do not write the answer into a test before observing it. - Then decide, and only then. Options range from leaving it (documented, dialect-honest) through refusing pre-flight in
driver-sql when the dialect cannot honour a named target — the latter changes the accept set on MySQL, so it is a minor and wants its own argument, not a rider. - Either way the MySQL cell should stop being structurally un-runnable: either wire this file's matrix into a job that has the servers, or state why it belongs where it is.
Related: #8567 (where the compiled SQL was read), #8445 / #8413 (the two faces that refuse this condition), #8590 (the other finding out of #8567), #5240 (one condition, one wording).
Filed out of #8567, which measured Postgres and generalised the unbacked-conflict-target refusal to it. Unassigned. Observation-class and deliberately unmeasured — the first deliverable of this card is the measurement, not a fix.
What #8567 established, without a server
SqlDriver.upsertpassesconflictKeysto knex'sonConflict(...).merge(...). On themysql2dialect knex compiles that toON DUPLICATE KEY UPDATE, which takes no conflict target:The caller's keys never leave the process, so the server is never asked to find an index for them. The unbacked-conflict-target condition cannot arise on MySQL, and
isUnbackedConflictTargetErrorcorrectly carries no MySQL limb.The open question this leaves
If the target is compiled away, MySQL does not refuse — it merges on whichever unique key the row happens to collide with. A caller who wrote
conflictKeys: ['email']on a table whose only unique index is ontax_idgets a silent merge againsttax_idon MySQL, where SQLite and Postgres both refuse. That is a worse failure than the one #8445 and #8567 fixed: those were legible-versus-illegible errors, this is a wrong write with no error at all.mysqld/mariadbdand its docker daemon is unreachable.The measurement channel exists — in CI, not on a dev box
That premise is resolvable. CI already provisions a live MySQL for the conformance job:
and
packages/drivers/driver-sql/src/live-dialect-matrix.testkit.tsalready readsOS_TEST_MYSQL_URL, reporting an un-run cell rather than passing vacuously.sql-driver-upsert-conflict-target-dialects.test.ts(landing with #8567) currently declares its MySQL cell un-run for exactly this reason — the file runs under Test Core, which has no such service.What this card is, in order
conflictKeys, upsert with the mismatched key, and record what actually happens — silent merge on the other key, an error, or something else. ⛔ Do not write the answer into a test before observing it.driver-sqlwhen the dialect cannot honour a named target — the latter changes the accept set on MySQL, so it is aminorand wants its own argument, not a rider.Related: #8567 (where the compiled SQL was read), #8445 / #8413 (the two faces that refuse this condition), #8590 (the other finding out of #8567), #5240 (one condition, one wording).