Skip to content

Add MySQL backend - #872

Draft
phdoerfler wants to merge 1 commit into
typelevel:mainfrom
phdoerfler:topic/mysql-backend
Draft

Add MySQL backend#872
phdoerfler wants to merge 1 commit into
typelevel:mainfrom
phdoerfler:topic/mysql-backend

Conversation

@phdoerfler

@phdoerfler phdoerfler commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This adds MySQL as a backend, same procedure as the SQLite (#864) and H2 (#866) PRs.

The mysql-connector-j driver is GPLv2 with the Universal FOSS Exception. It is added as a plain compile dependency (similar to the Oracle backend).

The first supported version of MySQL is 8.0.14+ (the first version with support for LATERAL joins). The CI tests against a newer version but the old one has been tested separately, too. In particular: the tests pass for 8.0.14, 8.0.46, and 8.4 (current LTS), via a MYSQL_VERSION override on the docker-compose service. It is reproducible with MYSQL_VERSION=8.0.14 sbt doobiemysql/test.

Dialect notes

  • LATERAL: native (8.0.14+), rendered the same way Oracle's dialect does. This is what sets the minimum version above.
  • NULL ordering: MySQL has no NULLS FIRST/LAST; its default is nulls-low (NULLs sort first under ASC, last under DESC). Emulated with a CASE WHEN col IS NULL THEN 1 ELSE 0 END sort-key prefix, added only where the requested placement differs from the default. Covered by the shared SqlNullOrderingSuite, which needed a mysql.sql for the null-ordering dataset and is wired here now. It orders and limits so the SQL-level placement is observable: grackle re-sorts fetched rows in memory, so a whole-response comparison cannot catch a dialect ordering bug, only a LIMIT cut can.
  • LIMIT/OFFSET: OFFSET is illegal without LIMIT, same as SQLite, so it reuses SQLite's comma-form rendering (LIMIT offset, limit) and the same normalizeOffsetLimit mechanism. MySQL has no LIMIT -1 idiom, so an offset-without-limit query gets Int.MaxValue as a practical "unbounded" sentinel instead.
  • LIKE: inverted from Postgres. MySQL's default *_ci collations make plain LIKE case-insensitive already, so it's the case-sensitive branch that needs an explicit COLLATE (now an overridable binaryCollation member, default utf8mb4_bin, for mappings over a non-default charset/collation).
  • DISTINCT ON: not supported. plain DISTINCT + the FirstValueColumn window-function strategy already used by the MSSQL backend.
  • CAST vocabulary: MySQL's CAST only accepts a small fixed set of target names, not arbitrary column types — mapped explicitly; anything without a safe CAST target (e.g. FLOAT/DOUBLE, since CAST(... AS DOUBLE) needs 8.0.17, above this backend's 8.0.14 floor) renders a bare NULL instead, which is safe since an ascribed NULL is only ever an inference hint.
  • Union branches: parenthesized branches with inline ORDER BY/LIMIT/OFFSET are legal, so no derived-table encapsulation is needed (unlike SQLite, which forbids this entirely).
  • Mutation: no INSERT ... RETURNING, no sequences. AUTO_INCREMENT + JDBC generated-keys (withUniqueGeneratedKeys), same pattern as the Oracle backend.

Schema-qualified tables

The qualified-names and union-order datasets put their tables in a schema. In MySQL a schema is a database, so CREATE SCHEMA qualified works, but the image grants the test user rights on its own database only and every query against the new one comes back SELECT command denied. The dataset's mysql.sql therefore grants them.

Test infrastructure

The official mysql docker image's docker-entrypoint-initdb.d fixture loader negotiates a latin1 client handshake by default, which double-encodes the UTF-8 fixture literals (accented city/country names). --skip-character-set-client-handshake was removed in MySQL 8.3+, and a bind-mounted my.cnf isn't reliable in every dev environment (some setups force bind-mounted files world-writable, which mysql refuses to read). Worked around with a small entrypoint.sh that writes a [client] default-character-set=utf8mb4 config file fresh inside the container before handing off to the stock entrypoint.

Disclaimer

This work was made in conjunction with an LLM.

@phdoerfler phdoerfler mentioned this pull request Jul 14, 2026
@phdoerfler
phdoerfler marked this pull request as ready for review July 14, 2026 19:57
@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch 2 times, most recently from 9942895 to 69012fd Compare July 17, 2026 20:49
@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch 4 times, most recently from c183e02 to 051fa24 Compare August 20, 2026 10:44
@milessabin

Copy link
Copy Markdown
Member

This will need rebasing.

@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch from 051fa24 to 616bf17 Compare August 30, 2026 00:34
@phdoerfler
phdoerfler marked this pull request as draft August 30, 2026 07:50
@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch 4 times, most recently from 19645fc to 93f95c5 Compare August 30, 2026 22:09
@milessabin

Copy link
Copy Markdown
Member

Needs to be set "Ready for review" (assuming that it is 😄 ).

@milessabin

Copy link
Copy Markdown
Member

Actually, I could do with some clarification on the "Known follow-up" section of the description ... is this still accurate?

@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch from 93f95c5 to 66b96d9 Compare August 31, 2026 09:32
@phdoerfler
phdoerfler force-pushed the topic/mysql-backend branch from 66b96d9 to 6ea775b Compare August 31, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants