diff --git a/.github/doltlite-patches/auxdelete-use-current-key.patch b/.github/doltlite-patches/auxdelete-use-current-key.patch new file mode 100644 index 00000000..4b77d438 --- /dev/null +++ b/.github/doltlite-patches/auxdelete-use-current-key.patch @@ -0,0 +1,37 @@ +Use current cursor key for auxiliary index deletes + +Doltlite's delete path used the cached seek key for BTREE_AUXDELETE even when +that seek key no longer matched the current cursor row. When SQLite deletes a +row through a non-key predicate, secondary index deletes can then target a +stale key and leave the real index entry visible in the transaction. A later +insert or update into the deleted key fails with a false UNIQUE violation. + +Trust the cached seek key only when it matches the current cursor state; the +existing fallback paths can derive the key from the current mutmap entry, +cached payload, or tree cursor. + +Reproduces deterministically on: + + CREATE TABLE s ( + a TEXT COLLATE NOCASE, + b TEXT COLLATE NOCASE, + idx TEXT COLLATE NOCASE, + seq INT, + col TEXT COLLATE NOCASE, + PRIMARY KEY (a, b, idx, seq), + UNIQUE (a, b, idx, seq) + ) STRICT; + INSERT INTO s VALUES ('sqlite_database', 't', 'u', 1, 'b'); + INSERT INTO s VALUES ('sqlite_database', 't', 'u', 2, 'c'); + BEGIN IMMEDIATE; + DELETE FROM s WHERE col = 'b'; + UPDATE s SET seq = 1 WHERE col = 'c'; + +diff --git a/src/prolly_btree.c b/src/prolly_btree.c +index 7085abb..9d73876 100644 +--- a/src/prolly_btree.c ++++ b/src/prolly_btree.c +@@ -7988,2 +7988 @@ static int prollyBtCursorDelete(BtCursor *pCur, u8 flags){ +- if( pCur->nSeekSortKey>0 +- && ((flags & BTREE_AUXDELETE) || cachedSeekKeyMatchesCurrent(pCur)) ){ ++ if( pCur->nSeekSortKey>0 && cachedSeekKeyMatchesCurrent(pCur) ){ diff --git a/.github/doltlite-patches/canonicalize-schema-sql-comments.patch b/.github/doltlite-patches/canonicalize-schema-sql-comments.patch new file mode 100644 index 00000000..4994c665 --- /dev/null +++ b/.github/doltlite-patches/canonicalize-schema-sql-comments.patch @@ -0,0 +1,51 @@ +Strip SQL comments while canonicalizing sqlite_schema SQL + +Doltlite canonicalizes schema SQL by collapsing whitespace before storing +sqlite_schema rows after rollback/restore paths. When the SQL contains line +comments, collapsing newlines but keeping `--` makes the comment consume the +rest of the CREATE statement, so SQLite later reports "malformed database +schema ... incomplete input". + +Skip line and block comments outside quoted strings during schema SQL +canonicalization. This keeps canonicalized schema text parseable while +preserving the existing whitespace normalization behavior. + +Reproduces deterministically on: + + CREATE TABLE c ( + id INT CHECK (id > 0) -- inline comment + ) STRICT; + BEGIN IMMEDIATE; + INSERT INTO c VALUES (0); -- CHECK failure + ROLLBACK; + SELECT * FROM c; -- malformed schema without this fix + +diff --git a/src/doltlite_hashof.c b/src/doltlite_hashof.c +index 4682333..ac1297e 100644 +--- a/src/doltlite_hashof.c ++++ b/src/doltlite_hashof.c +@@ -257,0 +258,24 @@ char *doltliteCanonicalizeSchemaSql(const char *zSql, const char *zName){ ++ if( c=='-' && z[1]=='-' ){ ++ z += 2; ++ while( *z && *z!='\n' && *z!='\r' ) z++; ++ pendingSpace = 1; ++ if( *z==0 ){ ++ z--; ++ }else if( z[0]=='\r' && z[1]=='\n' ){ ++ z++; ++ } ++ continue; ++ } ++ if( c=='/' && z[1]=='*' ){ ++ const char *zEnd = z + 2; ++ while( zEnd[0] && !(zEnd[0]=='*' && zEnd[1]=='/') ){ ++ zEnd++; ++ } ++ pendingSpace = 1; ++ if( zEnd[0]=='*' ){ ++ z = zEnd + 1; ++ }else{ ++ z = zEnd - 1; ++ } ++ continue; ++ } diff --git a/.github/doltlite-patches/keep-driver-rowid-tables.patch b/.github/doltlite-patches/keep-driver-rowid-tables.patch new file mode 100644 index 00000000..c2ff4181 --- /dev/null +++ b/.github/doltlite-patches/keep-driver-rowid-tables.patch @@ -0,0 +1,20 @@ +Keep mysql-on-sqlite tables as rowid tables + +Doltlite auto-converts ordinary primary-key tables without an integer rowid +to WITHOUT ROWID. mysql-on-sqlite currently relies on rowid access for +table recreation, limited UPDATE/DELETE rewrites, on-update triggers, +zero-column result statements, and information-schema ordering. Keep the +tables in rowid mode for the Doltlite compatibility test run. + +--- a/src/build.c ++++ b/src/build.c +@@ -2778,7 +2778,7 @@ + && (p->tabFlags & TF_HasPrimaryKey)!=0 + && p->iPKey<0 + && (tabOpts & TF_WithoutRowid)==0 ){ +- tabOpts |= TF_WithoutRowid; ++ /* patched off in CI: keep mysql-on-sqlite tables as rowid tables */ (void)0; + } +- ++ + /* Doltlite: dolt_ignore is a user-created system table whose diff --git a/.github/doltlite-patches/mergeScan-check-tree-delete.patch b/.github/doltlite-patches/mergeScan-check-tree-delete.patch new file mode 100644 index 00000000..8891b8f6 --- /dev/null +++ b/.github/doltlite-patches/mergeScan-check-tree-delete.patch @@ -0,0 +1,133 @@ +Keep mergeScan aligned with mutmap changes + +Doltlite merges committed prolly-tree rows with transaction-local mutmap +entries during cursor scans. Two edge cases currently leak stale tree rows: + +1) A cursor can be positioned directly at a mutmap INSERT via + setCursorToMutMapEntryPhys(), which jumps mmIdx past earlier mutmap + entries. A later tree emission must still notice a mutmap DELETE for + that same tree key, otherwise SQLite follows a rowid that the mutmap says + is deleted and reports "database disk image is malformed". + +2) After emitting a mutmap-only row, the committed tree cursor can still be + parked before the seek prefix used to find that mutmap row. The next scan + step can then emit a committed tree row that does not satisfy the original + prefix constraint. + +Fix both cases in mergeScan/mergeStep by checking tree keys against mutmap +DELETE entries and by advancing the tree cursor past the emitted mutmap key. + +Reproduces deterministically on: + + CREATE TABLE c (ka TEXT, kb TEXT, kc TEXT, v TEXT, + PRIMARY KEY (ka, kb, kc)); + INSERT INTO c VALUES ('db','t','a',''); + BEGIN IMMEDIATE; + INSERT INTO c VALUES ('db','t','b',''); + DELETE FROM c WHERE ka='db' AND kb='t' AND kc='a'; + UPDATE c SET v = 'x' WHERE ka='db' AND kb='t'; -- malformed w/o fix + +And on: + + CREATE TABLE c (ts TEXT NOT NULL COLLATE NOCASE, + tn TEXT NOT NULL COLLATE NOCASE, + cn TEXT NOT NULL COLLATE NOCASE, + ordinal INT NOT NULL, + PRIMARY KEY (ts, tn, cn)) STRICT; + BEGIN IMMEDIATE; + INSERT INTO c VALUES ('sqlite_database', 't1', 'id', 1); + COMMIT; + BEGIN IMMEDIATE; + INSERT INTO c VALUES ('sqlite_database', 't2', 'id', 1); + SELECT tn FROM c WHERE ts = 'sqlite_database' AND tn = 't2'; + -- returns t2,t1 without the tree-cursor alignment fix + +diff --git a/src/prolly_btree.c b/src/prolly_btree.c +index 7085abb..f9e7d31 100644 +--- a/src/prolly_btree.c ++++ b/src/prolly_btree.c +@@ -6026,0 +6027,33 @@ static int mergeCompare(BtCursor *pCur, ProllyMutMapEntry *e){ ++static int skipDeletedTreeEntry(BtCursor *pCur, int dir, int *pSkipped){ ++ ProllyMutMapEntry *delE = 0; ++ int rc; ++ *pSkipped = 0; ++ if( pCur->curIntKey ){ ++ rc = prollyMutMapFindRc(pCur->pMutMap, 0, 0, ++ prollyCursorIntKey(&pCur->pCur), &delE); ++ }else{ ++ const u8 *pK; int nK; ++ prollyCursorKey(&pCur->pCur, &pK, &nK); ++ rc = prollyMutMapFindRc(pCur->pMutMap, pK, nK, 0, &delE); ++ } ++ if( rc!=SQLITE_OK ) return rc; ++ if( delE && delE->op==PROLLY_EDIT_DELETE ){ ++ rc = advanceTreeCursor(pCur, dir); ++ if( rc!=SQLITE_OK ) return rc; ++ *pSkipped = 1; ++ } ++ return SQLITE_OK; ++} ++ ++static int advanceTreePastMutEntry(BtCursor *pCur, ProllyMutMapEntry *e, int dir){ ++ int rc; ++ if( !e ) return SQLITE_OK; ++ while( pCur->pCur.eState==PROLLY_CURSOR_VALID ){ ++ int cmp = mergeCompare(pCur, e); ++ if( (dir>0 && cmp>0) || (dir<0 && cmp<0) ) break; ++ rc = advanceTreeCursor(pCur, dir); ++ if( rc!=SQLITE_OK ) return rc; ++ } ++ return SQLITE_OK; ++} ++ +@@ -6044,0 +6078,4 @@ static int mergeScan(BtCursor *pCur, int dir, int *pRes){ ++ int skipped = 0; ++ int rc = skipDeletedTreeEntry(pCur, dir, &skipped); ++ if( rc!=SQLITE_OK ) return rc; ++ if( skipped ) continue; +@@ -6057,0 +6095,11 @@ static int mergeScan(BtCursor *pCur, int dir, int *pRes){ ++ /* Tree entry is ahead of mutmap[mmIdx] in scan direction. ++ ** Check whether the mutmap has a DELETE entry for the tree ++ ** key at an order index the iteration has already walked ++ ** past (e.g. after setCursorToMutMapEntryPhys jumped mmIdx ++ ** directly to a later INSERT). Without this check the scan ++ ** would emit a logically-deleted tree row and SQLite would ++ ** later TableMoveto a rowid that mutmap says is gone. */ ++ int skipped = 0; ++ int rc = skipDeletedTreeEntry(pCur, dir, &skipped); ++ if( rc!=SQLITE_OK ) return rc; ++ if( skipped ) continue; +@@ -6115 +6163 @@ static int mergeStepForward(BtCursor *pCur){ +- if( pCur->mergeSrc==MERGE_SRC_TREE || pCur->mergeSrc==MERGE_SRC_BOTH ){ ++ if( pCur->mergeSrc==MERGE_SRC_TREE ){ +@@ -6117,0 +6166,8 @@ static int mergeStepForward(BtCursor *pCur){ ++ }else if( pCur->mergeSrc==MERGE_SRC_MUT ++ || pCur->mergeSrc==MERGE_SRC_BOTH ){ ++ ProllyMutMapEntry *e = 0; ++ if( pCur->mmIdx >= 0 && pCur->mmIdx < pCur->pMutMap->nEntries ){ ++ e = orderedMutMapEntryAt(pCur->pMutMap, pCur->mmIdx); ++ } ++ rc = advanceTreePastMutEntry(pCur, e, 1); ++ if( rc!=SQLITE_OK ) return rc; +@@ -6119 +6175 @@ static int mergeStepForward(BtCursor *pCur){ +- if( pCur->mergeSrc==MERGE_SRC_MUT || pCur->mergeSrc==MERGE_SRC_BOTH ) ++ if( pCur->mergeSrc==MERGE_SRC_MUT || pCur->mergeSrc==MERGE_SRC_BOTH ){ +@@ -6120,0 +6177 @@ static int mergeStepForward(BtCursor *pCur){ ++ } +@@ -6129 +6186 @@ static int mergeStepBackward(BtCursor *pCur){ +- if( pCur->mergeSrc==MERGE_SRC_TREE || pCur->mergeSrc==MERGE_SRC_BOTH ){ ++ if( pCur->mergeSrc==MERGE_SRC_TREE ){ +@@ -6131,0 +6189,8 @@ static int mergeStepBackward(BtCursor *pCur){ ++ }else if( pCur->mergeSrc==MERGE_SRC_MUT ++ || pCur->mergeSrc==MERGE_SRC_BOTH ){ ++ ProllyMutMapEntry *e = 0; ++ if( pCur->mmIdx >= 0 && pCur->mmIdx < pCur->pMutMap->nEntries ){ ++ e = orderedMutMapEntryAt(pCur->pMutMap, pCur->mmIdx); ++ } ++ rc = advanceTreePastMutEntry(pCur, e, -1); ++ if( rc!=SQLITE_OK ) return rc; +@@ -6133 +6198 @@ static int mergeStepBackward(BtCursor *pCur){ +- if( pCur->mergeSrc==MERGE_SRC_MUT || pCur->mergeSrc==MERGE_SRC_BOTH ) ++ if( pCur->mergeSrc==MERGE_SRC_MUT || pCur->mergeSrc==MERGE_SRC_BOTH ){ +@@ -6134,0 +6200 @@ static int mergeStepBackward(BtCursor *pCur){ ++ } diff --git a/.github/workflows/phpunit-tests-doltlite.yml b/.github/workflows/phpunit-tests-doltlite.yml new file mode 100644 index 00000000..ec179b7a --- /dev/null +++ b/.github/workflows/phpunit-tests-doltlite.yml @@ -0,0 +1,304 @@ +name: PHPUnit Tests (Doltlite) + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + test: + name: PHP ${{ matrix.php }} / Doltlite + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + php: [ '8.3' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Doltlite build dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential zlib1g-dev tcl-dev + + - name: Check out Doltlite + uses: actions/checkout@v4 + with: + repository: dolthub/doltlite + path: doltlite-src + + - name: Patch Doltlite for mysql-on-sqlite compatibility + # Four narrow, verifiable source-level patches applied before build: + # + # 1) src/build.c — neutralize the auto-conversion of composite/TEXT-PK + # tables to WITHOUT ROWID, since the driver currently relies on + # rowid tables for table rewrites, DML rewrites, triggers, and + # metadata reads. + # + # 2) src/prolly_btree.c — keep mergeScan aligned when committed + # tree rows are merged with transaction-local mutmap entries. + # Without this, INSERT+DELETE+UPDATE in one transaction trips + # "database disk image is malformed", and prefix scans can leak + # committed tree rows that do not satisfy the seek constraint. + # + # 3) src/doltlite_hashof.c — while canonicalizing sqlite_schema SQL, + # strip comments before collapsing whitespace. Without this, a + # rollback can rewrite CREATE statements containing "--" comments + # into malformed schema SQL. + # + # 4) src/prolly_btree.c — for BTREE_AUXDELETE, trust the cached seek + # key only when it still matches the current cursor row. Without + # this, deletes through non-key predicates can leave stale unique + # index entries visible inside the transaction. + # + # Doltlite upstream already includes the eqSeen preservation and + # lossy-collation payload fixes this workflow previously patched in. + # The verification step below keeps covering those behaviors. + run: | + BUILD=doltlite-src/src/build.c + MOVETO=doltlite-src/src/prolly_btree.c + HASHOF=doltlite-src/src/doltlite_hashof.c + + # --- Patch 1: disable WITHOUT ROWID auto-conversion --- + patch -d doltlite-src -p1 --no-backup-if-mismatch \ + < .github/doltlite-patches/keep-driver-rowid-tables.patch + + # --- Patch 2: align mergeScan tree/mutmap iteration --- + patch -d doltlite-src -p1 --no-backup-if-mismatch \ + < .github/doltlite-patches/mergeScan-check-tree-delete.patch + + # --- Patch 3: keep canonicalized schema SQL parseable --- + patch -d doltlite-src -p1 --no-backup-if-mismatch \ + < .github/doltlite-patches/canonicalize-schema-sql-comments.patch + + # --- Patch 4: delete current auxiliary index keys --- + patch -d doltlite-src -p1 --no-backup-if-mismatch \ + < .github/doltlite-patches/auxdelete-use-current-key.patch + + echo "--- Patched lines ---" + grep -n 'keep mysql-on-sqlite tables as rowid tables\|logically-deleted tree row\|advanceTreePastMutEntry\|z\[1\]==\|cachedSeekKeyMatchesCurrent' "$BUILD" "$MOVETO" "$HASHOF" + + - name: Build Doltlite shared library + run: | + mkdir -p build + cd build + # Same feature set we enable for stock-SQLite runs. PHP's pdo_sqlite + # pulls in sqlite3_column_table_name, which needs COLUMN_METADATA. + CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS5 -DSQLITE_USE_URI -DSQLITE_ENABLE_JSON1" \ + ../configure + make -j"$(nproc)" doltlite-lib + ls -la libdoltlite* sqlite3.h + working-directory: doltlite-src + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php }}' + tools: phpunit-polyfills + + - name: Install Doltlite as the system libsqlite3 + # Replace the system libsqlite3 the dynamic linker resolves for + # pdo_sqlite with the Doltlite build. This is more reliable than + # LD_PRELOAD, which loses to pdo_sqlite's DT_NEEDED on libsqlite3.so.0. + run: | + SRC="${GITHUB_WORKSPACE}/doltlite-src/build/libdoltlite.so" + test -f "$SRC" + + # Drop the build into /usr/local/lib and update the linker cache before + # replacing the system libsqlite3 soname. Running ldconfig after the + # replacement can restore Ubuntu's stock libsqlite3.so.0 symlink. + sudo install -m 0755 "$SRC" /usr/local/lib/libdoltlite.so + sudo ln -sf /usr/local/lib/libdoltlite.so /usr/local/lib/libdoltlite.so.0 + + sudo ldconfig + + sudo ln -sf /usr/local/lib/libdoltlite.so /usr/local/lib/libsqlite3.so + sudo ln -sf /usr/local/lib/libdoltlite.so /usr/local/lib/libsqlite3.so.0 + + MULTIARCH_DIR="$(dirname "$(readlink -f /lib/x86_64-linux-gnu/libsqlite3.so.0)")" + sudo rm -f "${MULTIARCH_DIR}/libsqlite3.so.0" + sudo ln -s /usr/local/lib/libdoltlite.so "${MULTIARCH_DIR}/libsqlite3.so.0" + + echo "libsqlite3.so.0 target:" + readlink -f /lib/x86_64-linux-gnu/libsqlite3.so.0 + + echo "ldd pdo_sqlite:" + ldd "$(php -r 'echo ini_get("extension_dir");')/pdo_sqlite.so" + + - name: Verify Doltlite is active and compatibility behavior is intact + run: | + VERSION=$(php -r 'echo (new PDO("sqlite::memory:"))->query("SELECT SQLITE_VERSION()")->fetch()[0];') + ENGINE=$(php -r 'echo (new PDO("sqlite::memory:"))->query("SELECT doltlite_engine()")->fetch()[0];') + echo "SQLITE_VERSION() = ${VERSION}" + echo "doltlite_engine() = ${ENGINE}" + if [ "${ENGINE}" != "prolly" ]; then + echo "::error::Doltlite is not active (expected doltlite_engine() = 'prolly', got '${ENGINE}')" + exit 1 + fi + # 1) build.c patch: composite-PK driver tables keep rowid accessible + # instead of auto-converting to WITHOUT ROWID. + STATS_ROWID=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE _wp_sqlite_mysql_information_schema_statistics (table_schema TEXT, table_name TEXT, index_name TEXT, seq_in_index INT, PRIMARY KEY (table_schema, table_name, index_name, seq_in_index)) STRICT"); + $db->exec("INSERT INTO _wp_sqlite_mysql_information_schema_statistics VALUES (\"s\", \"t\", \"i\", 1)"); + echo $db->query("SELECT rowid FROM _wp_sqlite_mysql_information_schema_statistics")->fetch()[0]; + ' 2>&1) + echo "statistics metadata rowid = ${STATS_ROWID}" + if ! [[ "${STATS_ROWID}" =~ ^[0-9]+$ ]]; then + echo "::error::rowid-preservation patch did not take effect: ${STATS_ROWID}" + exit 1 + fi + ROWID=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE t (a INT, b INT, PRIMARY KEY(a, b))"); + $db->exec("INSERT INTO t VALUES (1, 2)"); + echo $db->query("SELECT rowid FROM t")->fetch()[0]; + ' 2>&1) + echo "ordinary composite-PK rowid = ${ROWID}" + if ! [[ "${ROWID}" =~ ^[0-9]+$ ]]; then + echo "::error::ordinary composite-PK rowid patch did not take effect: ${ROWID}" + exit 1 + fi + # 2) Doltlite upstream behavior: two sequential DELETEs inside a + # SAVEPOINT on a composite NOCASE PK no longer silently lose + # the second DELETE. + REMAINING=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE t (ts TEXT NOT NULL COLLATE NOCASE, tn TEXT NOT NULL COLLATE NOCASE, cn TEXT NOT NULL COLLATE NOCASE, PRIMARY KEY (ts, tn, cn))"); + $db->exec("INSERT INTO t VALUES (\"s\", \"t\", \"id\")"); + $db->exec("INSERT INTO t VALUES (\"s\", \"t\", \"alpha\")"); + $db->exec("INSERT INTO t VALUES (\"s\", \"t\", \"beta\")"); + $db->exec("SAVEPOINT sp"); + $db->exec("DELETE FROM t WHERE ts = \"s\" AND tn = \"t\" AND cn = \"alpha\""); + $db->exec("DELETE FROM t WHERE ts = \"s\" AND tn = \"t\" AND cn = \"beta\""); + $db->exec("RELEASE SAVEPOINT sp"); + $rows = $db->query("SELECT cn FROM t ORDER BY cn")->fetchAll(PDO::FETCH_COLUMN); + echo implode(",", $rows); + ' 2>&1) + echo "post-SAVEPOINT remaining rows = ${REMAINING}" + if [ "${REMAINING}" != "id" ]; then + echo "::error::eqSeen behavior is still broken: expected only 'id', got '${REMAINING}'" + exit 1 + fi + # 3) mergeScan patch: UPDATE inside a transaction after an + # INSERT + DELETE no longer trips "database disk image is + # malformed". + VAL=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE c (ka TEXT, kb TEXT, kc TEXT, v TEXT, PRIMARY KEY (ka, kb, kc))"); + $db->exec("INSERT INTO c VALUES (\"db\", \"t\", \"a\", \"\")"); + $db->exec("BEGIN IMMEDIATE"); + $db->exec("INSERT INTO c VALUES (\"db\", \"t\", \"b\", \"\")"); + $db->exec("DELETE FROM c WHERE ka = \"db\" AND kb = \"t\" AND kc = \"a\""); + $db->exec("UPDATE c SET v = \"x\" WHERE ka = \"db\" AND kb = \"t\""); + $db->exec("COMMIT"); + echo $db->query("SELECT v FROM c")->fetch()[0]; + ' 2>&1) + echo "post-UPDATE value = ${VAL}" + if [ "${VAL}" != "x" ]; then + echo "::error::mergeScan patch did not take effect: expected 'x', got '${VAL}'" + exit 1 + fi + # 4) mergeScan patch: a mutmap-only row must not be followed + # by a stale committed tree row that falls before the seek + # prefix. + MATCHED_TABLES=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE c (ts TEXT NOT NULL COLLATE NOCASE, tn TEXT NOT NULL COLLATE NOCASE, cn TEXT NOT NULL COLLATE NOCASE, ordinal INT NOT NULL, PRIMARY KEY (ts, tn, cn)) STRICT"); + $insert = $db->prepare("INSERT INTO c VALUES (?, ?, ?, ?)"); + $db->exec("BEGIN IMMEDIATE"); + $insert->execute(array("sqlite_database", "t1", "id", 1)); + $db->exec("COMMIT"); + $db->exec("BEGIN IMMEDIATE"); + $insert->execute(array("sqlite_database", "t2", "id", 1)); + $stmt = $db->prepare("SELECT tn FROM c WHERE ts = ? AND tn = ? ORDER BY ordinal"); + $stmt->execute(array("sqlite_database", "t2")); + echo implode(",", $stmt->fetchAll(PDO::FETCH_COLUMN)); + ' 2>&1) + echo "prefix-scan matched tables = ${MATCHED_TABLES}" + if [ "${MATCHED_TABLES}" != "t2" ]; then + echo "::error::mergeScan prefix scan is still broken: expected only 't2', got '${MATCHED_TABLES}'" + exit 1 + fi + # 5) schema SQL canonicalization patch: rollback after a + # CHECK failure must not make schema SQL with comments + # unparseable. + SCHEMA_AFTER_ROLLBACK=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->exec("CREATE TABLE c ( + id INT CHECK (id > 0), -- inline comment + name TEXT + ) STRICT"); + try { + $db->exec("BEGIN IMMEDIATE"); + $db->exec("INSERT INTO c VALUES (0, \"bad\")"); + $db->exec("COMMIT"); + } catch (Throwable $e) { + $db->exec("ROLLBACK"); + } + $db->query("SELECT * FROM c")->fetchAll(PDO::FETCH_ASSOC); + echo "ok"; + ' 2>&1) + echo "schema after CHECK rollback = ${SCHEMA_AFTER_ROLLBACK}" + if [ "${SCHEMA_AFTER_ROLLBACK}" != "ok" ]; then + echo "::error::schema canonicalization patch did not take effect: ${SCHEMA_AFTER_ROLLBACK}" + exit 1 + fi + # 6) auxiliary index delete patch: deleting through a non-key + # predicate must remove the corresponding unique index entry. + AUXDELETE_REUSE=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->exec("CREATE TABLE s (a TEXT COLLATE NOCASE, b TEXT COLLATE NOCASE, idx TEXT COLLATE NOCASE, seq INT, col TEXT COLLATE NOCASE, PRIMARY KEY (a, b, idx, seq), UNIQUE (a, b, idx, seq)) STRICT"); + $db->exec("INSERT INTO s VALUES (\"sqlite_database\", \"t\", \"u\", 1, \"b\")"); + $db->exec("INSERT INTO s VALUES (\"sqlite_database\", \"t\", \"u\", 2, \"c\")"); + $db->exec("BEGIN IMMEDIATE"); + $delete = $db->prepare("DELETE FROM s WHERE col = ?"); + $delete->execute(array("b")); + $update = $db->prepare("UPDATE s SET seq = 1 WHERE col = ?"); + $update->execute(array("c")); + $select = $db->prepare("SELECT seq FROM s WHERE col = ?"); + $select->execute(array("c")); + echo $select->fetchColumn(); + ' 2>&1) + echo "auxdelete key reuse = ${AUXDELETE_REUSE}" + if [ "${AUXDELETE_REUSE}" != "1" ]; then + echo "::error::auxiliary index delete patch did not take effect: ${AUXDELETE_REUSE}" + exit 1 + fi + # 7) Doltlite upstream behavior: a covering-index read on a + # composite-PK rowid table with a NOCASE text column no + # longer returns folded bytes. + NAME=$(php -r ' + $db = new PDO("sqlite::memory:"); + $db->exec("CREATE TABLE t (id INT, pkcol TEXT COLLATE NOCASE, extra TEXT, PRIMARY KEY (id, pkcol))"); + $db->exec("INSERT INTO t VALUES (1, \"Johnny\", \"x\")"); + echo $db->query("SELECT pkcol FROM t")->fetch()[0]; + ' 2>&1) + echo "covering-index pkcol = ${NAME}" + if [ "${NAME}" != "Johnny" ]; then + echo "::error::lossy-collation behavior is still broken: expected 'Johnny', got '${NAME}'" + exit 1 + fi + + - name: Install Composer dependencies (root) + uses: ramsey/composer-install@v3 + with: + ignore-cache: "yes" + composer-options: "--optimize-autoloader" + + - name: Install Composer dependencies (mysql-on-sqlite) + uses: ramsey/composer-install@v3 + with: + working-directory: packages/mysql-on-sqlite + ignore-cache: "yes" + composer-options: "--optimize-autoloader" + + - name: Run PHPUnit tests against Doltlite + run: php ./vendor/bin/phpunit -c ./phpunit.xml.dist + working-directory: packages/mysql-on-sqlite