Skip to content

Support MariaDB 10.11+ with a runtime-detected target engine - #3

Merged
damarbob merged 8 commits into
mainfrom
mariadb-support
Sep 21, 2026
Merged

damarbob merged 8 commits into
mainfrom
mariadb-support

Conversation

@damarbob

@damarbob damarbob commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

StarDust now runs on MariaDB 10.11+ alongside MySQL 8.0.13+ and Percona. The target engine is detected from the live connection rather than configured, so the same code, the same schema and the same smoke suite run unchanged on either, and anything below either floor is refused at boot.

  • Support\ServerEngine and Support\ServerEngineDetector resolve the engine and version from the connection itself and fail closed with UnsupportedServerException below MySQL 8.0.13 or MariaDB 10.11 (ADR 0055). StarDust::serverEngine() is the public accessor.
  • Support\Dialect branches the two constructs that actually diverge: the table collation, and ADR 0017's live-slot invariant. MariaDB has no functional-index syntax at all — it rejects the MySQL form with errno 1064 — so it gets a PERSISTENT generated column carrying the identical CASE expression plus a plain UNIQUE KEY. Nothing downstream branches on which shape is live.
  • Testing against real containers rather than inferring from version markers found a genuine bug: MariaDB 11 raises errno 1020 on the import checkpoint UPDATE where MySQL matches zero rows, because the manifest JSON column's implicit CHECK collides with a concurrent-writer race. ImportJobWorkSource now treats errno 1020 identically to the existing rowCount() === 0 lease-lost path.
  • CI gains mariadb-smoke across the full PHP matrix against MariaDB 10.11 and 11 (must pass), and mariadb-rejection moves from 11 to 10.6 so its "must fail" assertion stays meaningful — the suite now passes on 11, which made the old target vacuous.
  • Documentation and the Composer description updated. The package previously described itself as MySQL-native and told consumers MariaDB was actively rejected, which stopped being true the moment detection landed.

One accepted behavioural divergence

