-
Notifications
You must be signed in to change notification settings - Fork 54
PDO API foundations #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JanJakes
wants to merge
23
commits into
develop
Choose a base branch
from
pdo-api
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
PDO API foundations #291
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 d47bb69
Implement PDO::exec() API
JanJakes e56b913
Implement and use PDOStatement fetch() and fetchAll() with basic fetc…
JanJakes 6eadd3d
Implement PDO and PDOStatement getAttribute() and setAttribute()
JanJakes 5c1b7b7
Use the PHP default for PDO::ATTR_STRINGIFY_FETCHES
JanJakes 220c1e2
Add a fix for PDO::ATTR_STRINGIFY_FETCHES=false with PHP < 8.1
JanJakes 011d091
Implement PDOStatement::setFetchMode()
JanJakes 1da7bac
Respect PDO::query() fetch mode arguments
JanJakes 3e541a0
Add comment explaining PDO attribute default
JanJakes caf76e8
Support whitespace in PDO DSN as per PHP PDO
JanJakes 0859d00
Support semicolon quoting in PDO DSN
JanJakes 06f5681
Document PDO fetch modes
JanJakes ba5b0ce
Use PDO SQLite statement as a source for MySQL proxy statement
JanJakes 2d28a4e
Fix test assertions for PDO::FETCH_NAMED
JanJakes 90f96ef
Address differences between PDO on PHP < 8.1 and PDO on PHP >= 8.1
JanJakes 717a3c8
Add support for PDO::ATTR_DEFAULT_FETCH_MODE, simplify attribute hand…
JanJakes 5da4935
Fix PDO compatibility issues with older PHP versions
JanJakes f7248a3
Use SQLite query PDO statement as a proxy statement base when possible
JanJakes b636824
Improve DNS parsing by correctly handling "\0" bytes
JanJakes 3f9c967
Improve DSN whitespace parsing
JanJakes 3c8543b
Add support for FOUND_ROWS() without SQL_CALC_FOUND_ROWS
JanJakes 722d119
Fix typo in comment
JanJakes 4215423
Improve comments
JanJakes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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;' ); | ||
JanJakes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $this->assertInstanceOf( PDO::class, $driver ); | ||
| } | ||
|
|
||
| public function test_dsn_parsing(): void { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
adamziel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $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' ); | ||
adamziel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $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 { | ||
|
|
@@ -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. | ||
adamziel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| 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 { | ||
adamziel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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', | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.