-
Notifications
You must be signed in to change notification settings - Fork 52
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
base: develop
Are you sure you want to change the base?
PDO API foundations #291
Conversation
fd4cc59 to
c02c987
Compare
12df654 to
9e8e453
Compare
Extracting smaller chunks from #291 so we can review and merge them independently. This PR includes the following changes: - Removed support for nested transactions. MySQL doesn't support them. - Implemented PDO-like APIs for start transaction, commit, and rollback. - The PDO-like APIs differ from an equivalent SQL command in that they always throw an exception in scenarios when a transaction was already started or not yet started (depending on the method), irrespective of the `ATTR_ERRMODE` setting. - Polyfill `PDO::inTransaction()` on PHP < 8.4, where it is not reliable ([issue](https://bugs.php.net/bug.php?id=81227), [PR](php/php-src#14268)).
| } | ||
|
|
||
| public function test_connection(): void { | ||
| $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=WordPress;' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice ❤️
Extracting smaller chunks from #291 so we can review and merge them independently. This PR adds `Table` column to `SHOW CREATE TABLE` statements, [as per MySQL behavior](https://onecompiler.com/mysql/447x6gea7).
Extracting smaller chunks from #291 so we can review and merge them independently. This PR renames the current `WP_SQLite_Driver` to `WP_PDO_MySQL_On_SQLite` and reintroduces the `WP_SQLite_Driver` as a simple proxy over the new renamed class. It's done in these two steps (first rename, then reintroduce) so that Git understands the rename and presents history (hopefully) accurately. The changes are better understood in a commit-by-commit view. #### `WP_PDO_MySQL_On_SQLite` vs `WP_SQLite_Driver` The "reintroduced" `WP_SQLite_Driver` is not meant to be permanent. It is a temporary proxy so we can gradually modify `WP_PDO_MySQL_On_SQLite` to support PDO APIs while not touching the driver API just yet. Once the basics of PDO API are in place, we can make all dependencies use the new class directly and then remove the `WP_SQLite_Driver`. That is, in the future, the `WP_SQLite_DB extends wpdb` will use directly the new `WP_PDO_MySQL_On_SQLite` class.
5fe6619 to
4aadd4e
Compare
| */ | ||
|
|
||
| /** | ||
| * Some PDOStatement methods are not compatible across different PHP versions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow
| ) { | ||
| $this->pdo = $pdo; | ||
| $this->sqlite_column_meta = $sqlite_column_metadata; | ||
| $this->rows = $rows; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implies we're always loading the full result set into memory. Do we have to do that? I worry we could blow up memory on some large queries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to do that?
@adamziel In most cases, fortunately not. But in some, when we construct the result in PHP (things like SHOW GRANTS, etc.), we need to have a "synthetic" statement that is implementing the PDO API on top of PHP arrays.
For all statements where a MySQL statement corresponds to an SQLite statement, it will be better to use a "proxy" statement. We can't use the SQLite statement directly because some things like column metadata formats differ, but we can use a proxy to avoid loading all results into memory. I actually have a class-wp-pdo-proxy-statement.php in a WIP commit, but I'll probably extract it to a follow-up PR. Here's why I think it's fine:
In the first step, we can use the synthetic statement for everything because 1) the current driver already stores everything in memory ($this->last_result, etc.), and 2) $wpdb loads everything in memory as well. So I think I can complete this PR with the "synthetic" statement, and then in the next PR, I would replace all the $this->last_something props with a single $this->last_result_statement that we would use as the proxy target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(things like SHOW GRANTS, etc.),
I don't suppose we would ever see a 100 megabyte result set in those so that seems fine.
In the first step, we can use the synthetic statement for everything because 1) the current driver already stores everything in memory ($this->last_result, etc.), and 2) $wpdb loads everything in memory as well. So I think I can complete this PR with the "synthetic" statement, and then in the next PR, I would replace all the $this->last_something props with a single $this->last_result_statement that we would use as the proxy target.
That sounds good. For Playground, the database is in memory anyway so it's not a big deal. It will matter when a larger site wants to adapt this driver, and perhaps we're reasonably close to having a technical ability to do it with all the MySQL queries we support. Not a huge priority, but it would be nice to briefly document in a docstring that yes, this is possible and here's what needs to happen technically.
|
@adamziel I rebased this on all the extracted changes, and made the changeset a bit clearer and easier to understand. More details in the updated PR description. |
| * @var array<int, mixed> | ||
| */ | ||
| private $pdo_attributes = array( | ||
| PDO::ATTR_STRINGIFY_FETCHES => PHP_VERSION_ID < 80100 ? true : false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's document the rationale behind this condition
| } | ||
|
|
||
| $args = array(); | ||
| foreach ( explode( ';', $dsn_parts[1] ) as $arg ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's scrutinize this and make double sure we're doing it in the same way as php-src does, e.g. I wonder if mysql:dbname=testdb; host=127.0.0.1 with an extra space is also a correct dsn.
| $args[ $arg_parts[0] ] = $arg_parts[1] ?? null; | ||
| } | ||
|
|
||
| $path = $args['path'] ?? ':memory:'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're exploding by :, so for a DSN such as new PDO('mysql-on-sqlite::memory:') we'll get an empty string as $dsn_parts[1]. Let's add a thorough test suite for parsing the DSN string, it's such a small part that's so important to get right.
| $arg_colno = $fetch_mode_args[0]; | ||
| } elseif ( PDO::FETCH_CLASS === $fetch_mode ) { | ||
| if ( $arg_count < 3 ) { | ||
| throw new ArgumentCountError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pdo_stmt.c doesn't seem to check the number of args for this mode – would it tolerate more? Or is it either 3 or 4 args still?
| } | ||
| $arg_class = $fetch_mode_args[0]; | ||
| $arg_constructor_args = $fetch_mode_args[1] ?? array(); | ||
| } elseif ( PDO::FETCH_INTO === $fetch_mode ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've stumbled upon this weird comment in pdo_stmt.c:
default:
/* No support for PDO_FETCH_INTO which takes 2 args??? */I'm not sure if we need to do anything with it. Does PHP actually support FETCH_INTO? 😅
| // arguments are ignored, and the argument count is not validated. | ||
| $fetch_mode = PDO::FETCH_BOTH; | ||
| } elseif ( PDO::FETCH_COLUMN === $fetch_mode ) { | ||
| if ( 3 !== $arg_count ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it 3 because of the extra query arg at the beginning? I've noticed fetchAll() expects up to 2:
if (ZEND_NUM_ARGS() > 2) {
zend_string *func = get_active_function_or_method_name();
zend_argument_count_error("%s() expects at most 2 argument for the fetch mode provided, %d given",
ZSTR_VAL(func), ZEND_NUM_ARGS());
zend_string_release(func);
RETURN_THROWS();
}| return $type; | ||
| }; | ||
|
|
||
| if ( null === $fetch_mode ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also PDO::FETCH_DEFAULT that pdo_stmt.c seems to rely on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also PDO::FETCH_KEY_PAIR. Let's identify all of those and at least document the unsupported ones (and throw an explicit error when they're used). Ditto for flags.
| */ | ||
| #[ReturnTypeWillChange] | ||
| public function fetch( | ||
| $mode = 0, // PDO::FETCH_DEFAULT (available from PHP 8.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that's why we don't use that constant by name in the other file. Maybe we can refer to our own set of constants that's always consistently defined across all PHP versions? That way we won't need to wonder whether a specific 0 has a special constant-related meaning.
| if ( null === $cursorOrientation ) { | ||
| $cursorOrientation = PDO::FETCH_ORI_NEXT; | ||
| } | ||
| if ( null === $cursorOffset ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will only be null if someone explicitly overrides the default 0 value. It would read slightly more natural if the signature said $cursorOffset = null and then we'd correct nulls to 0s here. I suppose we can't do that because of the parent method signature?
| */ | ||
|
|
||
| /** | ||
| * With PHP < 8.1, the "PDO::ATTR_STRINGIFY_FETCHES" value of "false" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great find!
| case PDO::FETCH_CLASS: | ||
| throw new RuntimeException( "'PDO::FETCH_CLASS' mode is not supported" ); | ||
| case PDO::FETCH_INTO: | ||
| throw new RuntimeException( "'PDO::FETCH_INTO' mode is not supported" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, there we go throwing for unsupported modes. That takes care of my other comment.
This PR implements foundational PDO APIs and uses them in the existing
WP_SQLite_Driverclass.The changes are best understood commit-by-commit, and they include the following:
WP_PDO_MySQL_On_SQLiteextendPDO.PDO::exec()API.PDO::query()API.PDOStatementAPI for in-memory data (operating on PHP arrays) asWP_PDO_Synthetic_Statement. This is required forPDO::query()that must return aPDOStatementinstance.PDOStatement::fetch()andPDOStatement::fetchAll()with the most common PDO fetch modes.PDOandPDOStatementgetAttribute()andsetAttribute()methods.PDOStatement::setFetchMode().Synthetic vs. proxy PDO statement
This initial work implements only a
WP_PDO_Synthetic_Statementthat requires the full PDO statement result set to be loaded in memory. This makes it easier to implement a gradual transition to the PDO API (the current driver loads all result sets in memory as well) and it can support all statement types, including those that are composed on the PHP side.In a follow-up work, it should be possible to transition most statement types to a proxy-like PDO statement that would internally use PDO SQLite statements directly, without eagerly fetching all data. The proxy will be required to address incompatibilities between SQLite and MySQL statements.
Next steps
Subsequent PRs will focus on the following:
PDOandPDOStatementAPIs.WP_PDO_Proxy_Statementas described above.