Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
011a628
Make WP_PDO_MySQL_On_SQLite extend PDO, add a dummy WP_PDO_Synthetic_…
JanJakes Dec 10, 2025
d47bb69
Implement PDO::exec() API
JanJakes Dec 11, 2025
e56b913
Implement and use PDOStatement fetch() and fetchAll() with basic fetc…
JanJakes Dec 19, 2025
6eadd3d
Implement PDO and PDOStatement getAttribute() and setAttribute()
JanJakes Dec 19, 2025
5c1b7b7
Use the PHP default for PDO::ATTR_STRINGIFY_FETCHES
JanJakes Dec 19, 2025
220c1e2
Add a fix for PDO::ATTR_STRINGIFY_FETCHES=false with PHP < 8.1
JanJakes Dec 19, 2025
011d091
Implement PDOStatement::setFetchMode()
JanJakes Dec 19, 2025
1da7bac
Respect PDO::query() fetch mode arguments
JanJakes Dec 19, 2025
3e541a0
Add comment explaining PDO attribute default
JanJakes Jan 12, 2026
caf76e8
Support whitespace in PDO DSN as per PHP PDO
JanJakes Jan 12, 2026
0859d00
Support semicolon quoting in PDO DSN
JanJakes Jan 12, 2026
06f5681
Document PDO fetch modes
JanJakes Jan 12, 2026
ba5b0ce
Use PDO SQLite statement as a source for MySQL proxy statement
JanJakes Jan 13, 2026
2d28a4e
Fix test assertions for PDO::FETCH_NAMED
JanJakes Jan 14, 2026
90f96ef
Address differences between PDO on PHP < 8.1 and PDO on PHP >= 8.1
JanJakes Jan 14, 2026
717a3c8
Add support for PDO::ATTR_DEFAULT_FETCH_MODE, simplify attribute hand…
JanJakes Jan 14, 2026
5da4935
Fix PDO compatibility issues with older PHP versions
JanJakes Jan 14, 2026
f7248a3
Use SQLite query PDO statement as a proxy statement base when possible
JanJakes Jan 15, 2026
b636824
Improve DNS parsing by correctly handling "\0" bytes
JanJakes Jan 19, 2026
3f9c967
Improve DSN whitespace parsing
JanJakes Jan 19, 2026
3c8543b
Add support for FOUND_ROWS() without SQL_CALC_FOUND_ROWS
JanJakes Jan 19, 2026
722d119
Fix typo in comment
JanJakes Jan 19, 2026
4215423
Improve comments
JanJakes Jan 19, 2026
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
388 changes: 386 additions & 2 deletions tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,234 @@ class WP_PDO_MySQL_On_SQLite_PDO_API_Tests extends TestCase {
private $driver;

public function setUp(): void {
$connection = new WP_SQLite_Connection( array( 'path' => ':memory:' ) );
$this->driver = new WP_PDO_MySQL_On_SQLite( $connection, 'wp' );
$this->driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' );

// Run all tests with stringified fetch mode results, so we can use
// assertions that are consistent across all tested PHP versions.
// The "PDO::ATTR_STRINGIFY_FETCHES" mode is tested separately.
$this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
}

public function test_connection(): void {
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=WordPress;' );
$this->assertInstanceOf( PDO::class, $driver );
}

public function test_dsn_parsing(): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for these tests!

// Standard DSN.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp' );
$this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with trailing semicolon.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' );
$this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with whitespace before argument names.
$driver = new WP_PDO_MySQL_On_SQLite( "mysql-on-sqlite: path=:memory:; \n\r\t\v\fdbname=wp" );
$this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with whitespace in the database name.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname= w p ' );
$this->assertSame( ' w p ', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with semicolon in the database name.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;dbname=w;;p;' );
$this->assertSame( 'w;p', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with semicolon in the database name and a terminating semicolon.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=w;;;p' );
$this->assertSame( 'w;', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with two semicolons in the database name.
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=w;;;;p' );
$this->assertSame( 'w;;p', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );

// DSN with a "\0" byte (always terminates the DSN string).
$driver = new WP_PDO_MySQL_On_SQLite( "mysql-on-sqlite:path=:memory:;dbname=w\0p;" );
$this->assertSame( 'w', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
}

public function test_query(): void {
$result = $this->driver->query( "SELECT 1, 'abc'" );
$this->assertInstanceOf( PDOStatement::class, $result );
if ( PHP_VERSION_ID < 80000 ) {
$this->assertSame(
array(
1 => '1',
2 => '1',
'abc' => 'abc',
3 => 'abc',
),
$result->fetch()
);
} else {
$this->assertSame(
array(
1 => '1',
0 => '1',
'abc' => 'abc',
),
$result->fetch()
);
}
}

/**
* @dataProvider data_pdo_fetch_methods
*/
public function test_query_with_fetch_mode( $query, $mode, $expected ): void {
$stmt = $this->driver->query( $query, $mode );
$result = $stmt->fetch();

if ( is_object( $expected ) ) {
$this->assertInstanceOf( get_class( $expected ), $result );
$this->assertSame( (array) $expected, (array) $result );
} elseif ( PDO::FETCH_NAMED === $mode ) {
// PDO::FETCH_NAMED returns all array keys as strings, even numeric
// ones. This is not possible in plain PHP and might be a PDO bug.
$this->assertSame( array_map( 'strval', array_keys( $expected ) ), array_keys( $result ) );
$this->assertSame( array_values( $expected ), array_values( $result ) );
} else {
$this->assertSame( $expected, $result );
}

$this->assertFalse( $stmt->fetch() );
}

public function test_query_fetch_mode_not_set(): void {
$result = $this->driver->query( 'SELECT 1' );
if ( PHP_VERSION_ID < 80000 ) {
$this->assertSame(
array(
1 => '1',
2 => '1',
),
$result->fetch()
);
} else {
$this->assertSame(
array(
1 => '1',
0 => '1',
),
$result->fetch()
);
}
$this->assertFalse( $result->fetch() );
}

public function test_query_fetch_mode_invalid_arg_count(): void {
$this->expectException( ArgumentCountError::class );
$this->expectExceptionMessage( 'PDO::query() expects exactly 2 arguments for the fetch mode provided, 3 given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_ASSOC, 0 );
}

public function test_query_fetch_default_mode_allow_any_args(): void {
if ( PHP_VERSION_ID < 80100 ) {
// On PHP < 8.1, fetch mode value of NULL is not allowed.
$result = @$this->driver->query( 'SELECT 1', null, 1, 2, 'abc', array(), true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$this->assertFalse( $result );
$this->assertSame( 'PDO::query(): SQLSTATE[HY000]: General error: mode must be an integer', error_get_last()['message'] );
return;
}

// On PHP >= 8.1, NULL fetch mode is allowed to use the default fetch mode.
// In such cases, any additional arguments are ignored and not validated.
$expected_result = array(
array(
1 => '1',
0 => '1',
),
);

$result = $this->driver->query( 'SELECT 1' );
$this->assertSame( $expected_result, $result->fetchAll() );

$result = $this->driver->query( 'SELECT 1', null );
$this->assertSame( $expected_result, $result->fetchAll() );

$result = $this->driver->query( 'SELECT 1', null, 1 );
$this->assertSame( $expected_result, $result->fetchAll() );

$result = $this->driver->query( 'SELECT 1', null, 'abc' );
$this->assertSame( $expected_result, $result->fetchAll() );

$result = $this->driver->query( 'SELECT 1', null, 1, 2, 'abc', array(), true );
$this->assertSame( $expected_result, $result->fetchAll() );
}

public function test_query_fetch_class_not_enough_args(): void {
$this->expectException( ArgumentCountError::class );
$this->expectExceptionMessage( 'PDO::query() expects at least 3 arguments for the fetch mode provided, 2 given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_CLASS );
}

public function test_query_fetch_class_too_many_args(): void {
$this->expectException( ArgumentCountError::class );
$this->expectExceptionMessage( 'PDO::query() expects at most 4 arguments for the fetch mode provided, 5 given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, '\stdClass', array(), array() );
}

public function test_query_fetch_class_invalid_class_type(): void {
$this->expectException( TypeError::class );
$this->expectExceptionMessage( 'PDO::query(): Argument #3 must be of type string, int given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 1 );
}

public function test_query_fetch_class_invalid_class_name(): void {
$this->expectException( TypeError::class );
$this->expectExceptionMessage( 'PDO::query(): Argument #3 must be a valid class' );
$this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 'non-existent-class' );
}

public function test_query_fetch_class_invalid_constructor_args_type(): void {
$this->expectException( TypeError::class );
$this->expectExceptionMessage( 'PDO::query(): Argument #4 must be of type ?array, int given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 'stdClass', 1 );
}

public function test_query_fetch_into_invalid_arg_count(): void {
$this->expectException( ArgumentCountError::class );
$this->expectExceptionMessage( 'PDO::query() expects exactly 3 arguments for the fetch mode provided, 2 given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_INTO );
}

public function test_query_fetch_into_invalid_object_type(): void {
$this->expectException( TypeError::class );
$this->expectExceptionMessage( 'PDO::query(): Argument #3 must be of type object, int given' );
$this->driver->query( 'SELECT 1', PDO::FETCH_INTO, 1 );
}

public function test_exec(): void {
$result = $this->driver->exec( 'SELECT 1' );
$this->assertEquals( 0, $result );

$result = $this->driver->exec( 'CREATE TABLE t (id INT)' );
$this->assertEquals( 0, $result );

$result = $this->driver->exec( 'INSERT INTO t (id) VALUES (1)' );
$this->assertEquals( 1, $result );

$result = $this->driver->exec( 'INSERT INTO t (id) VALUES (2), (3)' );
$this->assertEquals( 2, $result );

$result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id = 0' );
$this->assertEquals( 0, $result );

$result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id = 1' );
$this->assertEquals( 1, $result );

$result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id < 10' );
$this->assertEquals( 2, $result );

$result = $this->driver->exec( 'DELETE FROM t WHERE id = 11' );
$this->assertEquals( 1, $result );

$result = $this->driver->exec( 'DELETE FROM t' );
$this->assertEquals( 2, $result );

$result = $this->driver->exec( 'DROP TABLE t' );
$this->assertEquals( 0, $result );
}

public function test_begin_transaction(): void {
Expand Down Expand Up @@ -50,4 +276,162 @@ public function test_rollback_no_active_transaction(): void {
$this->expectExceptionCode( 0 );
$this->driver->rollBack();
}

public function test_fetch_default(): void {
// Default fetch mode is PDO::FETCH_BOTH.
$result = $this->driver->query( "SELECT 1, 'abc', 2" );
if ( PHP_VERSION_ID < 80000 ) {
$this->assertSame(
array(
1 => '1',
2 => '2',
'abc' => 'abc',
3 => 'abc',
4 => '2',
),
$result->fetch()
);
} else {
$this->assertSame(
array(
1 => '1',
0 => '1',
'abc' => 'abc',
'2' => '2',
),
$result->fetch()
);
}
}

/**
* @dataProvider data_pdo_fetch_methods
*/
public function test_fetch( $query, $mode, $expected ): void {
$stmt = $this->driver->query( $query );
$result = $stmt->fetch( $mode );

if ( is_object( $expected ) ) {
$this->assertInstanceOf( get_class( $expected ), $result );
$this->assertEquals( $expected, $result );
} elseif ( PDO::FETCH_NAMED === $mode ) {
// PDO::FETCH_NAMED returns all array keys as strings, even numeric
// ones. This is not possible in plain PHP and might be a PDO bug.
$this->assertSame( array_map( 'strval', array_keys( $expected ) ), array_keys( $result ) );
$this->assertSame( array_values( $expected ), array_values( $result ) );
} else {
$this->assertSame( $expected, $result );
}
}

public function test_attr_default_fetch_mode(): void {
$this->driver->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM );
$result = $this->driver->query( "SELECT 'a', 'b', 'c'" );
$this->assertSame(
array( 'a', 'b', 'c' ),
$result->fetch()
);

$this->driver->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
$result = $this->driver->query( "SELECT 'a', 'b', 'c'" );
$this->assertSame(
array(
'a' => 'a',
'b' => 'b',
'c' => 'c',
),
$result->fetch()
);
}

public function test_attr_stringify_fetches(): void {
$this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
$result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" );
$this->assertSame(
array( '123', '1.23', 'abc', '1', '0' ),
$result->fetch( PDO::FETCH_NUM )
);

$this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false );
$result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" );
$this->assertSame(
/*
* On PHP < 8.1, "PDO::ATTR_STRINGIFY_FETCHES" set to "false" has no
* effect when "PDO::ATTR_EMULATE_PREPARES" is "true" (the default).
*
* TODO: Consider supporting non-string values on PHP < 8.1 when both
* "PDO::ATTR_STRINGIFY_FETCHES" and "PDO::ATTR_EMULATE_PREPARES"
* are set to "false". This would require emulating the behavior,
* as PDO SQLite on PHP < 8.1 seems to always return strings.
*/
PHP_VERSION_ID < 80100
? array( '123', '1.23', 'abc', '1', '0' )
: array( 123, 1.23, 'abc', 1, 0 ),
$result->fetch( PDO::FETCH_NUM )
);
}

public function data_pdo_fetch_methods(): Generator {
// PDO::FETCH_BOTH
yield 'PDO::FETCH_BOTH' => array(
"SELECT 1, 'abc', 2, 'two' as `2`",
PDO::FETCH_BOTH,
PHP_VERSION_ID < 80000
? array(
1 => '1',
2 => 'two',
'abc' => 'abc',
3 => 'abc',
4 => '2',
5 => 'two',
)
: array(
1 => '1',
0 => '1',
'abc' => 'abc',
2 => 'two',
3 => 'two',
),
);

// PDO::FETCH_NUM
yield 'PDO::FETCH_NUM' => array(
"SELECT 1, 'abc', 2, 'two' as `2`",
PDO::FETCH_NUM,
array( '1', 'abc', '2', 'two' ),
);

// PDO::FETCH_ASSOC
yield 'PDO::FETCH_ASSOC' => array(
"SELECT 1, 'abc', 2, 'two' as `2`",
PDO::FETCH_ASSOC,
array(
1 => '1',
'abc' => 'abc',
2 => 'two',
),
);

// PDO::FETCH_NAMED
yield 'PDO::FETCH_NAMED' => array(
"SELECT 1, 'abc', 2, 'two' as `2`",
PDO::FETCH_NAMED,
array(
1 => '1',
'abc' => 'abc',
2 => array( '2', 'two' ),
),
);

// PDO::FETCH_OBJ
yield 'PDO::FETCH_OBJ' => array(
"SELECT 1, 'abc', 2, 'two' as `2`",
PDO::FETCH_OBJ,
(object) array(
1 => '1',
'abc' => 'abc',
2 => 'two',
),
);
}
}
Loading
Loading