Skip to content

Add PHPUnit tests workflow for Doltlite#371

Draft
JanJakes wants to merge 5 commits intotrunkfrom
doltlite
Draft

Add PHPUnit tests workflow for Doltlite#371
JanJakes wants to merge 5 commits intotrunkfrom
doltlite

Conversation

@JanJakes
Copy link
Copy Markdown
Member

@JanJakes JanJakes commented Apr 24, 2026

Summary

Runs the mysql-on-sqlite PHPUnit suite against Doltlite, a SQLite fork whose storage engine is a content-addressed prolly tree. The suite passes with the same counts as on stock SQLite: 667 tests, ~1.4M assertions, 2 skipped, 2 incomplete.

The workflow builds Doltlite from source, swaps the system libsqlite3 for it so PHP's pdo_sqlite transparently resolves against Doltlite, verifies the swap took effect, and then runs PHPUnit without any filters. The swap replaces the LD_PRELOAD approach we started with — LD_PRELOAD loses to pdo_sqlite's DT_NEEDED binding on libsqlite3.so.0.

Doltlite has four deviations from stock SQLite that break the driver. Each one is addressed by a narrow source-level patch applied before build, paired with a one-shot verify assertion in the workflow that fails fast if the patch ever stops applying upstream:

  1. Disable auto-conversion of tables with a primary key to WITHOUT ROWID, because the driver leans on ORDER BY ROWID as a stable tie-break when reading its own information_schema tables and that idiom disappears once rowid goes away.

  2. Preserve the eqSeen flag across Doltlite's mutmap lookup, because the lookup was resetting it mid-call and causing the second same-table DELETE inside a SAVEPOINT to silently do nothing.

  3. Skip tree entries that the mutmap has already marked for deletion, because otherwise an INSERT + DELETE + UPDATE sequence inside one transaction trips "database disk image is malformed".

  4. Keep the original column bytes in index entries when a column uses NOCASE or RTRIM collation, because Doltlite otherwise reconstructs rows from the sort key — which has folded 'Johnny' down to 'johnny' — whenever SQLite picks the index as a covering index.

The patches are CI-only. Upstreaming them to Doltlite is a separate task.

Test plan

  • PHPUnit Tests (Doltlite) runs green on this PR.
  • Each patch has its own assertion in the workflow's verify step; the job fails fast if any patch stops applying.

…lite

Builds Doltlite from source, replaces the system libsqlite3 with it
so pdo_sqlite resolves against the Doltlite library, and runs the
mysql-on-sqlite PHPUnit suite without any filters.

The libsqlite3 swap is done at /usr/local/lib and in the multiarch
dir — LD_PRELOAD is unreliable because pdo_sqlite's DT_NEEDED binds
the soname at link time.

This commit is the scaffolding only. Doltlite has several deviations
from stock SQLite that break the driver; the compatibility patches
land in follow-up commits, each with its own verify assertion.
Doltlite silently sets TF_WithoutRowid on every table declared with
a primary key, so `SELECT rowid FROM t` fails with "no such column:
rowid". The mysql-on-sqlite driver uses `ORDER BY ROWID` as a stable
tie-break when reading its internal information_schema tables, so
dropping rowid breaks info-schema lookups across the board.

Patch by sed: replace the single `tabOpts |= TF_WithoutRowid;` line
in src/build.c with a no-op, leaving table creation otherwise
untouched. Verified with a PHP one-liner that a composite-PK table
still returns a rowid.
prollyBtCursorIndexMoveto's tree-scan loop sets pIdxKey->eqSeen when
it finds an exact prefix match, and OP_SeekGE with BTREE_SEEK_EQ
relies on that flag to decide whether the seek landed on an exact
row. The subsequent findMatchingMutMapEntry call internally runs
sqlite3VdbeRecordCompare against each candidate mutmap entry, and
each probe resets eqSeen to 0 before the comparison — so after the
lookup the flag is whatever the last mutmap probe left behind, not
what the tree scan saw.

The observable symptom: two sequential DELETEs on a composite NOCASE
PK inside a SAVEPOINT. The first DELETE seeds the mutmap; the
second goes through prollyBtCursorIndexMoveto, finds the matching
tree row, sets eqSeen=1, then loses that flag when checking the
mutmap. OP_SeekGE reads eqSeen=0, bails to seek_not_found, and the
second DELETE silently does nothing.

Patch saves eqSeen before the mutmap lookup and restores it when the
lookup returns no override — checked-in as a .patch file and applied
before build.
prollyBtCursorIndexMoveto can reposition the merge cursor directly
at a mutmap INSERT entry via setCursorToMutMapEntryPhys, which jumps
mmIdx past any mutmap entries that sort earlier. A subsequent
mergeScan iteration that sees `cmp*dir < 0` — the tree entry is
ahead of mutmap[mmIdx] — then emits the tree row without noticing
that the mutmap holds a DELETE for that tree key at an earlier order
index. The tree row is logically deleted but the scan walks over it
anyway; SQLite eventually follows its rowid and hits the deleted
slot, tripping the "database disk image is malformed" check in
sqlite3VdbeFinishMoveto.

Reproduces on an INSERT + DELETE + UPDATE sequence inside one
transaction: the INSERT fills the mutmap, the DELETE adds a
tombstone for an earlier row, the UPDATE's scan emits that earlier
row from the tree, then crashes.

Patch consults the mutmap before emitting a tree entry; if a DELETE
exists for that key, skip and advance the tree cursor. No behavior
change in the common case (no matching mutmap entry), constant-
factor overhead otherwise. Checked in as a .patch file.
…tries

prollyBtCursorInsert stored no value for non-INTKEY inserts where
every record field fits into the sort key (splitKey=0), relying on
getCursorPayload to reconstruct the record from the sort key bytes
on read. That round-trip is lossless for BINARY but the sort-key
encoder in sortkey.c folds A-Z to a-z under NOCASE and strips
trailing spaces under RTRIM, so when SQLite picks the auto-index as
a covering index for a plain column read (e.g. `SELECT pkcol FROM t`
on a composite-PK rowid table with a NOCASE pkcol), OP_Column emits
the folded bytes instead of the original column value.

Reproduces testAlterTableModifyColumnComplexChange — the driver
rebuilds the table via CREATE/INSERT/RENAME and then reads back
'johnny' where it stored 'Johnny'.

Patch: when any pKeyInfo collation is non-identity (NOCASE or
RTRIM), preserve pPayload->pKey as the value alongside the sort
key. getCursorPayload prefers the stored value over reconstruction,
so reads return the original bytes. No behavior change for BINARY-
only records.
@JanJakes JanJakes changed the title Try running PHPUnit tests against Doltlite Add PHPUnit tests workflow for Doltlite Apr 24, 2026
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