Skip to content

Commit cebdfa6

Browse files
authored
Improve compatibility with legacy SQLite versions (#302)
## SQLite Version Compatibility Fixes This PR adds support for older SQLite versions by addressing various compatibility issues. It also updates the CI workflow to **test against multiple SQLite versions**. **Compatibility fixes:** 1. **UPSERT / ON CONFLICT clause (SQLite < 3.35.0)** — The generic `ON CONFLICT DO UPDATE` clause without an explicit column list is only supported from SQLite 3.35.0. For older versions, we now catch the constraint violation error, parse the conflicting column names from the error message, and re-execute with an explicit column list. 2. **STRICT tables (SQLite < 3.37.0)** — The `STRICT` keyword for table creation is only supported from SQLite 3.37.0. It is now conditionally omitted on older versions. 3. **VALUES list `columnN` naming (SQLite < 3.33.0)** — Automatic column naming for VALUES lists (`column1`, `column2`, etc.) is only supported from SQLite 3.33.0. For older versions, we now emulate this by prepending a dummy `SELECT NULL AS column1, ... WHERE FALSE UNION ALL ...` header. 4. **`UPDATE ... FROM` syntax (SQLite < 3.33.0)** — The `UPDATE ... FROM` syntax is only supported from SQLite 3.33.0. We now avoid using it internally in the information schema builder. 5. **`IIF()` function (all versions)** — The `IIF()` function was added in SQLite 3.32.0. We now use `CASE WHEN ... THEN ... ELSE ... END` instead for broader compatibility. 6. **`SUBSTRING()` function (all versions)** — The `SUBSTRING()` function is not supported in older SQLite versions. We now translate it to `SUBSTR()`. 7. **`sqlite_schema` table (SQLite < 3.33.0)** — The `sqlite_schema` table alias was added in SQLite 3.33.0. We now use the older `sqlite_master` name for compatibility. 8. **SQLite version detection** — Changed from using a `SELECT SQLITE_VERSION()` query to using the `PDO::ATTR_SERVER_VERSION` attribute for retrieving the SQLite version, which avoids extra queries by using SQLite C API directly. 9. **CI workflow** — Added support for testing against multiple SQLite versions.
1 parent 216e3a5 commit cebdfa6

13 files changed

+476
-135
lines changed

.github/workflows/phpunit-tests-run.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ on:
1717
required: false
1818
type: 'string'
1919
default: 'phpunit.xml.dist'
20+
sqlite:
21+
description: 'SQLite version to install (e.g., 3.24.0). Leave empty for latest version.'
22+
required: false
23+
type: 'string'
24+
default: 'latest'
2025
env:
2126
LOCAL_PHP: ${{ inputs.php }}-fpm
2227
PHPUNIT_CONFIG: ${{ inputs.phpunit-config }}
@@ -31,12 +36,42 @@ jobs:
3136
- name: Checkout repository
3237
uses: actions/checkout@v4
3338

39+
- name: Set up SQLite
40+
run: |
41+
VERSION='${{ inputs.sqlite }}'
42+
if [ "$VERSION" = 'latest' ]; then
43+
TAG='release'
44+
else
45+
TAG="version-${VERSION}"
46+
fi
47+
wget -O sqlite.tar.gz "https://sqlite.org/src/tarball/sqlite.tar.gz?r=${TAG}"
48+
tar xzf sqlite.tar.gz
49+
cd sqlite
50+
./configure --prefix=/usr/local CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS5 -DSQLITE_USE_URI -DSQLITE_ENABLE_JSON1" LDFLAGS="-lm"
51+
make -j$(nproc)
52+
sudo make install
53+
sudo ldconfig
54+
3455
- name: Set up PHP
3556
uses: shivammathur/setup-php@v2
3657
with:
3758
php-version: '${{ inputs.php }}'
3859
tools: phpunit-polyfills
3960

61+
- name: Verify SQLite version in PHP
62+
run: |
63+
EXPECTED='${{ inputs.sqlite }}'
64+
if [ "$EXPECTED" = 'latest' ]; then
65+
EXPECTED=$(cat sqlite/VERSION)
66+
fi
67+
PDO=$(php -r "echo (new PDO('sqlite::memory'))->query('SELECT SQLITE_VERSION();')->fetch()[0];")
68+
echo "Expected SQLite version: $EXPECTED"
69+
echo "PHP PDO SQLite version: $PDO"
70+
if [ "$EXPECTED" != "$PDO" ]; then
71+
echo "Error: Expected SQLite version $EXPECTED, but PHP PDO uses $PDO"
72+
exit 1
73+
fi
74+
4075
- name: Install Composer dependencies
4176
uses: ramsey/composer-install@v3
4277
with:

.github/workflows/phpunit-tests.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88

99
jobs:
1010
test:
11-
name: PHP ${{ matrix.php }}
11+
name: PHP ${{ matrix.php }} / SQLite ${{ matrix.sqlite || 'latest' }}
1212
uses: ./.github/workflows/phpunit-tests-run.yml
1313
permissions:
1414
contents: read
@@ -18,8 +18,29 @@ jobs:
1818
matrix:
1919
os: [ ubuntu-latest ]
2020
php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ]
21+
include:
22+
# Add specific SQLite versions for specific PHP versions here:
23+
- php: '7.2'
24+
sqlite: '3.27.0' # minimum version with WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS
25+
- php: '7.3'
26+
sqlite: '3.31.1' # Ubuntu 20.04 LTS
27+
- php: '7.4'
28+
sqlite: '3.34.1' # Debian 11 (Bullseye), common with PHP < 8.1
29+
- php: '8.0'
30+
sqlite: '3.37.0' # minimum supported version (STRICT table support), Ubuntu 22.04 LTS (3.37.2)
31+
- php: '8.1'
32+
sqlite: '3.40.1' # Debian 12 (Bookworm)
33+
- php: '8.2'
34+
sqlite: '3.45.1' # Ubuntu 24.04 LTS
35+
- php: '8.3'
36+
sqlite: '3.46.1' # Debian 13 (Trixie), Ubuntu >= 24.10
37+
- php: '8.4'
38+
sqlite: '3.51.2' # First 2026 release
39+
- php: '8.5'
40+
sqlite: 'latest'
2141

