Skip to content

drivers(sql): what MySQL does with a conflict target it cannot honour is unmeasured — knex compiles the target away, so the refusal cannot arise there #8592

Description

@os-zhuang

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:583
image: mysql:8.0
# .github/workflows/ci.yml:673
OS_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

  1. 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.
  2. 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.
  3. 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).

Activity

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions