Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,13 @@ class WP_PDO_MySQL_On_SQLite extends PDO {
*/
private $connection;

/**
* User-defined functions registered on the SQLite connection.
*
* @var WP_SQLite_PDO_User_Defined_Functions
*/
private $user_defined_functions;

/**
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
*
Expand Down Expand Up @@ -724,7 +731,7 @@ public function __construct(
$this->connection->query( 'PRAGMA foreign_keys = ON' );

// Register SQLite functions.
WP_SQLite_PDO_User_Defined_Functions::register_for( $this->connection->get_pdo() );
$this->user_defined_functions = WP_SQLite_PDO_User_Defined_Functions::register_for( $this->connection->get_pdo() );

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

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

Expand Down Expand Up @@ -6609,6 +6635,7 @@ private function flush(): void {
$this->last_column_meta = array();
$this->is_readonly = false;
$this->wrapper_transaction_type = null;
$this->user_defined_functions->flush();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,39 @@ public static function register_for( $pdo ): self {
'_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern',
);

/**
* First element of the RAND(N) LCG state (the value the output is derived from).
*
* @var int|null
*/
private $rand_seed1 = null;

/**
* Second element of the RAND(N) LCG state (the paired value used in the recurrence).
*
* @var int|null
*/
private $rand_seed2 = null;

/**
* Last seed value passed to RAND(N) in the current statement.
*
* Used to detect whether the rand sequence is advancing with the same seed
* (e.g. "SELECT RAND(3) FROM t"), or reseeding (starting a new sequence).
*
* @var int|null
*/
private $rand_last_seed = null;

/**
* Clear any per-statement state held by the UDFs.
*/
public function flush(): void {
$this->rand_seed1 = null;
$this->rand_seed2 = null;
$this->rand_last_seed = null;
}

/**
* A helper function to throw an error from SQLite expressions.
*
Expand Down Expand Up @@ -167,19 +200,74 @@ public function md5( $field ) {
}

/**
* Method to emulate MySQL RAND() function.
* Method to emulate MySQL's seeded RAND(N) function.
*
* SQLite does have a random generator, but it is called RANDOM() and returns random
* number between -9223372036854775808 and +9223372036854775807. So we substitute it
* with PHP random generator.
* Implements MySQL's deterministic LCG (Linear Congruential Generator),
* producing bit-exact output for a given seed.
*
* This function uses mt_rand() which is four times faster than rand() and returns
* the random number between 0 and 1.
* Known divergences from MySQL:
*
* @return int
* 1. In MySQL, RAND(N) behaves differently depending on whether the seed
* is constant expression or varies per invocation:
* - Constant seed (e.g. "SELECT RAND(3) FROM t"):
* LCG is initialized once per statement and advanced for each row.
* - Non-constant seed (e.g. "SELECT RAND(col) FROM t"):
* LCG is initialized for every row with its seed value.
*
* A SQLite UDF cannot tell whether the seed expression is constant, so
* we just compare the seed against its last value. This diverges from
* MySQL in rare cases, and we can consider improving it in the future.
*
* 2. The LCG state is shared across call sites in the same query, so
* "SELECT RAND(1), RAND(1)" yields different results here than in MySQL.
* This is a rare edge case that we can consider improving in the future.
*
* Unseeded RAND() never reaches this function. The AST driver translates it
* directly to a more efficient SQLite-native expression.
*
* @param int|float|string|null $seed Seed value.
*
* @return float A value in [0, 1).
*/
public function rand() {
return mt_rand( 0, 1 );
public function rand( $seed ) {
// Requires 64-bit PHP. Seed * 0x10000001 can exceed PHP_INT_MAX on 32-bit.
$max_value = 0x3FFFFFFF;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate this defensive polish we have here. Despite the rarity of 32-bit systems, it is always better to handle them.


if ( null === $seed ) {
// MySQL treats NULL seed as 0.
$seed = 0;
} elseif ( ! is_int( $seed ) ) {
/*
* MySQL rounds float values and numeric strings take the same path.
* Reduce the value to a 32-bit range using "fmod" to avoid firing
* the "out-of-range float to int" cast deprecation on PHP 8.1+.
*/
$seed = (int) fmod( round( (float) $seed, 0, PHP_ROUND_HALF_EVEN ), 0x100000000 );
}

// Initialize MySQL's internal 30-bit seeds.
if ( $seed !== $this->rand_last_seed ) {
/*
* MySQL casts to uint32, and the intermediate results wrap at 32-bit
* unsigned boundaries. We emulate this with & 0xFFFFFFFF masks.
*/
$seed_u32 = $seed & 0xFFFFFFFF;
$this->rand_seed1 = ( ( $seed_u32 * 0x10001 + 55555555 ) & 0xFFFFFFFF ) % $max_value;
$this->rand_seed2 = ( ( $seed_u32 * 0x10000001 ) & 0xFFFFFFFF ) % $max_value;
$this->rand_last_seed = $seed;
}

/*
* MySQL's LCG recurrence:
* seed1 = (seed1 * 3 + seed2) % max_value
* seed2 = (seed1 + seed2 + 33) % max_value
*
* Note that seed1 is updated first and the new value is used for seed2.
*/
$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;
}

/**
Expand Down
Loading
Loading