Skip to content

Commit 9e0d046

Browse files
committed
Fix RAND() function behavior
Replace the stub that returned mt_rand(0, 1) — an integer 0 or 1, not a float in [0, 1) as MySQL requires. Unseeded RAND() compiles to a native SQLite expression: ((RANDOM() & 0x001FFFFFFFFFFFFF) / 9007199254740992.0) The 53-bit mask matches the IEEE 754 double mantissa, so division by 2^53 is exact and strictly less than 1.0. Matches MySQL, where unseeded RAND() uses a thread-level state independent of RAND(N). Seeded RAND(N) routes through a PHP UDF implementing MySQL's exact LCG from my_rnd_init()/my_rnd() (sql/item_func.cc, mysys/my_rnd.cc), bit-exact against MySQL 9.6. Requires 64-bit PHP. Seed handling matches val_int(): NULL becomes 0, floats round to nearest (RAND(3.9) == RAND(4)), numeric strings follow the same path. The UDF keeps a single LCG state per connection, so multiple RAND(N) call sites in one query share a stream: `SELECT RAND(1), RAND(1)` returns (v1, v2) here vs (v, v) in MySQL. Documented divergence. Column metadata is now reported as DOUBLE / PARAM_STR.
1 parent 1e4a19a commit 9e0d046

3 files changed

Lines changed: 414 additions & 21 deletions

File tree

packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,13 @@ class WP_PDO_MySQL_On_SQLite extends PDO {
436436
*/
437437
private $connection;
438438

439+
/**
440+
* User-defined functions registered on the SQLite connection.
441+
*
442+
* @var WP_SQLite_PDO_User_Defined_Functions
443+
*/
444+
private $user_defined_functions;
445+
439446
/**
440447
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
441448
*
@@ -724,7 +731,7 @@ public function __construct(
724731
$this->connection->query( 'PRAGMA foreign_keys = ON' );
725732

726733
// Register SQLite functions.
727-
WP_SQLite_PDO_User_Defined_Functions::register_for( $this->connection->get_pdo() );
734+
$this->user_defined_functions = WP_SQLite_PDO_User_Defined_Functions::register_for( $this->connection->get_pdo() );
728735

729736
// Load MySQL grammar.
730737
if ( null === self::$mysql_grammar ) {
@@ -4395,6 +4402,25 @@ private function translate_function_call( WP_Parser_Node $node ): string {
43954402
}
43964403

43974404
switch ( $name ) {
4405+
case 'RAND':
4406+
/*
4407+
* Unseeded RAND() compiles to a fast native SQLite expression.
4408+
* In MySQL, unseeded RAND() uses the thread-level random state,
4409+
* independent of any RAND(N) seeding, so we don't need PHP UDF.
4410+
*
4411+
* We map SQLite's RANDOM() to a float in [0, 1) by masking to
4412+
* 53 bits (IEEE 754 double mantissa width) and dividing by 2^53.
4413+
* This avoids the edge case where masking to 63 bits and dividing
4414+
* by 2^63 could round to 1.0 due to loss of precision in double.
4415+
*
4416+
* Seeded RAND(N) falls through to the UDF, which implements
4417+
* MySQL's deterministic LCG (Linear Congruential Generator).
4418+
* The UDF also handles RAND(NULL) as RAND(0), matching MySQL.
4419+
*/
4420+
if ( 0 === count( $args ) ) {
4421+
return '((RANDOM() & 0x001FFFFFFFFFFFFF) / 9007199254740992.0)';
4422+
}
4423+
return $this->translate_sequence( $node->get_children() );
43984424
case 'DATE_FORMAT':
43994425
list ( $date, $mysql_format ) = $args;
44004426

@@ -6609,6 +6635,7 @@ private function flush(): void {
66096635
$this->last_column_meta = array();
66106636
$this->is_readonly = false;
66116637
$this->wrapper_transaction_type = null;
6638+
$this->user_defined_functions->flush();
66126639
}
66136640

66146641
/**

packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-pdo-user-defined-functions.php

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,39 @@ public static function register_for( $pdo ): self {
9595
'_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern',
9696
);
9797

98+
/**
99+
* First element of the RAND(N) LCG state (the value the output is derived from).
100+
*
101+
* @var int|null
102+
*/
103+
private $rand_seed1 = null;
104+
105+
/**
106+
* Second element of the RAND(N) LCG state (the paired value used in the recurrence).
107+
*
108+
* @var int|null
109+
*/
110+
private $rand_seed2 = null;
111+
112+
/**
113+
* Last seed value passed to RAND(N) in the current statement.
114+
*
115+
* Used to detect whether the rand sequence is advancing with the same seed
116+
* (e.g. "SELECT RAND(3) FROM t"), or reseeding (starting a new sequence).
117+
*
118+
* @var int|null
119+
*/
120+
private $rand_last_seed = null;
121+
122+
/**
123+
* Clear any per-statement state held by the UDFs.
124+
*/
125+
public function flush(): void {
126+
$this->rand_seed1 = null;
127+
$this->rand_seed2 = null;
128+
$this->rand_last_seed = null;
129+
}
130+
98131
/**
99132
* A helper function to throw an error from SQLite expressions.
100133
*
@@ -167,19 +200,74 @@ public function md5( $field ) {
167200
}
168201

169202
/**
170-
* Method to emulate MySQL RAND() function.
203+
* Method to emulate MySQL's seeded RAND(N) function.
171204
*
172-
* SQLite does have a random generator, but it is called RANDOM() and returns random
173-
* number between -9223372036854775808 and +9223372036854775807. So we substitute it
174-
* with PHP random generator.
205+
* Implements MySQL's deterministic LCG (Linear Congruential Generator),
206+
* producing bit-exact output for a given seed.
175207
*
176-
* This function uses mt_rand() which is four times faster than rand() and returns
177-
* the random number between 0 and 1.
208+
* Known divergences from MySQL:
178209
*
179-
* @return int
210+
* 1. In MySQL, RAND(N) behaves differently depending on whether the seed
211+
* is constant expression or varies per invocation:
212+
* - Constant seed (e.g. "SELECT RAND(3) FROM t"):
213+
* LCG is initialized once per statement and advanced for each row.
214+
* - Non-constant seed (e.g. "SELECT RAND(col) FROM t"):
215+
* LCG is initialized for every row with its seed value.
216+
*
217+
* A SQLite UDF cannot tell whether the seed expression is constant, so
218+
* we just compare the seed against its last value. This diverges from
219+
* MySQL in rare cases, and we can consider improving it in the future.
220+
*
221+
* 2. The LCG state is shared across call sites in the same query, so
222+
* "SELECT RAND(1), RAND(1)" yields different results here than in MySQL.
223+
* This is a rare edge case that we can consider improving in the future.
224+
*
225+
* Unseeded RAND() never reaches this function. The AST driver translates it
226+
* directly to a more efficient SQLite-native expression.
227+
*
228+
* @param int|float|string|null $seed Seed value.
229+
*
230+
* @return float A value in [0, 1).
180231
*/
181-
public function rand() {
182-
return mt_rand( 0, 1 );
232+
public function rand( $seed ) {
233+
// Requires 64-bit PHP. Seed * 0x10000001 can exceed PHP_INT_MAX on 32-bit.
234+
$max_value = 0x3FFFFFFF;
235+
236+
if ( null === $seed ) {
237+
// MySQL treats NULL seed as 0.
238+
$seed = 0;
239+
} elseif ( ! is_int( $seed ) ) {
240+
/*
241+
* MySQL rounds float values and numeric strings take the same path.
242+
* Reduce the value to a 32-bit range using "fmod" to avoid firing
243+
* the "out-of-range float to int" cast deprecation on PHP 8.1+.
244+
*/
245+
$seed = (int) fmod( round( (float) $seed, 0, PHP_ROUND_HALF_EVEN ), 0x100000000 );
246+
}
247+
248+
// Initialize MySQL's internal 30-bit seeds.
249+
if ( $seed !== $this->rand_last_seed ) {
250+
/*
251+
* MySQL casts to uint32, and the intermediate results wrap at 32-bit
252+
* unsigned boundaries. We emulate this with & 0xFFFFFFFF masks.
253+
*/
254+
$seed_u32 = $seed & 0xFFFFFFFF;
255+
$this->rand_seed1 = ( ( $seed_u32 * 0x10001 + 55555555 ) & 0xFFFFFFFF ) % $max_value;
256+
$this->rand_seed2 = ( ( $seed_u32 * 0x10000001 ) & 0xFFFFFFFF ) % $max_value;
257+
$this->rand_last_seed = $seed;
258+
}
259+
260+
/*
261+
* MySQL's LCG recurrence:
262+
* seed1 = (seed1 * 3 + seed2) % max_value
263+
* seed2 = (seed1 + seed2 + 33) % max_value
264+
*
265+
* Note that seed1 is updated first and the new value is used for seed2.
266+
*/
267+
$this->rand_seed1 = ( $this->rand_seed1 * 3 + $this->rand_seed2 ) % $max_value;
268+
$this->rand_seed2 = ( $this->rand_seed1 + $this->rand_seed2 + 33 ) % $max_value;
269+
270+
return (float) $this->rand_seed1 / (float) $max_value;
183271
}
184272

185273
/**

0 commit comments

Comments
 (0)