On MariaDB, range filters and field sorts order supplementary-plane characters — in practice, emoji — at the opposite end from MySQL. Ordinary text in any language, and every other comparison, behave identically on both engines. This is accepted rather than engineered around (ADR 0054 §3, and ADR 0041's Consequences), and is stated in README.md, docs/reading-entries.md and TESTING.md.

Test plan

  • Full 937-test smoke suite green against a real MySQL 8.0, a real MariaDB 10.11 and a real MariaDB 11
  • Suite refuses MariaDB 10.6: 671 errors, every one an UnsupportedServerException raised by ServerEngineDetector, so the rejection is the floor guard rather than incidental breakage
  • PHPStan level 8 over src/ and bin/, zero errors, no baseline
  • markdownlint clean across all 42 tracked markdown files
  • Every load-bearing fix neutered and confirmed red before being trusted
  • CI green across all 15 jobs, including PHP 8.1, 8.2 and 8.3, which have not been exercised locally

Detects the target engine from the live connection rather than trusting a configured one, per the maintainer-confirmed ADR 0055 decision: a wrong MySQL declaration against MariaDB fails loudly, but a wrong MariaDB declaration against MySQL would have silently produced a working but permanently divergent registry shape.

- Adds `Support\ServerEngine` (closed enum, MySQL/MariaDB) and `Support\ServerEngineDetector::detect()`, which matches the `MariaDB` marker in the version string before any numeric parsing, reads `PDO::ATTR_SERVER_VERSION` with a `SELECT VERSION()` fallback, and fails closed below either engine's floor (MySQL/Percona 8.0.13, MariaDB 10.11) via the new `Exception\UnsupportedServerException`.
- Branches both `Support\Dialect` methods on `ServerEngine`: the table collation clause, and the ADR 0017 live-slot uniqueness DDL.
- Adds `Bootstrapper::ensureSlotAssignmentLiveFieldIdColumn()`, a MariaDB-only `PERSISTENT` generated column that the branched unique index enforces the live-slot invariant over, since MariaDB has no functional-index syntax.
- `StarDust::serverEngine()` memoises the detected engine and threads it into `bootstrap()` and `watcher()`'s `PageProvisioner` construction; both `Bootstrapper` and `PageProvisioner` now require `ServerEngine` in their constructors.
- Updates every test fixture, the README example, and the Docker seed script for the new required constructor parameter, and extends `DialectTest` to cover both branches.

Verified against real MySQL 8.0.13 (full 925-test smoke suite, unmodified pass) and real MariaDB 10.6/10.11/11 containers (the floor rejection, the generated-column substitute's DDL and its live-slot enforcement, and 17 of 19 `BootstrapTest` cases — the two failures are MySQL-only assertions this change does not address, not defects introduced by it).
The previous commit deliberately deferred this fixture rather than half-track it: LegacyPage.php hand-freezes the pre-ADR-0043 sixty-column page shape independently of PageProvisioner, and it still had utf8mb4_0900_ai_ci hardcoded with no branch, so any test constructing one against a real MariaDB server failed on the first CREATE TABLE with errno 1273.

- provision() and the private ddl() now take a ServerEngine parameter.
- The collation is a local match(), not a call to Dialect::tableOptionsClause() — deliberately, so a future change to Dialect's signature can't break a fixture whose entire job is to hold still.
- Updates the four call sites: WritePathTestCase, SlotReserverTest, Slot/SlotAffinityTest, EmptyTableGuardTest.

Verified against a real MariaDB 10.11 container: the 38 tests across the three classes that exercise LegacyPage pass there, confirmed to fail with errno 1273 before this change. Re-ran the full 925-test suite against a separate, fresh MariaDB 10.11 container afterward — 76 failures remain, all pre-existing and unrelated (mostly RetypeInitiator.php's FOR UPDATE OF f syntax, which MariaDB rejects), and none trace into any file this commit touches. Re-ran the full suite against MySQL 8.0.13 too, with no regression.
Running the full smoke suite against a real MariaDB 10.11 server surfaced two genuine divergences from MySQL, neither anticipated by the earlier collation- and DDL-focused probing.

- RetypeInitiator::loadField() used FOR UPDATE OF f to lock only the field row in a join with stardust_models, avoiding contention with deleteModel(). MariaDB has no OF clause at all (errno 1064).
- Rewritten as two single-table statements: a plain FOR UPDATE on stardust_fields, then an unlocked tenant_id lookup on stardust_models. Safe because model_id is immutable once set and fk_fields_model guarantees the model row exists.
- This needed no engine branching at all — the new shape is portable and identical on both engines. Updated the one test fixture that mirrored the old SQL verbatim, plus docblocks in RetypeCheckpointRepository, RenameCheckpointRepository, and both Retype/CLAUDE.md and Rename/CLAUDE.md.
- MysqlNativeDriver relied on MySQL 8.0.13's undocumented exemption from max_sort_length truncation on TEXT column sorts (the premise src/Read/CLAUDE.md's 'string slots sort exactly, on the full value' claim rests on). MariaDB does not share it: reproduced directly in raw SQL against real 10.6/10.11/11 servers, collation-independent, rows sharing a long common prefix come back in scan order rather than sort order.
- Because the keyset pagination predicate compares the full value regardless, this could silently skip or duplicate rows across a page boundary on a MariaDB deployment.
- Fixed by raising max_sort_length to the full string-slot bound when the target is MariaDB, left untouched on MySQL since its correctness never depended on the setting. ServerEngine defaults to self-detection on MysqlNativeDriver's constructor rather than a required parameter, since the legacy EntryReader façade's constructor is deliberately frozen at (PDO, LoggerInterface) and cannot thread one through.
- Amended ADR 0041's Consequences section with a dated correction.

Verified against real MySQL 8.0.13 and MariaDB 10.11: the full 927-test suite passes on MySQL unmodified, and MariaDB's failure count drops from 76 to 4 — all four of which are already-known, already-scoped items on this stage's own checklist (the deliberate rejection gate and three MySQL-only test assertions), not new problems.
The full 937-test suite now passes with zero failures against both real MySQL 8.0.13 and real MariaDB 10.11.

- EnvironmentTest::testServerIsMySql renamed to testServerIsASupportedEngine, delegating to ServerEngineDetector::detect() (now called in setUp(), so every test in the class inherits the same fail-closed behaviour an unsupported server would trigger) rather than unconditionally rejecting any MariaDB-marked version string.
- New tests/Smoke/Support/ServerEngineDetectorTest.php formalises the detector's boundary coverage — floor edges for both engines via reflection on the private parsing methods, the 5.5.5- legacy-prefix strip, and a live-connection happy path — none of which had permanent test coverage before; it previously existed only as a discarded scratch script.
- testPartialUniqueIndexSupported now branches its standalone smoke-table DDL per engine (MySQL's literal functional index, MariaDB's generated-column substitute inline) and asserts identical observable behaviour on both — a second tombstoned row allowed, a second live one refused — rather than asserting MySQL's specific DDL syntax parses.
- BootstrapTest::testPartialUniqueIndexOnSlotAssignmentsIsPresent now branches on SHOW INDEX's shape: MariaDB indexes the live_field_id generated column directly, so the CASE expression text is checked via information_schema.COLUMNS.GENERATION_EXPRESSION instead of SHOW INDEX's Expression column.
- BootstrapTest::testBootstrapAddsModelsDeletedAtColumn now accounts for MariaDB reporting COLUMN_DEFAULT as the literal string 'NULL' for an explicit DEFAULT NULL column, where MySQL reports SQL NULL — measured directly against a real MariaDB 10.11 server before being encoded as an assertion.
- CLAUDE.md's 'Database support' section corrected — it previously said MariaDB was unsupported and actively rejected, which became false the moment these tests changed. Also flags a gap this surfaced but does not fix: CI's mariadb-rejection job still targets mariadb:11 expecting the suite to fail, and likely no longer does, since MariaDB 11 shares the same detection path as 10.11. Retargeting it at a genuinely below-floor version is separate follow-up work.

Every changed or added assertion was neutered and confirmed red before being trusted: reverting the fix under test reproduces the exact original failure in each case.
… bug

- New mariadb-smoke job runs the full PHP matrix against both MariaDB 10.11 and 11; mariadb-rejection now targets mariadb:10.6 (genuinely below the ADR 0054 floor) instead of mariadb:11, which the suite now passes.
- Verifying against real containers (not just the version-marker inference) found MariaDB 11 throws errno 1020 on the import checkpoint UPDATE instead of matching zero rows, because the manifest JSON column's implicit CHECK constraint collides with a concurrent-writer race. ImportJobWorkSource now treats errno 1020 identically to the existing rowCount()===0 lease-lost path.
- CLAUDE.md's CI-jobs count and Database support gap note, plus a dated addendum on ADR 0040 and src/Reconciler/CLAUDE.md, updated to match.
- README.md: rewrite the Requirements/Database section, the "good fit"/"not a fit" callouts, and the CI paragraph to state the real floor (MySQL 8.0.13+ or MariaDB 10.11+; MariaDB 10.6 and MySQL 5.7 still rejected) and the supplementary-plane sort/range caveat.
- docs/deployment.md: update the prerequisite list and the shared-hosting deployment-tier table to admit MariaDB 10.11+ on the same terms as MySQL.
- TESTING.md and CONTRIBUTING.md: update the engine-floor language and the CI job descriptions (mariadb-smoke added, mariadb-rejection retargeted at 10.6).
- AGENTS.md and examples/README.md: fix the one-line MariaDB claim each still carried.
- CLAUDE.md: correct three bullets left behind by the Dialect/DDL branching work (Support/Dialect, the functional-unique-index invariant, and the LegacyPage test fixture) that still described MySQL-only behavior.
TESTING.md's sort-ordering coverage, docs/reading-entries.md's SortSpec guide, and examples/README.md's database note were all missing or overstating MariaDB's one accepted behavioral divergence: field sorts and range filters order supplementary-plane characters at the opposite end from MySQL. examples/README.md had it backwards, claiming MariaDB "works the same as MySQL." Found during a Stage 7 review pass over the MariaDB support item that re-verified the full suite against four real engines and neutered every load-bearing fix added since detection landed — no code defects, only these doc gaps, the last of which surfaced only on a second "is this really finished?" pass.
The engine has supported MariaDB 10.11+ since target-engine detection landed, but the Composer description still read "MySQL-native". Packagist renders that verbatim, so the most widely read sentence about this package implied MariaDB users were excluded.

- Widen the description to "MySQL & MariaDB-native" and add a mariadb keyword, so someone searching for the engine can find the package.
- Stop calling the default search driver MySQL-native in the README; the same driver serves both engines.
@damarbob damarbob self-assigned this Sep 21, 2026
@damarbob
damarbob merged commit 489520d into main Sep 21, 2026
31 checks passed
@damarbob
damarbob deleted the mariadb-support branch September 21, 2026 13:20
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.

1 participant