Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/phpunit-tests-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ on:
required: false
type: 'string'
default: 'phpunit.xml.dist'
sqlite:
description: 'SQLite version to install (e.g., 3.24.0). Leave empty for latest version.'
required: false
type: 'string'
default: 'latest'
env:
LOCAL_PHP: ${{ inputs.php }}-fpm
PHPUNIT_CONFIG: ${{ inputs.phpunit-config }}
Expand All @@ -31,12 +36,42 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up SQLite
run: |
VERSION='${{ inputs.sqlite }}'
if [ "$VERSION" = 'latest' ]; then
TAG='release'
else
TAG="version-${VERSION}"
fi
wget -O sqlite.tar.gz "https://sqlite.org/src/tarball/sqlite.tar.gz?r=${TAG}"
tar xzf sqlite.tar.gz
cd sqlite
./configure --prefix=/usr/local CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_FTS5 -DSQLITE_USE_URI -DSQLITE_ENABLE_JSON1" LDFLAGS="-lm"
make -j$(nproc)
sudo make install
sudo ldconfig

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '${{ inputs.php }}'
tools: phpunit-polyfills

- name: Verify SQLite version in PHP
run: |
EXPECTED='${{ inputs.sqlite }}'
if [ "$EXPECTED" = 'latest' ]; then
EXPECTED=$(cat sqlite/VERSION)
fi
PDO=$(php -r "echo (new PDO('sqlite::memory'))->query('SELECT SQLITE_VERSION();')->fetch()[0];")
echo "Expected SQLite version: $EXPECTED"
echo "PHP PDO SQLite version: $PDO"
if [ "$EXPECTED" != "$PDO" ]; then
echo "Error: Expected SQLite version $EXPECTED, but PHP PDO uses $PDO"
exit 1
fi

- name: Install Composer dependencies
uses: ramsey/composer-install@v3
with:
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/phpunit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
test:
name: PHP ${{ matrix.php }}
name: PHP ${{ matrix.php }} / SQLite ${{ matrix.sqlite || 'latest' }}
uses: ./.github/workflows/phpunit-tests-run.yml
permissions:
contents: read
Expand All @@ -18,8 +18,29 @@ jobs:
matrix:
os: [ ubuntu-latest ]
php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ]
include:
# Add specific SQLite versions for specific PHP versions here:
- php: '7.2'
sqlite: '3.27.0' # minimum version with WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS
- php: '7.3'
sqlite: '3.31.1' # Ubuntu 20.04 LTS
- php: '7.4'
sqlite: '3.34.1' # Debian 11 (Bullseye), common with PHP < 8.1
- php: '8.0'
sqlite: '3.37.0' # minimum supported version (STRICT table support), Ubuntu 22.04 LTS (3.37.2)
- php: '8.1'
sqlite: '3.40.1' # Debian 12 (Bookworm)
- php: '8.2'
sqlite: '3.45.1' # Ubuntu 24.04 LTS
- php: '8.3'
sqlite: '3.46.1' # Debian 13 (Trixie), Ubuntu >= 24.10
- php: '8.4'
sqlite: '3.51.2' # First 2026 release
- php: '8.5'
sqlite: 'latest'

with:
os: ${{ matrix.os }}
php: ${{ matrix.php }}
sqlite: ${{ matrix.sqlite || 'latest' }}
phpunit-config: ${{ 'phpunit.xml.dist' }}
2 changes: 1 addition & 1 deletion tests/WP_SQLite_Driver_Metadata_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function testInformationSchemaTables() {
"SELECT
table_name as 'name',
engine AS 'engine',
FLOOR( data_length / 1024 / 1024 ) 'data'
CAST( data_length / 1024 / 1024 AS UNSIGNED ) AS 'data'
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 't'
ORDER BY name ASC"
Expand Down
14 changes: 14 additions & 0 deletions tests/WP_SQLite_Driver_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -11231,4 +11231,18 @@ public function testVersionFunction(): void {
$result = $this->engine->query( 'SELECT VERSION()' );
$this->assertSame( '8.0.38', $result[0]->{'VERSION()'} );
}

public function testSubstringFunction(): void {
$result = $this->assertQuery( "SELECT SUBSTRING('abcdef', 1, 3) AS s" );
$this->assertSame( 'abc', $result[0]->s );

$result = $this->assertQuery( "SELECT SUBSTRING('abcdef', 4) AS s" );
$this->assertSame( 'def', $result[0]->s );

$result = $this->assertQuery( "SELECT SUBSTRING('abcdef' FROM 1 FOR 3) AS s" );
$this->assertSame( 'abc', $result[0]->s );

$result = $this->assertQuery( "SELECT SUBSTRING('abcdef' FROM 4) AS s" );
$this->assertSame( 'def', $result[0]->s );
}
}
100 changes: 54 additions & 46 deletions tests/WP_SQLite_Driver_Translation_Tests.php

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/WP_SQLite_Metadata_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function testInformationSchemaTables() {
"SELECT
table_name as 'name',
engine AS 'engine',
FLOOR( data_length / 1024 / 1024 ) 'data'
CAST( data_length / 1024 / 1024 AS UNSIGNED ) AS 'data'
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'wp_posts'
ORDER BY name ASC;"
Expand Down
11 changes: 10 additions & 1 deletion tests/WP_SQLite_Translator_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ class WP_SQLite_Translator_Tests extends TestCase {
public function setUp(): void {
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class;
$this->sqlite = new $pdo_class( 'sqlite::memory:' );

$this->engine = new WP_SQLite_Translator( $this->sqlite );

// Skip all old driver tests when running on legacy SQLite version.
// The old driver is to be removed in favor of the new AST driver,
// so this is just a temporary measure to pass all CI combinations.
$is_legacy_sqlite = version_compare( $this->engine->get_sqlite_version(), WP_PDO_MySQL_On_SQLite::MINIMUM_SQLITE_VERSION, '<' );
if ( $is_legacy_sqlite ) {
$this->markTestSkipped( "The old SQLite driver doesn't pass some test on legacy SQLite versions" );
return;
}

$this->engine->query(
"CREATE TABLE _options (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
Expand Down
6 changes: 6 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
require_once __DIR__ . '/../wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php';
require_once __DIR__ . '/../wp-includes/sqlite/class-wp-sqlite-translator.php';

// When on an older SQLite version, enable unsafe back compatibility.
$sqlite_version = ( new PDO( 'sqlite::memory:' ) )->query( 'SELECT SQLITE_VERSION();' )->fetch()[0];
if ( version_compare( $sqlite_version, WP_PDO_MySQL_On_SQLite::MINIMUM_SQLITE_VERSION, '<' ) ) {
define( 'WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS', true );
}

// Configure the test environment.
error_reporting( E_ALL );
define( 'FQDB', ':memory:' );
Expand Down
117 changes: 107 additions & 10 deletions wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ public function get_connection(): WP_SQLite_Connection {
* @return string SQLite engine version as a string.
*/
public function get_sqlite_version(): string {
return $this->connection->query( 'SELECT SQLITE_VERSION()' )->fetchColumn();
return $this->connection->get_pdo()->getAttribute( PDO::ATTR_SERVER_VERSION );
}

/**
Expand Down Expand Up @@ -1646,7 +1646,8 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
* @throws WP_SQLite_Driver_Exception When the query execution fails.
*/
private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void {
$parts = array();
$parts = array();
$on_conflict_update_list = null;
foreach ( $node->get_children() as $child ) {
$is_token = $child instanceof WP_MySQL_Token;
$is_node = $child instanceof WP_Parser_Node;
Expand Down Expand Up @@ -1678,14 +1679,87 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
$table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) );
$parts[] = $this->translate_insert_or_replace_body( $table_name, $child );
} elseif ( $is_node && 'insertUpdateList' === $child->rule_name ) {
// Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
$parts[] = 'ON CONFLICT DO UPDATE SET ';
$parts[] = $this->translate_update_list( $table_name, $child );
/*
* Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
*
* For SQLite versions older than 3.35.0, we need to handle the
* ON CONFLICT clause differently, and at this stage, we only
* save the translated update list to a variable.
*
* See bellow at "Handle ON CONFLICT clause for SQLite < 3.35.0".
*/
$sqlite_version = $this->get_sqlite_version();
if ( version_compare( $sqlite_version, '3.35.0', '<' ) ) {
$on_conflict_update_list = $this->translate_update_list( $table_name, $child );
} else {
$parts[] = 'ON CONFLICT DO UPDATE SET ';
$parts[] = $this->translate_update_list( $table_name, $child );
}
} else {
$parts[] = $this->translate( $child );
}
}

$query = implode( ' ', $parts );

/*
* Handle ON CONFLICT clause for SQLite < 3.35.0.
*
* If and "$on_conflict_update_list" was saved, we are on SQLite version
* older than 3.35.0 and an ON CONFLICT clause was used in the query.
*
* SQLite supports a generic ON CONFLICT clause without an explicit column
* list only from version 3.35.0.
*
* For older versions, we need to work around this limitation:
* 1. Save the ON CONFLICT update list to a variable.
* 2. Execute the query without the ON CONFLICT clause.
* 3. If a constraint violation error occurs, parse the names of the
* columns that caused the violation from the error message.
* 4. Execute the query again, appending the ON CONFLICT clause with
* the column names parsed from the error message.
*/
if ( null !== $on_conflict_update_list ) {
try {
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
} catch ( PDOException $e ) {
$unique_key_violation_prefix = 'SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: ';
if ( '23000' === $e->getCode() && str_contains( $e->getMessage(), $unique_key_violation_prefix ) ) {
/*
* Parse column names from the constraint violation error.
*
* The error message is in the following format:
* <prefix>: <table>.<col1>, <table>.<col2>, ...
*
* The table and column names in the message are not quoted.
* To be on the safe side, we first strip the error message
* prefix and the "<table>." part for the first column, and
* then split the rest of the list by ", <table>." sequence.
*/
$column_list = substr( $e->getMessage(), strlen( $unique_key_violation_prefix ) + strlen( $table_name ) + 1 );
$column_names = explode( ", $table_name.", $column_list );
$quoted_column_names = array_map(
function ( $column ) {
return $this->quote_sqlite_identifier( $column );
},
$column_names
);
$this->execute_sqlite_query(
$query . sprintf(
' ON CONFLICT(%s) DO UPDATE SET %s',
implode( ', ', $quoted_column_names ),
$on_conflict_update_list
)
);
$this->set_result_from_affected_rows();
} else {
throw $e;
}
}
return;
}

$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
}
Expand Down Expand Up @@ -2556,7 +2630,9 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void
sprintf(
'SELECT SCHEMA_NAME AS Database
FROM (
SELECT IIF(SCHEMA_NAME = ?, ?, SCHEMA_NAME) AS SCHEMA_NAME FROM %s ORDER BY SCHEMA_NAME
SELECT CASE WHEN SCHEMA_NAME = ? THEN ? ELSE SCHEMA_NAME END AS SCHEMA_NAME
FROM %s
ORDER BY SCHEMA_NAME
)%s',
$this->quote_sqlite_identifier( $schemata_table ),
isset( $condition ) ? ( ' WHERE TRUE ' . $condition ) : ''
Expand Down Expand Up @@ -3420,6 +3496,22 @@ private function translate( $node ): ?string {
return $this->translate_runtime_function_call( $node );
case 'functionCall':
return $this->translate_function_call( $node );
case 'substringFunction':
$nodes = $node->get_child_nodes();
if ( count( $nodes ) === 2 ) {
return sprintf(
'SUBSTR(%s, %s)',
$this->translate( $nodes[0] ),
$this->translate( $nodes[1] )
);
} else {
return sprintf(
'SUBSTR(%s, %s, %s)',
$this->translate( $nodes[0] ),
$this->translate( $nodes[1] ),
$this->translate( $nodes[2] )
);
}
case 'systemVariable':
$var_ident_type = $node->get_first_child_node( 'varIdentType' );
$type_token = $var_ident_type ? $var_ident_type->get_first_child_token() : null;
Expand Down Expand Up @@ -4040,7 +4132,7 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string
case WP_MySQL_Lexer::LEFT_SYMBOL:
$nodes = $node->get_child_nodes();
return sprintf(
'SUBSTRING(%s, 1, %s)',
'SUBSTR(%s, 1, %s)',
$this->translate( $nodes[0] ),
$this->translate( $nodes[1] )
);
Expand Down Expand Up @@ -4298,7 +4390,7 @@ public function translate_select_item( WP_Parser_Node $node ): string {
* SELECT *, `t`.*, `t`.`table_schema` FROM (
* SELECT
* `TABLE_CATALOG`,
* IIF(`TABLE_SCHEMA` = 'information_schema', `TABLE_SCHEMA`, 'database_name') AS `TABLE_SCHEMA`,
* CASE WHEN `TABLE_SCHEMA` = 'information_schema' THEN `TABLE_SCHEMA` ELSE 'database_name' END AS `TABLE_SCHEMA`,
* `TABLE_NAME`,
* ...
* FROM `_wp_sqlite_mysql_information_schema_tables` AS `tables`
Expand Down Expand Up @@ -4368,7 +4460,7 @@ public function translate_table_ref( WP_Parser_Node $node ): string {
$quoted_column = $this->quote_sqlite_identifier( $column );
if ( isset( $information_schema_db_column_map[ strtoupper( $column ) ] ) ) {
$expanded_list[] = sprintf(
"IIF(%s = 'information_schema', %s, %s) AS %s",
"CASE WHEN %s = 'information_schema' THEN %s ELSE %s END AS %s",
$quoted_column,
$quoted_column,
$this->connection->quote( $this->main_db_name ),
Expand Down Expand Up @@ -5726,7 +5818,12 @@ function ( $column ) {
$this->quote_sqlite_identifier( $new_table_name ?? $table_name )
);
$create_table_query .= implode( ",\n", $rows );
$create_table_query .= "\n) STRICT";
$create_table_query .= "\n)";

if ( version_compare( $this->get_sqlite_version(), '3.37.0', '>=' ) ) {
$create_table_query .= ' STRICT';
}

return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries );
}

Expand Down
2 changes: 1 addition & 1 deletion wp-includes/sqlite-ast/class-wp-sqlite-configurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public function ensure_database_data(): void {
SET
TABLE_SCHEMA = ?,
CONSTRAINT_SCHEMA = ?,
REFERENCED_TABLE_SCHEMA = IIF(REFERENCED_TABLE_SCHEMA IS NULL, NULL, ?)
REFERENCED_TABLE_SCHEMA = CASE WHEN REFERENCED_TABLE_SCHEMA IS NULL THEN NULL ELSE ? END
WHERE TABLE_SCHEMA != 'information_schema'",
$this->driver->get_connection()->quote_identifier( $key_column_usage_table )
),
Expand Down
Loading