-
Notifications
You must be signed in to change notification settings - Fork 57
Fix RAND() implementation and add plugin compatibility layer #340
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: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||
| <?php | ||||
| /** | ||||
| * Plugin compatibility layer. | ||||
| * | ||||
| * Provides string-level translation fallbacks for complex third-party plugin queries | ||||
| * that are incompatible with the pure AST SQLite evaluator (e.g. Action Scheduler). | ||||
| */ | ||||
|
|
||||
| if ( ! defined( 'ABSPATH' ) ) { | ||||
| exit; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Filter SQL queries early to fix plugin compatibility issues. | ||||
| * | ||||
| * @param string $query The SQL query. | ||||
| * @return string Modified query. | ||||
| */ | ||||
| function wp_sqlite_integration_plugin_compat( $query ) { | ||||
| if ( ! is_string( $query ) ) { | ||||
| return $query; | ||||
| } | ||||
|
|
||||
| // 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 ) { | ||||
|
Member
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. This is implemented in the driver: sqlite-database-integration/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php Line 3810 in bf6a7ac
Author
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. 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( '/\s+FOR\s+UPDATE(?:\s+(?:SKIP\s+LOCKED|NOWAIT))?\b/is', '', $query ); | ||||
| } | ||||
|
|
||||
| // 2. Action Scheduler specific compatibility fixes. | ||||
| if ( stripos( $query, 'actionscheduler' ) !== false ) { | ||||
| // Escape the 'group' keyword safely. | ||||
| // Action Scheduler sometimes queries an unquoted `group` column. | ||||
| $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 ); | ||||
|
Member
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. Can you provide an example of a MySQL query that is failing here?
Author
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. 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. |
||||
|
|
||||
| // Fix 'UPDATE ... JOIN' syntax manually. | ||||
| // Handles variants like "UPDATE table t1 JOIN" and "UPDATE table AS t1 JOIN". | ||||
| $pattern = '/UPDATE\s+([^\s]+)\s+(?:AS\s+)?t1\s+JOIN\s*\((.*?)\)\s*(?:AS\s+)?t2\s*ON\s*t1\.action_id\s*=\s*t2\.action_id\s*SET\s*(.*)/is'; | ||||
| if ( preg_match( $pattern, $query, $matches ) ) { | ||||
| $set_clause = str_ireplace( 't1.', '', $matches[3] ); | ||||
| // 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]})"; | ||||
| } | ||||
| } | ||||
|
Member
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.
sqlite-database-integration/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php Line 2112 in bf6a7ac
Author
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. 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 $query; | ||||
| } | ||||
|
|
||||
| if ( function_exists( 'add_filter' ) ) { | ||||
| // Execute the compatibility fixes at priority 0 (before other generic manipulations). | ||||
| add_filter( 'query', 'wp_sqlite_integration_plugin_compat', 0 ); | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4287,10 +4287,14 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string { | |
| * be reasonably safe since PHP does not allow null bytes in | ||
| * regular expressions anyway. | ||
| */ | ||
| $pattern = $this->translate( $node->get_first_child_node() ); | ||
| // Fix double backslash escaping for REGEXP patterns. | ||
| $pattern = str_replace( '\\\\/', '/', $pattern ); | ||
|
|
||
| if ( true === $is_binary ) { | ||
| return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_first_child_node() ); | ||
| return 'REGEXP CHAR(0) || ' . $pattern; | ||
| } | ||
| return 'REGEXP ' . $this->translate( $node->get_first_child_node() ); | ||
| return 'REGEXP ' . $pattern; | ||
|
Member
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. 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
Author
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. 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! |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -4366,6 +4370,12 @@ private function translate_function_call( WP_Parser_Node $node ): string { | |
| } | ||
|
|
||
| switch ( $name ) { | ||
| case 'RAND': | ||
| if ( empty( $args ) ) { | ||
| return '(ABS(RANDOM()) / 9223372036854775808.0)'; | ||
|
Member
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. Good idea using an SQLite expression when a seed is not provided! Looking closer, we need to improve this a little bit:
I think both of the issues above would be addressed by using
Author
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. 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. |
||
| } | ||
| // Seeded RAND() calls should be handled by the PHP UDF. | ||
| return $this->translate_sequence( $node->get_children() ); | ||
| case 'DATE_FORMAT': | ||
| list ( $date, $mysql_format ) = $args; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -176,10 +176,15 @@ public function md5( $field ) { | |
| * This function uses mt_rand() which is four times faster than rand() and returns | ||
| * the random number between 0 and 1. | ||
| * | ||
| * @return int | ||
| * @param int|null $seed The seed value (optional). | ||
| * | ||
| * @return float | ||
| */ | ||
| public function rand() { | ||
| return mt_rand( 0, 1 ); | ||
| public function rand( $seed = null ) { | ||
| if ( null !== $seed ) { | ||
| mt_srand( intval( $seed ) ); | ||
| } | ||
| return mt_rand( 0, mt_getrandmax() ) / mt_getrandmax(); | ||
|
Member
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. I see two potential issues here:
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();
}
Author
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. 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. |
||
| } | ||
|
|
||
| /** | ||
|
|
||
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 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.