diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index c3251708..55d8f6df 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -9068,14 +9068,13 @@ public function testColumnInfoForExpressions(): void { 'mysqli:type' => 8, ), array( - // TODO: Fix custom "RAND()" function to behave like in MySQL. - 'native_type' => 'LONGLONG', // DOUBLE in MySQL. - 'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL. + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, 'flags' => array( 'not_null' ), 'table' => '', 'name' => 'col_expr_19', - 'len' => 21, // 23 in MySQL. - 'precision' => 0, // 31 in MySQL. + 'len' => 23, + 'precision' => 31, 'sqlite:decl_type' => '', // Additional MySQLi metadata. @@ -9084,7 +9083,7 @@ public function testColumnInfoForExpressions(): void { 'mysqli:db' => 'wp', 'mysqli:charsetnr' => 63, 'mysqli:flags' => 0, // 32769 in MySQL. - 'mysqli:type' => 8, // 5 in MySQL. + 'mysqli:type' => 5, ), array( 'native_type' => 'LONGLONG', diff --git a/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php b/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php index b70a0dd6..2588b877 100644 --- a/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php +++ b/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php @@ -4366,6 +4366,18 @@ private function translate_function_call( WP_Parser_Node $node ): string { } switch ( $name ) { + case 'RAND': + if ( empty( $args ) ) { + /* + * SQLite's RANDOM() returns a value between -9223372036854775808 and +9223372036854775807. + * We clear the sign bit (using & 0x7FFFFFFFFFFFFFFF) to get a positive integer, + * then divide by 9223372036854775808.0 to get a float between 0.0 and 1.0. + * We avoid ABS() because ABS(-9223372036854775808) would overflow. + */ + return '((RANDOM() & 0x7FFFFFFFFFFFFFFF) / 9223372036854775808.0)'; + } + // 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; diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index b72d787f..6c28aa51 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -95,6 +95,14 @@ public static function register_for( $pdo ): self { '_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern', ); + /** + * Seed instances used for the LCG pseudo-random generator (RAND). + * + * @var int|null + */ + private $rand_seed1 = null; + private $rand_seed2 = null; + /** * A helper function to throw an error from SQLite expressions. * @@ -178,8 +186,40 @@ public function md5( $field ) { * * @return int */ - public function rand() { - return mt_rand( 0, 1 ); + public function rand( $seed = null ) { + $max_value = 0x3FFFFFFF; // 1073741823 + + if ( null !== $seed ) { + /* + * Initialize MySQL's internal 30-bit seeds. + * These constants match MySQL's my_rnd_init() implementation. + */ + $n = (int) $seed; + $this->rand_seed1 = ( $n * 0x10001 + 55555555 ) % $max_value; + $this->rand_seed2 = ( $n * 0x10000001 ) % $max_value; + + // Ensure seeds are positive. + if ( $this->rand_seed1 < 0 ) { + $this->rand_seed1 += $max_value; + } + if ( $this->rand_seed2 < 0 ) { + $this->rand_seed2 += $max_value; + } + } + + if ( null !== $this->rand_seed1 && null !== $this->rand_seed2 ) { + /* + * MySQL's LCG (Linear Congruential Generator) recurrence: + * seed1 = (seed1 * 3 + seed2) % 0x3FFFFFFF + * seed2 = (seed1 + seed2 + 33) % 0x3FFFFFFF + */ + $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(); } /**