Skip to content

Commit e835807

Browse files
committed
Fix RAND() implementation and add plugin compatibility layer
- 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
1 parent 783790c commit e835807

5 files changed

Lines changed: 84 additions & 11 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
/**
3+
* Plugin compatibility layer.
4+
*
5+
* Provides string-level translation fallbacks for complex third-party plugin queries
6+
* that are incompatible with the pure AST SQLite evaluator (e.g. Action Scheduler).
7+
*/
8+
9+
if ( ! defined( 'ABSPATH' ) ) {
10+
exit;
11+
}
12+
13+
/**
14+
* Filter SQL queries early to fix plugin compatibility issues.
15+
*
16+
* @param string $query The SQL query.
17+
* @return string Modified query.
18+
*/
19+
function wp_sqlite_integration_plugin_compat( $query ) {
20+
if ( ! is_string( $query ) ) {
21+
return $query;
22+
}
23+
24+
// 1. Heavy cleaning of unsupported MySQL locking clauses.
25+
// SQLite doesn't support FOR UPDATE, SKIP LOCKED, or NOWAIT anywhere.
26+
// We strip these globally (case-insensitive, multi-line) to prevent syntax errors in subqueries.
27+
if ( stripos( $query, 'FOR UPDATE' ) !== false ) {
28+
$query = preg_replace( '/\s+FOR\s+UPDATE(?:\s+(?:SKIP\s+LOCKED|NOWAIT))?\b/is', '', $query );
29+
}
30+
31+
// 2. Action Scheduler specific compatibility fixes.
32+
if ( stripos( $query, 'actionscheduler' ) !== false ) {
33+
// Escape the 'group' keyword safely.
34+
// Action Scheduler sometimes queries an unquoted `group` column.
35+
$query = preg_replace( '/(?<![\'"`])\bgroup\b(?!\s+by)(?![\'"`])/i', '`group`', $query );
36+
37+
// Fix 'INSERT wp_actionscheduler...' syntax to include 'INTO'.
38+
$query = preg_replace( '/INSERT\s+(?!INTO\s+)(wp_actionscheduler_[a-zA-Z0-9_]+)/i', 'INSERT INTO $1', $query );
39+
40+
// Fix 'UPDATE ... JOIN' syntax manually.
41+
// Handles variants like "UPDATE table t1 JOIN" and "UPDATE table AS t1 JOIN".
42+
$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';
43+
if ( preg_match( $pattern, $query, $matches ) ) {
44+
$set_clause = str_ireplace( 't1.', '', $matches[3] );
45+
// Extract the SELECT logic from the join to use in an IN clause.
46+
$query = "UPDATE {$matches[1]} SET {$set_clause} WHERE action_id IN ({$matches[2]})";
47+
}
48+
}
49+
50+
return $query;
51+
}
52+
53+
if ( function_exists( 'add_filter' ) ) {
54+
// Execute the compatibility fixes at priority 0 (before other generic manipulations).
55+
add_filter( 'query', 'wp_sqlite_integration_plugin_compat', 0 );
56+
}

tests/WP_SQLite_Driver_Tests.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9068,14 +9068,13 @@ public function testColumnInfoForExpressions(): void {
90689068
'mysqli:type' => 8,
90699069
),
90709070
array(
9071-
// TODO: Fix custom "RAND()" function to behave like in MySQL.
9072-
'native_type' => 'LONGLONG', // DOUBLE in MySQL.
9073-
'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL.
9071+
'native_type' => 'DOUBLE', // DOUBLE in MySQL.
9072+
'pdo_type' => PDO::PARAM_STR, // PARAM_STR in MySQL.
90749073
'flags' => array( 'not_null' ),
90759074
'table' => '',
90769075
'name' => 'col_expr_19',
9077-
'len' => 21, // 23 in MySQL.
9078-
'precision' => 0, // 31 in MySQL.
9076+
'len' => 23, // 23 in MySQL.
9077+
'precision' => 31, // 31 in MySQL.
90799078
'sqlite:decl_type' => '',
90809079

90819080
// Additional MySQLi metadata.
@@ -9084,7 +9083,7 @@ public function testColumnInfoForExpressions(): void {
90849083
'mysqli:db' => 'wp',
90859084
'mysqli:charsetnr' => 63,
90869085
'mysqli:flags' => 0, // 32769 in MySQL.
9087-
'mysqli:type' => 8, // 5 in MySQL.
9086+
'mysqli:type' => 5, // 5 in MySQL.
90889087
),
90899088
array(
90909089
'native_type' => 'LONGLONG',

wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4287,10 +4287,14 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string {
42874287
* be reasonably safe since PHP does not allow null bytes in
42884288
* regular expressions anyway.
42894289
*/
4290+
$pattern = $this->translate( $node->get_first_child_node() );
4291+
// Fix double backslash escaping for REGEXP patterns.
4292+
$pattern = str_replace( '\\\\/', '/', $pattern );
4293+
42904294
if ( true === $is_binary ) {
4291-
return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_first_child_node() );
4295+
return 'REGEXP CHAR(0) || ' . $pattern;
42924296
}
4293-
return 'REGEXP ' . $this->translate( $node->get_first_child_node() );
4297+
return 'REGEXP ' . $pattern;
42944298
}
42954299

42964300
/**
@@ -4366,6 +4370,12 @@ private function translate_function_call( WP_Parser_Node $node ): string {
43664370
}
43674371

43684372
switch ( $name ) {
4373+
case 'RAND':
4374+
if ( empty( $args ) ) {
4375+
return '(ABS(RANDOM()) / 9223372036854775808.0)';
4376+
}
4377+
// Seeded RAND() calls should be handled by the PHP UDF.
4378+
return $this->translate_sequence( $node->get_children() );
43694379
case 'DATE_FORMAT':
43704380
list ( $date, $mysql_format ) = $args;
43714381

wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,15 @@ public function md5( $field ) {
176176
* This function uses mt_rand() which is four times faster than rand() and returns
177177
* the random number between 0 and 1.
178178
*
179-
* @return int
179+
* @param int|null $seed The seed value (optional).
180+
*
181+
* @return float
180182
*/
181-
public function rand() {
182-
return mt_rand( 0, 1 );
183+
public function rand( $seed = null ) {
184+
if ( null !== $seed ) {
185+
mt_srand( intval( $seed ) );
186+
}
187+
return mt_rand( 0, mt_getrandmax() ) / mt_getrandmax();
183188
}
184189

185190
/**

wp-includes/sqlite/db.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,7 @@
8585

8686
// Boot the Query Monitor plugin if it is active.
8787
require_once dirname( __DIR__, 2 ) . '/integrations/query-monitor/boot.php';
88+
89+
// Boot the SQLite Plugin Compatibility Layer.
90+
require_once dirname( __DIR__, 2 ) . '/integrations/plugin-compatibility/boot.php';
8891
}

0 commit comments

Comments
 (0)