2242
with:
2343
os: ${{ matrix.os }}
2444
php: ${{ matrix.php }}
45+
sqlite: ${{ matrix.sqlite || 'latest' }}
2546
phpunit-config: ${{ 'phpunit.xml.dist' }}

tests/WP_SQLite_Driver_Metadata_Tests.php

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public function testInformationSchemaTables() {
7171
"SELECT
7272
table_name as 'name',
7373
engine AS 'engine',
74-
FLOOR( data_length / 1024 / 1024 ) 'data'
74+
CAST( data_length / 1024 / 1024 AS UNSIGNED ) AS 'data'
7575
FROM INFORMATION_SCHEMA.TABLES
7676
WHERE TABLE_NAME = 't'
7777
ORDER BY name ASC"
@@ -251,51 +251,83 @@ public function testCheckTable() {
251251
$result
252252
);
253253

254+
/**
255+
* With SQLite < 3.33.0, the integrity check operation doesn't throw
256+
* an error for missing tables. Let's reflect this in the assertions.
257+
*/
258+
$is_strict_integrity_check_supported = version_compare( $this->engine->get_sqlite_version(), '3.33.0', '>=' );
259+
254260
// A missing table.
255-
$result = $this->assertQuery( 'CHECK TABLE missing' );
256-
$this->assertEquals(
257-
array(
258-
(object) array(
259-
'Table' => 'wp.missing',
260-
'Op' => 'check',
261-
'Msg_type' => 'Error',
262-
'Msg_text' => "Table 'missing' doesn't exist",
263-
),
261+
$result = $this->assertQuery( 'CHECK TABLE missing' );
262+
$expected = array(
263+
(object) array(
264+
'Table' => 'wp.missing',
265+
'Op' => 'check',
266+
'Msg_type' => 'Error',
267+
'Msg_text' => "Table 'missing' doesn't exist",
268+
),
269+
(object) array(
270+
'Table' => 'wp.missing',
271+
'Op' => 'check',
272+
'Msg_type' => 'status',
273+
'Msg_text' => 'Operation failed',
274+
),
275+
);
276+
277+
if ( ! $is_strict_integrity_check_supported ) {
278+
$expected = array(
264279
(object) array(
265280
'Table' => 'wp.missing',
266281
'Op' => 'check',
267282
'Msg_type' => 'status',
268-
'Msg_text' => 'Operation failed',
283+
'Msg_text' => 'OK',
269284
),
285+
);
286+
}
287+
288+
$this->assertEquals( $expected, $result );
289+
290+
// One good and one missing table.
291+
$result = $this->assertQuery( 'CHECK TABLE t1, missing' );
292+
$expected = array(
293+
(object) array(
294+
'Table' => 'wp.t1',
295+
'Op' => 'check',
296+
'Msg_type' => 'status',
297+
'Msg_text' => 'OK',
298+
),
299+
(object) array(
300+
'Table' => 'wp.missing',
301+
'Op' => 'check',
302+
'Msg_type' => 'Error',
303+
'Msg_text' => "Table 'missing' doesn't exist",
304+
),
305+
(object) array(
306+
'Table' => 'wp.missing',
307+
'Op' => 'check',
308+
'Msg_type' => 'status',
309+
'Msg_text' => 'Operation failed',
270310
),
271-
$result
272311
);
273312

274-
// One good and one missing table.
275-
$result = $this->assertQuery( 'CHECK TABLE t1, missing' );
276-
$this->assertEquals(
277-
array(
313+
if ( ! $is_strict_integrity_check_supported ) {
314+
$expected = array(
278315
(object) array(
279316
'Table' => 'wp.t1',
280317
'Op' => 'check',
281318
'Msg_type' => 'status',
282319
'Msg_text' => 'OK',
283320
),
284-
(object) array(
285-
'Table' => 'wp.missing',
286-
'Op' => 'check',
287-
'Msg_type' => 'Error',
288-
'Msg_text' => "Table 'missing' doesn't exist",
289-
),
290321
(object) array(
291322
'Table' => 'wp.missing',
292323
'Op' => 'check',
293324
'Msg_type' => 'status',
294-
'Msg_text' => 'Operation failed',
325+
'Msg_text' => 'OK',
295326
),
296-
),
297-
$result
298-
);
327+
);
328+
}
329+
330+
$this->assertEquals( $expected, $result );
299331
}
300332

301333
public function testOptimizeTable() {

tests/WP_SQLite_Driver_Tests.php

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6768,6 +6768,14 @@ public function testForeignKeyOnDeleteSetDefault(): void {
67686768
}
67696769

67706770
public function testUpdateWithJoinedTables(): void {
6771+
$sqlite_version = $this->engine->get_sqlite_version();
6772+
if ( version_compare( $sqlite_version, '3.33.0', '<' ) ) {
6773+
$this->markTestSkipped(
6774+
sprintf( "SQLite version %s doesn't support UPDATE with FROM clause.", $sqlite_version )
6775+
);
6776+
return;
6777+
}
6778+
67716779
$this->assertQuery( 'CREATE TABLE t1 (id INT, comment TEXT)' );
67726780
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );
67736781
$this->assertQuery( 'CREATE TABLE t3 (id INT, name TEXT)' );
@@ -6841,6 +6849,14 @@ public function testUpdateWithJoinedTables(): void {
68416849
}
68426850

68436851
public function testUpdateWithJoinedTablesInNonStrictMode(): void {
6852+
$sqlite_version = $this->engine->get_sqlite_version();
6853+
if ( version_compare( $sqlite_version, '3.33.0', '<' ) ) {
6854+
$this->markTestSkipped(
6855+
sprintf( "SQLite version %s doesn't support UPDATE with FROM clause.", $sqlite_version )
6856+
);
6857+
return;
6858+
}
6859+
68446860
$this->assertQuery( "SET SESSION sql_mode = ''" );
68456861
$this->assertQuery( 'CREATE TABLE t1 (id INT, comment TEXT)' );
68466862
$this->assertQuery( 'CREATE TABLE t2 (id INT, name TEXT)' );
@@ -6915,6 +6931,14 @@ public function testUpdateWithJoinedTablesInNonStrictMode(): void {
69156931
}
69166932

69176933
public function testUpdateWithJoinComplexQuery(): void {
6934+
$sqlite_version = $this->engine->get_sqlite_version();
6935+
if ( version_compare( $sqlite_version, '3.33.0', '<' ) ) {
6936+
$this->markTestSkipped(
6937+
sprintf( "SQLite version %s doesn't support UPDATE with FROM clause.", $sqlite_version )
6938+
);
6939+
return;
6940+
}
6941+
69186942
$this->assertQuery( "SET SESSION sql_mode = ''" );
69196943

69206944
$default_date = '0000-00-00 00:00:00';
@@ -10118,10 +10142,17 @@ public function testCastValuesOnInsert(): void {
1011810142
$this->assertQuery( "INSERT INTO t VALUES ('2')" );
1011910143
$this->assertQuery( "INSERT INTO t VALUES ('3.0')" );
1012010144

10121-
// TODO: These are supported in MySQL:
10122-
$this->assertQueryError( "INSERT INTO t VALUES ('4.5')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store REAL value in INTEGER column t.value' );
10123-
$this->assertQueryError( 'INSERT INTO t VALUES (0x05)', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10124-
$this->assertQueryError( "INSERT INTO t VALUES (x'06')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10145+
$is_legacy_sqlite = version_compare( $this->engine->get_sqlite_version(), WP_PDO_MySQL_On_SQLite::MINIMUM_SQLITE_VERSION, '<' );
10146+
if ( $is_legacy_sqlite ) {
10147+
$this->assertQuery( "INSERT INTO t VALUES ('4.5')" );
10148+
$this->assertQuery( 'INSERT INTO t VALUES (0x05)' );
10149+
$this->assertQuery( "INSERT INTO t VALUES (x'06')" );
10150+
} else {
10151+
// TODO: These are supported in MySQL:
10152+
$this->assertQueryError( "INSERT INTO t VALUES ('4.5')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store REAL value in INTEGER column t.value' );
10153+
$this->assertQueryError( 'INSERT INTO t VALUES (0x05)', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10154+
$this->assertQueryError( "INSERT INTO t VALUES (x'06')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10155+
}
1012510156

1012610157
$result = $this->assertQuery( 'SELECT * FROM t' );
1012710158
$this->assertSame( null, $result[0]->value );
@@ -10146,8 +10177,13 @@ public function testCastValuesOnInsert(): void {
1014610177
$this->assertQuery( "INSERT INTO t VALUES ('5')" );
1014710178

1014810179
// TODO: These are supported in MySQL:
10149-
$this->assertQueryError( 'INSERT INTO t VALUES (0x06)', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10150-
$this->assertQueryError( "INSERT INTO t VALUES (x'07')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10180+
if ( $is_legacy_sqlite ) {
10181+
$this->assertQuery( 'INSERT INTO t VALUES (0x06)' );
10182+
$this->assertQuery( "INSERT INTO t VALUES (x'07')" );
10183+
} else {
10184+
$this->assertQueryError( 'INSERT INTO t VALUES (0x06)', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10185+
$this->assertQueryError( "INSERT INTO t VALUES (x'07')", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10186+
}
1015110187

1015210188
$result = $this->assertQuery( 'SELECT * FROM t' );
1015310189
$this->assertSame( null, $result[0]->value );
@@ -10613,10 +10649,17 @@ public function testCastValuesOnUpdate(): void {
1061310649
$this->assertQuery( "UPDATE t SET value = '3.0'" );
1061410650
$this->assertSame( '3', $this->assertQuery( 'SELECT * FROM t' )[0]->value );
1061510651

10616-
// TODO: These are supported in MySQL:
10617-
$this->assertQueryError( "UPDATE t SET value = '4.5'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store REAL value in INTEGER column t.value' );
10618-
$this->assertQueryError( 'UPDATE t SET value = 0x05', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10619-
$this->assertQueryError( "UPDATE t SET value = x'06'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10652+
$is_legacy_sqlite = version_compare( $this->engine->get_sqlite_version(), WP_PDO_MySQL_On_SQLite::MINIMUM_SQLITE_VERSION, '<' );
10653+
if ( $is_legacy_sqlite ) {
10654+
$this->assertQuery( "UPDATE t SET value = '4.5'" );
10655+
$this->assertQuery( 'UPDATE t SET value = 0x05' );
10656+
$this->assertQuery( "UPDATE t SET value = x'06'" );
10657+
} else {
10658+
// TODO: These are supported in MySQL:
10659+
$this->assertQueryError( "UPDATE t SET value = '4.5'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store REAL value in INTEGER column t.value' );
10660+
$this->assertQueryError( 'UPDATE t SET value = 0x05', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10661+
$this->assertQueryError( "UPDATE t SET value = x'06'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in INTEGER column t.value' );
10662+
}
1062010663

1062110664
$this->assertQuery( 'DROP TABLE t' );
1062210665

@@ -10652,8 +10695,13 @@ public function testCastValuesOnUpdate(): void {
1065210695
$this->assertSame( PHP_VERSION_ID < 80100 ? '5.0' : '5', $this->assertQuery( 'SELECT * FROM t' )[0]->value );
1065310696

1065410697
// TODO: These are supported in MySQL:
10655-
$this->assertQueryError( 'UPDATE t SET value = 0x06', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10656-
$this->assertQueryError( "UPDATE t SET value = x'07'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10698+
if ( $is_legacy_sqlite ) {
10699+
$this->assertQuery( 'UPDATE t SET value = 0x06' );
10700+
$this->assertQuery( "UPDATE t SET value = x'07'" );
10701+
} else {
10702+
$this->assertQueryError( 'UPDATE t SET value = 0x06', 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10703+
$this->assertQueryError( "UPDATE t SET value = x'07'", 'SQLSTATE[23000]: Integrity constraint violation: 19 cannot store BLOB value in REAL column t.value' );
10704+
}
1065710705

1065810706
$this->assertQuery( 'DROP TABLE t' );
1065910707

@@ -11231,4 +11279,18 @@ public function testVersionFunction(): void {
1123111279
$result = $this->engine->query( 'SELECT VERSION()' );
1123211280
$this->assertSame( '8.0.38', $result[0]->{'VERSION()'} );
1123311281
}
11282+
11283+
public function testSubstringFunction(): void {
11284+
$result = $this->assertQuery( "SELECT SUBSTRING('abcdef', 1, 3) AS s" );
11285+
$this->assertSame( 'abc', $result[0]->s );
11286+
11287+
$result = $this->assertQuery( "SELECT SUBSTRING('abcdef', 4) AS s" );
11288+
$this->assertSame( 'def', $result[0]->s );
11289+
11290+
$result = $this->assertQuery( "SELECT SUBSTRING('abcdef' FROM 1 FOR 3) AS s" );
11291+
$this->assertSame( 'abc', $result[0]->s );
11292+
11293+
$result = $this->assertQuery( "SELECT SUBSTRING('abcdef' FROM 4) AS s" );
11294+
$this->assertSame( 'def', $result[0]->s );
11295+
}
1123411296
}

0 commit comments

Comments
 (0)