Fix RAND() implementation and add plugin compatibility layer#340
Fix RAND() implementation and add plugin compatibility layer#340wp-fuse wants to merge 1 commit intoWordPress:trunkfrom
Conversation
- Translate parameter-less RAND() natively to SQLite for better performance - Fix seeded RAND(N) to correctly delegate to the PHP UDF, resolving a TypeError - Update rand() UDF to return a float (0.0-1.0) and support optional seeds - Add plugin compatibility integration (integrations/plugin-compatibility/) to strip FOR UPDATE/SKIP LOCKED/NOWAIT and rewrite Action Scheduler queries - Update RAND() test metadata to reflect correct DOUBLE return type
90063a9 to
e835807
Compare
JanJakes
left a comment
There was a problem hiding this comment.
Hi @wp-fuse, thanks for catching the RAND() issue!
I shared my feedback in the code. It is a single pull request and a single commit that bundles multiple unrelated changes; maybe it would benefit from being separate PRs.
The RAND() fix is relevant; don't hesitate to update it per feedback, or I can take it over later on. The rest of the changes need more context about what they are aiming to address. Thanks!
| * @param string $query The SQL query. | ||
| * @return string Modified query. | ||
| */ | ||
| function wp_sqlite_integration_plugin_compat( $query ) { |
There was a problem hiding this comment.
This is an unsafe hack that conflicts with the new driver architecture and goes against what it stands for. The new driver was created to shift from fragile, non-deterministic regex matching to fully analyzing complex database queries. If a specific MySQL construct is not yet supported, it needs to be fixed on the driver level. Don't hesitate to create an issue describing which query doesn't work, and we'll take a look at it.
| // 1. Heavy cleaning of unsupported MySQL locking clauses. | ||
| // SQLite doesn't support FOR UPDATE, SKIP LOCKED, or NOWAIT anywhere. | ||
| // We strip these globally (case-insensitive, multi-line) to prevent syntax errors in subqueries. | ||
| if ( stripos( $query, 'FOR UPDATE' ) !== false ) { |
There was a problem hiding this comment.
This is implemented in the driver:
There was a problem hiding this comment.
You're absolutely right. I wasn't fully aware it was already cleanly handled by the driver. I will be removing this entire regex file from the PR to stick to the intended architecture.
| $query = preg_replace( '/(?<![\'"`])\bgroup\b(?!\s+by)(?![\'"`])/i', '`group`', $query ); | ||
|
|
||
| // Fix 'INSERT wp_actionscheduler...' syntax to include 'INTO'. | ||
| $query = preg_replace( '/INSERT\s+(?!INTO\s+)(wp_actionscheduler_[a-zA-Z0-9_]+)/i', 'INSERT INTO $1', $query ); |
There was a problem hiding this comment.
Can you provide an example of a MySQL query that is failing here?
There was a problem hiding this comment.
We noticed Action Scheduler was occasionally firing INSERT table instead of INSERT INTO table and failing, but I will capture the exact raw queries that failed and open a separate Issue for the driver to handle them natively as you recommended. I'll drop this from the PR.
| // Extract the SELECT logic from the join to use in an IN clause. | ||
| $query = "UPDATE {$matches[1]} SET {$set_clause} WHERE action_id IN ({$matches[2]})"; | ||
| } | ||
| } |
There was a problem hiding this comment.
UPDATE with JOIN is supported by the driver:
There was a problem hiding this comment.
Same as above, I’ll remove this entirely. I will test these plugin operations against the current AST driver natively and report any isolated failures we find via a dedicated Issue.
| return 'REGEXP CHAR(0) || ' . $pattern; | ||
| } | ||
| return 'REGEXP ' . $this->translate( $node->get_first_child_node() ); | ||
| return 'REGEXP ' . $pattern; |
There was a problem hiding this comment.
Could you, please, explain what this change is intended for and provide an example? It doesn't seem to be documented anywhere, and I don't think it's safe (simple str_replace will change all occurrences of a given sequence, even the ones where it's intentional).
There was a problem hiding this comment.
We were temporarily attempting to bypass some complex constraints that were failing on specific regex statements in third-party plugins (FlyingPress), but you are totally right that a blind string replacement is unsafe. I'll revert this undocumented change entirely to keep the AST parser clean!
| switch ( $name ) { | ||
| case 'RAND': | ||
| if ( empty( $args ) ) { | ||
| return '(ABS(RANDOM()) / 9223372036854775808.0)'; |
There was a problem hiding this comment.
Good idea using an SQLite expression when a seed is not provided!
Looking closer, we need to improve this a little bit:
- Calling
ABS(-9223372036854775808)will throw an integer overflow error, as there is no corresponding positive number. - Using
ABSwill cause the value of0.0occur half as likely as any other value (-nandnboth map to the same value).
I think both of the issues above would be addressed by using (RANDOM() / 18446744073709551616.0 + 0.5). Or maybe even better, we can just clear the sign bit instead of ABS: ((RANDOM() & 0x7FFFFFFFFFFFFFFF) / 9223372036854775808.0). Additionally, we need to add a comment about what this snippet does and what the constant is.
There was a problem hiding this comment.
Excellent catch on the integer overflow mapping to 0.0! I will update it to use ((RANDOM() & 0x7FFFFFFFFFFFFFFF) / 9223372036854775808.0) and add an inline comment explaining the clearing of the sign bit and what the floating point constant represents. I'll include it in the next push.
| if ( null !== $seed ) { | ||
| mt_srand( intval( $seed ) ); | ||
| } | ||
| return mt_rand( 0, mt_getrandmax() ) / mt_getrandmax(); |
There was a problem hiding this comment.
I see two potential issues here:
- Global state pollution —
mt_srand()affects allmt_rand()calls process-wide, potentially breaking other code in WordPress core or plugins. - Wrong sequence — MySQL uses a specific LCG, not Mersenne Twister. Code depending on deterministic output from
RAND(42)gets different results.
The UDF should replicate MySQL's actual algorithm with instance-local state. Something like:
private $rand_seed1 = null;
private $rand_seed2 = null;
public function rand( $seed = null ) {
$max_value = 0x3FFFFFFF;
if ( null !== $seed ) {
$this->rand_seed1 = intval( $seed ) % $max_value;
$this->rand_seed2 = $this->rand_seed1 * 3 + 1;
}
if ( null !== $this->rand_seed1 ) {
$this->rand_seed1 = ( $this->rand_seed1 * 3 + $this->rand_seed2 ) % $max_value;
$this->rand_seed2 = ( $this->rand_seed1 + $this->rand_seed2 + 33 ) % $max_value;
return (float) $this->rand_seed1 / (float) $max_value;
}
return mt_rand( 0, mt_getrandmax() ) / mt_getrandmax();
}There was a problem hiding this comment.
I'll use your snippet as a foundation, properly implement the LCG sequence logic to closely match MySQL's output, and run some math verifications against it before updating the PR.
Hi @JanJakes, Thank you so much for the detailed review and guidance! We apologize for bundling multiple changes here. We ended up mixing the RAND() implementation with temporary "regex workarounds" we were using locally for Action Scheduler compatibility. I completely agree with the architectural direction of the AST driver and understand why the regex layer is unsafe and shouldn't be here. I will: Clean up this branch by dropping all the regex/compatibility hacks entirely. |
Fix RAND() implementation and add plugin compatibility layer
Summary
This PR fixes bugs in the
RAND()implementation and adds a new plugin compatibility integration (following the existingintegrations/query-monitor/pattern) to handle third-party plugin queries that are incompatible with SQLite.Changes
RAND() fixes (
wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php)RAND()natively to(ABS(RANDOM()) / 9223372036854775808.0)for better performance (avoids PHP UDF overhead per row).RAND(N)calls correctly fall through to the PHP UDF viatranslate_sequence(), fixing aTypeErrorwhere the function returned no value.UDF fix (
wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php)rand()to accept an optional$seedparameter and return afloat(0.0–1.0) instead of anint, matching MySQL behavior.Plugin compatibility layer (
integrations/plugin-compatibility/)New integration module (same pattern as
integrations/query-monitor/) that sanitizes third-party plugin queries before they reach the AST parser:FOR UPDATE,SKIP LOCKED, andNOWAITglobally (including inside subqueries), since SQLite doesn't support row-level locking.UPDATE ... JOINsyntax toUPDATE ... WHERE IN (...), escapes the unquotedgroupkeyword, and fixes missingINTOinINSERTstatements.Test updates (
tests/WP_SQLite_Driver_Tests.php)RAND()to reflect the correctDOUBLEreturn type.Boot integration (
wp-includes/sqlite/db.php)require_oncefor the new plugin compatibility boot file.Testing