initial
This commit is contained in:
+7180
File diff suppressed because it is too large
Load Diff
+382
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDOStatement
|
||||
*
|
||||
* PDO uses camel case naming, enable non-snake case:
|
||||
* phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
* phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
|
||||
*
|
||||
* PDO uses $class as a variable name, enable it:
|
||||
* phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.classFound
|
||||
*
|
||||
* Some PDOStatement methods use $var as a variable name, enable it:
|
||||
* phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.varFound
|
||||
*
|
||||
* We use traits to support different PHP versions with incompatible PDO statement
|
||||
* method signatures. For that, enable multiple object structures in one file:
|
||||
* phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some PDOStatement methods are not compatible across different PHP versions.
|
||||
* To address "Declaration of ... should be compatible with ..." PHP warnings,
|
||||
* we conditionally define traits with different APIs based on the PHP version.
|
||||
*/
|
||||
if ( PHP_VERSION_ID < 80000 ) {
|
||||
trait WP_PDO_Proxy_Statement_PHP_Compat {
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $params Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function setFetchMode( $mode, $params = null ): bool {
|
||||
// Do not pass additional arguments when they are NULL to prevent
|
||||
// "fetch mode doesn't allow any extra arguments" error.
|
||||
if ( null === $params ) {
|
||||
return $this->setDefaultFetchMode( $mode );
|
||||
}
|
||||
return $this->setDefaultFetchMode( $mode, $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $class_name With PDO::FETCH_CLASS, the name of the class to instantiate.
|
||||
* @param mixed $constructor_args With PDO::FETCH_CLASS, the parameters to pass to the class constructor.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
public function fetchAll( $mode = null, $class_name = null, $constructor_args = null ): array {
|
||||
// Do not pass additional arguments when they are NULL to prevent
|
||||
// "Extraneous additional parameters" error.
|
||||
if ( null === $class_name && null === $constructor_args ) {
|
||||
return $this->fetchAllRows( $mode );
|
||||
}
|
||||
return $this->fetchAllRows( $mode, $class_name, $constructor_args );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trait WP_PDO_Proxy_Statement_PHP_Compat {
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $args Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function setFetchMode( $mode, ...$args ): bool {
|
||||
return $this->setDefaultFetchMode( $mode, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $args Additional parameters for the fetch mode.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array {
|
||||
return $this->fetchAllRows( $mode, ...$args );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PDOStatement implementation that operates on in-memory data.
|
||||
*
|
||||
* This class implements a complete PDOStatement interface on top of PHP arrays.
|
||||
* It is used for result sets that are composed or transformed in the PHP layer.
|
||||
*
|
||||
* PDO supports the following fetch modes:
|
||||
* - PDO::FETCH_DEFAULT: current default fetch mode (available from PHP 8.0)
|
||||
* - PDO::FETCH_BOTH: default
|
||||
* - PDO::FETCH_NUM: numeric array
|
||||
* - PDO::FETCH_ASSOC: associative array
|
||||
* - PDO::FETCH_NAMED: associative array retaining duplicate columns
|
||||
* - PDO::FETCH_COLUMN: single column value [1 extra arg]
|
||||
* - PDO::FETCH_KEY_PAIR: key-value pair
|
||||
* - PDO::FETCH_OBJ: object (stdClass)
|
||||
* - PDO::FETCH_CLASS: object (custom class) [1-2 extra args]
|
||||
* - PDO::FETCH_INTO: update an exisisting object, can't be used with fetchAll() [1 extra arg]
|
||||
* - PDO::FETCH_LAZY: lazy fetch via PDORow, can't be used with fetchAll()
|
||||
* - PDO::FETCH_BOUND: bind values to PHP variables, can't be used with fetchAll()
|
||||
* - PDO::FETCH_FUNC: custom function, only works with fetchAll(), can't be default [1 extra arg]
|
||||
*/
|
||||
class WP_PDO_Proxy_Statement extends PDOStatement {
|
||||
use WP_PDO_Proxy_Statement_PHP_Compat;
|
||||
|
||||
/**
|
||||
* The original PDO statement.
|
||||
*
|
||||
* @var PDOStatement
|
||||
*/
|
||||
private $statement;
|
||||
|
||||
/**
|
||||
* The number of affected rows.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $affected_rows;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param PDOStatement $statement The original PDO statement.
|
||||
* @param int $affected_rows The number of affected rows.
|
||||
*/
|
||||
public function __construct(
|
||||
PDOStatement $statement,
|
||||
?int $affected_rows = null
|
||||
) {
|
||||
$this->statement = $statement;
|
||||
$this->affected_rows = $affected_rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a prepared statement.
|
||||
*
|
||||
* @param mixed $params The values to bind to the parameters of the prepared statement.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function execute( $params = null ): bool {
|
||||
return $this->statement->execute( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of columns in the result set.
|
||||
*
|
||||
* @return int The number of columns in the result set.
|
||||
*/
|
||||
public function columnCount(): int {
|
||||
return $this->statement->columnCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of rows affected by the statement.
|
||||
*
|
||||
* @return int The number of rows affected by the statement.
|
||||
*/
|
||||
public function rowCount(): int {
|
||||
return $this->affected_rows ?? $this->statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the next row from the result set.
|
||||
*
|
||||
* @param int|null $mode The fetch mode. Controls how the row is returned.
|
||||
* Default: PDO::FETCH_DEFAULT (null for PHP < 8.0)
|
||||
* @param int|null $cursorOrientation The cursor orientation. Controls which row is returned.
|
||||
* Default: PDO::FETCH_ORI_NEXT (null for PHP < 8.0)
|
||||
* @param int|null $cursorOffset The cursor offset. Controls which row is returned.
|
||||
* Default: 0 (null for PHP < 8.0)
|
||||
* @return mixed The row data formatted according to the fetch mode;
|
||||
* false if there are no more rows or a failure occurs.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetch(
|
||||
$mode = 0, // PDO::FETCH_DEFAULT (available from PHP 8.0)
|
||||
$cursorOrientation = 0,
|
||||
$cursorOffset = 0
|
||||
) {
|
||||
return $this->statement->fetch( $mode, $cursorOrientation, $cursorOffset );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single column from the next row of a result set.
|
||||
*
|
||||
* @param int $column The index of the column to fetch (0-indexed).
|
||||
* @return mixed The value of the column; false if there are no more rows.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetchColumn( $column = 0 ) {
|
||||
return $this->statement->fetchColumn( $column );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the next row as an object.
|
||||
*
|
||||
* @param string $class The name of the class to instantiate.
|
||||
* @param array $constructorArgs The parameters to pass to the class constructor.
|
||||
* @return object The next row as an object.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function fetchObject( $class = 'stdClass', $constructorArgs = array() ) {
|
||||
return $this->statement->fetchObject( $class, $constructorArgs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for a column in a result set.
|
||||
*
|
||||
* @param int $column The index of the column (0-indexed).
|
||||
* @return array|false The column metadata as an associative array,
|
||||
* or false if the column does not exist.
|
||||
*/
|
||||
public function getColumnMeta( $column ): array {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SQLSTATE associated with the last statement operation.
|
||||
*
|
||||
* @return string|null The SQLSTATE error code (as defined by the ANSI SQL standard),
|
||||
* or null if there is no error.
|
||||
*/
|
||||
public function errorCode(): ?string {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch error information associated with the last statement operation.
|
||||
*
|
||||
* @return array The array consists of at least the following fields:
|
||||
* 0: SQLSTATE error code (as defined by the ANSI SQL standard).
|
||||
* 1: Driver-specific error code.
|
||||
* 2: Driver-specific error message.
|
||||
*/
|
||||
public function errorInfo(): array {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a statement attribute.
|
||||
*
|
||||
* @param int $attribute The attribute to get.
|
||||
* @return mixed The value of the attribute.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function getAttribute( $attribute ) {
|
||||
return $this->statement->getAttribute( $attribute );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a statement attribute.
|
||||
*
|
||||
* @param int $attribute The attribute to set.
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function setAttribute( $attribute, $value ): bool {
|
||||
return $this->statement->setAttribute( $attribute, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result set as iterator.
|
||||
*
|
||||
* @return Iterator The iterator for the result set.
|
||||
*/
|
||||
public function getIterator(): Iterator {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances to the next rowset in a multi-rowset statement handle.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function nextRowset(): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cursor, enabling the statement to be executed again.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function closeCursor(): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a column to a PHP variable.
|
||||
*
|
||||
* @param int|string $column Number of the column (1-indexed) or name of the column in the result set.
|
||||
* @param mixed $var PHP variable to which the column will be bound.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @param int $maxLength A hint for pre-allocation.
|
||||
* @param mixed $driverOptions Optional parameters for the driver.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindColumn( $column, &$var, $type = null, $maxLength = null, $driverOptions = null ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a parameter to a PHP variable.
|
||||
*
|
||||
* @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
|
||||
* @param mixed $var PHP variable to which the parameter will be bound.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @param int $maxLength Length of the data type.
|
||||
* @param mixed $driverOptions Optional parameters for the driver.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindParam( $param, &$var, $type = PDO::PARAM_STR, $maxLength = 0, $driverOptions = null ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a value to a parameter.
|
||||
*
|
||||
* @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
|
||||
* @param mixed $value The value to bind to the parameter.
|
||||
* @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function bindValue( $param, $value, $type = PDO::PARAM_STR ): bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump information about the statement.
|
||||
*
|
||||
* Dupms the SQL query and parameters information.
|
||||
*
|
||||
* @return bool|null Returns null, or false on failure.
|
||||
*/
|
||||
public function debugDumpParams(): ?bool {
|
||||
throw new RuntimeException( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all remaining rows from the result set.
|
||||
*
|
||||
* This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
|
||||
* that is defined conditionally based on the current PHP version.
|
||||
*
|
||||
* @param int $mode The fetch mode to use.
|
||||
* @param mixed $args Additional parameters for the fetch mode.
|
||||
* @return array The result set as an array of rows.
|
||||
*/
|
||||
private function fetchAllRows( $mode = null, ...$args ): array {
|
||||
return $this->statement->fetchAll( $mode, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default fetch mode for this statement.
|
||||
*
|
||||
* This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
|
||||
* that is defined conditionally based on the current PHP version.
|
||||
*
|
||||
* @param int $mode The fetch mode to set as the default.
|
||||
* @param mixed $args Additional parameters for the default fetch mode.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
private function setDefaultFetchMode( $mode, ...$args ): bool {
|
||||
return $this->statement->setFetchMode( $mode, ...$args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill ValueError for PHP < 8.0.
|
||||
*/
|
||||
if ( PHP_VERSION_ID < 80000 && ! class_exists( ValueError::class ) ) {
|
||||
class ValueError extends Error {
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SQLite database configurator.
|
||||
*
|
||||
* This class initializes and configures the SQLite database, so that it can be
|
||||
* used by the SQLite driver to translate and emulate MySQL queries in SQLite.
|
||||
*
|
||||
* The configurator ensures that tables required for emulating MySQL behaviors
|
||||
* are created and populated with necessary data. It is also able to partially
|
||||
* repair and update these tables and metadata in case of database corruption.
|
||||
*/
|
||||
class WP_SQLite_Configurator {
|
||||
/**
|
||||
* The SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Builder
|
||||
*/
|
||||
private $schema_builder;
|
||||
|
||||
/**
|
||||
* A service for reconstructing the MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Reconstructor
|
||||
*/
|
||||
private $schema_reconstructor;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver instance.
|
||||
* @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_PDO_MySQL_On_SQLite $driver,
|
||||
WP_SQLite_Information_Schema_Builder $schema_builder
|
||||
) {
|
||||
$this->driver = $driver;
|
||||
$this->schema_builder = $schema_builder;
|
||||
$this->schema_reconstructor = new WP_SQLite_Information_Schema_Reconstructor(
|
||||
$driver,
|
||||
$schema_builder
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the SQLite database is configured.
|
||||
*
|
||||
* This method checks if the database is configured for the latest SQLite
|
||||
* driver version, and if it is not, it will configure the database.
|
||||
*/
|
||||
public function ensure_database_configured(): void {
|
||||
$version = SQLITE_DRIVER_VERSION;
|
||||
$db_version = $this->driver->get_saved_driver_version();
|
||||
if ( version_compare( $version, $db_version ) > 0 ) {
|
||||
$this->configure_database();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the SQLite database.
|
||||
*
|
||||
* This method creates tables used for emulating MySQL behaviors in SQLite,
|
||||
* and populates them with necessary data. When it is used with an already
|
||||
* configured database, it will update the configuration as per the current
|
||||
* SQLite driver version and attempt to repair any configuration corruption.
|
||||
*/
|
||||
public function configure_database(): void {
|
||||
// Use an EXCLUSIVE transaction to prevent multiple connections
|
||||
// from attempting to configure the database at the same time.
|
||||
$this->driver->execute_sqlite_query( 'BEGIN EXCLUSIVE TRANSACTION' );
|
||||
try {
|
||||
$this->ensure_global_variables_table();
|
||||
$this->schema_builder->ensure_information_schema_tables();
|
||||
$this->schema_reconstructor->ensure_correct_information_schema();
|
||||
$this->save_current_driver_version();
|
||||
$this->ensure_database_data();
|
||||
} catch ( Throwable $e ) {
|
||||
$this->driver->execute_sqlite_query( 'ROLLBACK' );
|
||||
throw $e;
|
||||
}
|
||||
$this->driver->execute_sqlite_query( 'COMMIT' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the global variables table exists.
|
||||
*
|
||||
* This method configures a database table to store MySQL global variables
|
||||
* and other internal configuration values.
|
||||
*/
|
||||
private function ensure_global_variables_table(): void {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'CREATE TABLE IF NOT EXISTS %s (name TEXT PRIMARY KEY, value TEXT)',
|
||||
$this->driver->get_connection()->quote_identifier(
|
||||
WP_PDO_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the database data is correctly populated.
|
||||
*
|
||||
* This method ensures that the "INFORMATION_SCHEMA.SCHEMATA" table contains
|
||||
* records for both the "INFORMATION_SCHEMA" database and the user database.
|
||||
* At the moment, only a single user database is supported.
|
||||
*
|
||||
* Additionally, this method ensures that the user database name is stored
|
||||
* correctly in all the information schema tables.
|
||||
*/
|
||||
public function ensure_database_data(): void {
|
||||
// Get all databases from the "SCHEMATA" table.
|
||||
$schemata_table = $this->schema_builder->get_table_name( false, 'schemata' );
|
||||
$databases = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT SCHEMA_NAME FROM %s',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN ); // phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
|
||||
// Ensure that the "INFORMATION_SCHEMA" database record exists.
|
||||
if ( ! in_array( 'information_schema', $databases, true ) ) {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
// The "INFORMATION_SCHEMA" database stays on "utf8mb3" even in MySQL 8 and 9.
|
||||
array( 'information_schema', 'utf8mb3', 'utf8mb3_general_ci' )
|
||||
);
|
||||
}
|
||||
|
||||
// Get the existing user database name.
|
||||
$existing_user_db_name = null;
|
||||
foreach ( $databases as $database ) {
|
||||
if ( 'information_schema' !== strtolower( $database ) ) {
|
||||
$existing_user_db_name = $database;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the user database record exists.
|
||||
if ( null === $existing_user_db_name ) {
|
||||
$existing_user_db_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME;
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)',
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
// @TODO: This should probably be version-dependent.
|
||||
// Before MySQL 8, the default was different.
|
||||
array( $existing_user_db_name, 'utf8mb4', 'utf8mb4_0900_ai_ci' )
|
||||
);
|
||||
}
|
||||
|
||||
// Migrate from older versions without dynamic database names.
|
||||
$saved_database_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME;
|
||||
if ( $saved_database_name !== $existing_user_db_name ) {
|
||||
// INFORMATION_SCHEMA.SCHEMATA
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET SCHEMA_NAME = ? WHERE SCHEMA_NAME != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $schemata_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.TABLES
|
||||
$tables_table = $this->schema_builder->get_table_name( false, 'tables' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $tables_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.COLUMNS
|
||||
$columns_table = $this->schema_builder->get_table_name( false, 'columns' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $columns_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.STATISTICS
|
||||
$statistics_table = $this->schema_builder->get_table_name( false, 'statistics' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ?, INDEX_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $statistics_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.TABLE_CONSTRAINTS
|
||||
$table_constraints_table = $this->schema_builder->get_table_name( false, 'table_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET TABLE_SCHEMA = ?, CONSTRAINT_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $table_constraints_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
|
||||
$referential_constraints_table = $this->schema_builder->get_table_name( false, 'referential_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET CONSTRAINT_SCHEMA = ?, UNIQUE_CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $referential_constraints_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
$key_column_usage_table = $this->schema_builder->get_table_name( false, 'key_column_usage' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s
|
||||
SET
|
||||
TABLE_SCHEMA = ?,
|
||||
CONSTRAINT_SCHEMA = ?,
|
||||
REFERENCED_TABLE_SCHEMA = CASE WHEN REFERENCED_TABLE_SCHEMA IS NULL THEN NULL ELSE ? END
|
||||
WHERE TABLE_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $key_column_usage_table )
|
||||
),
|
||||
array( $saved_database_name, $saved_database_name, $saved_database_name )
|
||||
);
|
||||
|
||||
// INFORMATION_SCHEMA.CHECK_CONSTRAINTS
|
||||
$check_constraints_table = $this->schema_builder->get_table_name( false, 'check_constraints' );
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
"UPDATE %s SET CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'",
|
||||
$this->driver->get_connection()->quote_identifier( $check_constraints_table )
|
||||
),
|
||||
array( $saved_database_name )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current SQLite driver version.
|
||||
*
|
||||
* This method saves the current SQLite driver version to the database.
|
||||
*/
|
||||
private function save_current_driver_version(): void {
|
||||
$this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'INSERT INTO %s (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?',
|
||||
$this->driver->get_connection()->quote_identifier(
|
||||
WP_PDO_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME
|
||||
)
|
||||
),
|
||||
array(
|
||||
WP_PDO_MySQL_On_SQLite::DRIVER_VERSION_VARIABLE_NAME,
|
||||
SQLITE_DRIVER_VERSION,
|
||||
SQLITE_DRIVER_VERSION,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
/*
|
||||
* The SQLite connection uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* SQLite connection.
|
||||
*
|
||||
* This class configures and encapsulates the connection to an SQLite database.
|
||||
* It requires PDO with the SQLite driver, and currently, it is only a simple
|
||||
* wrapper that leaks some of the PDO APIs (returns PDOStatement values, etc.).
|
||||
* In the future, we may abstract it away from PDO and support SQLite3 as well.
|
||||
*/
|
||||
class WP_SQLite_Connection {
|
||||
/**
|
||||
* The default timeout in seconds for SQLite to wait for a writable lock.
|
||||
*/
|
||||
const DEFAULT_SQLITE_TIMEOUT = 10;
|
||||
|
||||
/**
|
||||
* The supported SQLite journal modes.
|
||||
*
|
||||
* See: https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
*/
|
||||
const SQLITE_JOURNAL_MODES = array(
|
||||
'DELETE',
|
||||
'TRUNCATE',
|
||||
'PERSIST',
|
||||
'MEMORY',
|
||||
'WAL',
|
||||
'OFF',
|
||||
);
|
||||
|
||||
/**
|
||||
* The PDO connection for SQLite.
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
/**
|
||||
* A query logger callback.
|
||||
*
|
||||
* @var callable(string, array): void
|
||||
*/
|
||||
private $query_logger;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Set up an SQLite connection.
|
||||
*
|
||||
* @param array $options {
|
||||
* An array of options.
|
||||
*
|
||||
* @type string|null $path Optional. SQLite database path.
|
||||
* For in-memory database, use ':memory:'.
|
||||
* Must be set when PDO instance is not provided.
|
||||
* @type PDO|null $pdo Optional. PDO instance with SQLite connection.
|
||||
* If not provided, a new PDO instance will be created.
|
||||
* @type int|null $timeout Optional. SQLite timeout in seconds.
|
||||
* The time to wait for a writable lock.
|
||||
* @type string|null $journal_mode Optional. SQLite journal mode.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException When some connection options are invalid.
|
||||
* @throws PDOException When the driver initialization fails.
|
||||
*/
|
||||
public function __construct( array $options ) {
|
||||
// Setup PDO connection.
|
||||
if ( isset( $options['pdo'] ) && $options['pdo'] instanceof PDO ) {
|
||||
$this->pdo = $options['pdo'];
|
||||
} else {
|
||||
if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
|
||||
throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' );
|
||||
}
|
||||
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class;
|
||||
$this->pdo = new $pdo_class( 'sqlite:' . $options['path'] );
|
||||
}
|
||||
|
||||
// Throw exceptions on error.
|
||||
$this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
|
||||
|
||||
// Configure SQLite timeout.
|
||||
if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) {
|
||||
$timeout = $options['timeout'];
|
||||
} else {
|
||||
$timeout = self::DEFAULT_SQLITE_TIMEOUT;
|
||||
}
|
||||
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
|
||||
|
||||
// Configure SQLite journal mode.
|
||||
$journal_mode = $options['journal_mode'] ?? null;
|
||||
if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
|
||||
$this->query( 'PRAGMA journal_mode = ' . $journal_mode );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query in SQLite.
|
||||
*
|
||||
* @param string $sql The query to execute.
|
||||
* @param array $params The query parameters.
|
||||
* @throws PDOException When the query execution fails.
|
||||
* @return PDOStatement The PDO statement object.
|
||||
*/
|
||||
public function query( string $sql, array $params = array() ): PDOStatement {
|
||||
if ( $this->query_logger ) {
|
||||
( $this->query_logger )( $sql, $params );
|
||||
}
|
||||
$stmt = $this->pdo->prepare( $sql );
|
||||
$stmt->execute( $params );
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a SQLite query for execution.
|
||||
*
|
||||
* @param string $sql The query to prepare.
|
||||
* @return PDOStatement The prepared statement.
|
||||
* @throws PDOException When the query preparation fails.
|
||||
*/
|
||||
public function prepare( string $sql ): PDOStatement {
|
||||
if ( $this->query_logger ) {
|
||||
( $this->query_logger )( $sql, array() );
|
||||
}
|
||||
return $this->pdo->prepare( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last inserted row.
|
||||
*
|
||||
* @return string The ID of the last inserted row.
|
||||
*/
|
||||
public function get_last_insert_id(): string {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a value for use in a query.
|
||||
*
|
||||
* @param mixed $value The value to quote.
|
||||
* @param int $type The type of the value.
|
||||
* @return string The quoted value.
|
||||
*/
|
||||
public function quote( $value, int $type = PDO::PARAM_STR ): string {
|
||||
return $this->pdo->quote( $value, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote an SQLite identifier.
|
||||
*
|
||||
* Wraps the identifier in backticks and escapes backtick characters within.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Quoted identifiers in SQLite are represented by string constants:
|
||||
*
|
||||
* A string constant is formed by enclosing the string in single quotes (').
|
||||
* A single quote within the string can be encoded by putting two single
|
||||
* quotes in a row - as in Pascal. C-style escapes using the backslash
|
||||
* character are not supported because they are not standard SQL.
|
||||
*
|
||||
* See: https://www.sqlite.org/lang_expr.html#literal_values_constants_
|
||||
*
|
||||
* Although sparsely documented, this applies to backtick and double quoted
|
||||
* string constants as well, so only the quote character needs to be escaped.
|
||||
*
|
||||
* For more details, see the grammar for SQLite table and column names:
|
||||
*
|
||||
* - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/tokenize.c#L395-L419
|
||||
* - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/parse.y#L321-L338
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* We use backtick quotes instead of the SQL standard double quotes, due to
|
||||
* an SQLite quirk causing double-quoted strings to be accepted as literals:
|
||||
*
|
||||
* This misfeature means that a misspelled double-quoted identifier will
|
||||
* be interpreted as a string literal, rather than generating an error.
|
||||
*
|
||||
* See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
|
||||
*
|
||||
* @param string $unquoted_identifier The unquoted identifier value.
|
||||
* @return string The quoted identifier value.
|
||||
*/
|
||||
public function quote_identifier( string $unquoted_identifier ): string {
|
||||
return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDO object.
|
||||
*
|
||||
* @return PDO
|
||||
*/
|
||||
public function get_pdo(): PDO {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a logger for the queries.
|
||||
*
|
||||
* @param callable(string, array): void $logger A query logger callback.
|
||||
*/
|
||||
public function set_query_logger( callable $logger ): void {
|
||||
$this->query_logger = $logger;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
class WP_SQLite_Driver_Exception extends PDOException {
|
||||
/**
|
||||
* The SQLite driver that originated the exception.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver that originated the exception.
|
||||
* @param string $message The exception message.
|
||||
* @param int|string $code The exception code. In PDO, it can be a string with value of SQLSTATE.
|
||||
* @param Throwable|null $previous The previous throwable used for the exception chaining.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_PDO_MySQL_On_SQLite $driver,
|
||||
string $message,
|
||||
$code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct( $message, 0, $previous );
|
||||
$this->code = $code;
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
public function getDriver(): WP_PDO_MySQL_On_SQLite {
|
||||
return $this->driver;
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* For back compatibility with dependencies that use their own loader scripts
|
||||
* (e.g., WP CLI SQLite Command), ensure the new PDO-based classes are loaded.
|
||||
*/
|
||||
require_once __DIR__ . '/class-wp-pdo-mysql-on-sqlite.php';
|
||||
require_once __DIR__ . '/class-wp-pdo-proxy-statement.php';
|
||||
|
||||
/**
|
||||
* Deprecated: A proxy of the WP_PDO_MySQL_On_SQLite class preserving legacy API.
|
||||
*
|
||||
* This is a temporary class to preserve the legacy API for easier transition
|
||||
* to the new PDO-based API, developed in the "WP_PDO_MySQL_On_SQLite" class.
|
||||
*/
|
||||
class WP_SQLite_Driver {
|
||||
/**
|
||||
* The SQLite engine version.
|
||||
*
|
||||
* This is a mysqli-like property that is needed to avoid a PHP warning in
|
||||
* the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
|
||||
* calls "$wpdb->dbh->client_info" - a mysqli-specific abstraction leak.
|
||||
*
|
||||
* @TODO: This should be fixed in WordPress core.
|
||||
*
|
||||
* See:
|
||||
* https://github.com/WordPress/wordpress-develop/blob/bcdca3f9925f1d3eca7b78d231837c0caf0c8c24/src/wp-admin/includes/class-wp-debug-data.php#L1579
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $client_info;
|
||||
|
||||
/**
|
||||
* The MySQL-on-SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $mysql_on_sqlite_driver;
|
||||
|
||||
/**
|
||||
* Results of the last emulated query.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
private $last_result;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Set up an SQLite connection and the MySQL-on-SQLite driver.
|
||||
*
|
||||
* @param WP_SQLite_Connection $connection A SQLite database connection.
|
||||
* @param string $database The database name.
|
||||
*
|
||||
* @throws WP_SQLite_Driver_Exception When the driver initialization fails.
|
||||
*/
|
||||
public function __construct(
|
||||
WP_SQLite_Connection $connection,
|
||||
string $database,
|
||||
int $mysql_version = 80038
|
||||
) {
|
||||
$this->mysql_on_sqlite_driver = new WP_PDO_MySQL_On_SQLite(
|
||||
sprintf( 'mysql-on-sqlite:dbname=%s', $database ),
|
||||
null,
|
||||
null,
|
||||
array(
|
||||
'mysql_version' => $mysql_version,
|
||||
'pdo' => $connection->get_pdo(),
|
||||
)
|
||||
);
|
||||
$this->main_db_name = $database;
|
||||
$this->client_info = $this->mysql_on_sqlite_driver->client_info;
|
||||
|
||||
$connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQLite connection instance.
|
||||
*
|
||||
* @return WP_SQLite_Connection
|
||||
*/
|
||||
public function get_connection(): WP_SQLite_Connection {
|
||||
return $this->mysql_on_sqlite_driver->get_connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the SQLite engine.
|
||||
*
|
||||
* @return string SQLite engine version as a string.
|
||||
*/
|
||||
public function get_sqlite_version(): string {
|
||||
return $this->mysql_on_sqlite_driver->get_sqlite_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQLite driver version saved in the database.
|
||||
*
|
||||
* The saved driver version corresponds to the latest version of the SQLite
|
||||
* driver that was used to initialize and configure the SQLite database.
|
||||
*
|
||||
* @return string SQLite driver version as a string.
|
||||
* @throws PDOException When the query execution fails.
|
||||
*/
|
||||
public function get_saved_driver_version(): string {
|
||||
return $this->mysql_on_sqlite_driver->get_saved_driver_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific SQL mode is active.
|
||||
*
|
||||
* @param string $mode The SQL mode to check.
|
||||
* @return bool True if the SQL mode is active, false otherwise.
|
||||
*/
|
||||
public function is_sql_mode_active( string $mode ): bool {
|
||||
return $this->mysql_on_sqlite_driver->is_sql_mode_active( $mode );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last executed MySQL query.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_last_mysql_query(): ?string {
|
||||
return $this->mysql_on_sqlite_driver->get_last_mysql_query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SQLite queries executed for the last MySQL query.
|
||||
*
|
||||
* @return array{ sql: string, params: array }[]
|
||||
*/
|
||||
public function get_last_sqlite_queries(): array {
|
||||
return $this->mysql_on_sqlite_driver->get_last_sqlite_queries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the auto-increment value generated for the last query.
|
||||
*
|
||||
* @return int|string
|
||||
*/
|
||||
public function get_insert_id() {
|
||||
return $this->mysql_on_sqlite_driver->get_insert_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query Full SQL statement string.
|
||||
* @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ.
|
||||
* @param array ...$fetch_mode_args Additional fetch mode arguments.
|
||||
*
|
||||
* @return mixed Return value, depending on the query type.
|
||||
*
|
||||
* @throws WP_SQLite_Driver_Exception When the query execution fails.
|
||||
*/
|
||||
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
|
||||
$stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args );
|
||||
|
||||
if ( $stmt->columnCount() > 0 ) {
|
||||
$this->last_result = $stmt->fetchAll( $fetch_mode );
|
||||
} else {
|
||||
$this->last_result = $stmt->rowCount();
|
||||
}
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize a MySQL query and initialize a parser.
|
||||
*
|
||||
* @param string $query The MySQL query to parse.
|
||||
* @return WP_MySQL_Parser A parser initialized for the MySQL query.
|
||||
*/
|
||||
public function create_parser( string $query ): WP_MySQL_Parser {
|
||||
return $this->mysql_on_sqlite_driver->create_parser( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get results of the last query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_query_results() {
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get return value of the last query() function call.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_last_return_value() {
|
||||
return $this->last_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of columns returned by the last emulated query.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_last_column_count(): int {
|
||||
return $this->mysql_on_sqlite_driver->get_last_column_count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column metadata for results of the last emulated query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_last_column_meta(): array {
|
||||
return $this->mysql_on_sqlite_driver->get_last_column_meta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query in SQLite.
|
||||
*
|
||||
* @param string $sql The query to execute.
|
||||
* @param array $params The query parameters.
|
||||
* @throws PDOException When the query execution fails.
|
||||
* @return PDOStatement The PDO statement object.
|
||||
*/
|
||||
public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement {
|
||||
return $this->mysql_on_sqlite_driver->execute_sqlite_query( $sql, $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a new transaction or nested transaction.
|
||||
*/
|
||||
public function beginTransaction(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
$this->mysql_on_sqlite_driver->begin_transaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* A temporary alias for back compatibility.
|
||||
*
|
||||
* @see self::beginTransaction()
|
||||
*/
|
||||
public function begin_transaction(): void {
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current transaction or nested transaction.
|
||||
*/
|
||||
public function commit(): void {
|
||||
$this->mysql_on_sqlite_driver->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the current transaction or nested transaction.
|
||||
*/
|
||||
public function rollback(): void {
|
||||
$this->mysql_on_sqlite_driver->rollback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy also the private property "$main_db_name", as it is used in tests.
|
||||
*/
|
||||
public function __set( string $name, $value ): void {
|
||||
if ( 'main_db_name' === $name ) {
|
||||
$closure = function ( string $value ) {
|
||||
$this->main_db_name = $value;
|
||||
};
|
||||
$closure->call( $this->mysql_on_sqlite_driver, $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy also this private method, as it is used in tests.
|
||||
*/
|
||||
private function quote_mysql_utf8_string_literal( string $utf8_literal ): string {
|
||||
$closure = function ( string $utf8_literal ) {
|
||||
return $this->quote_mysql_utf8_string_literal( $utf8_literal );
|
||||
};
|
||||
return $closure->call( $this->mysql_on_sqlite_driver, $utf8_literal );
|
||||
}
|
||||
}
|
||||
+3210
File diff suppressed because it is too large
Load Diff
+152
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SQLite information schema exception.
|
||||
*
|
||||
* This class is used to represent errors that may occur when building
|
||||
* the MySQL information schema for emulation in SQLite.
|
||||
*/
|
||||
class WP_SQLite_Information_Schema_Exception extends Exception {
|
||||
// Information schema exception types.
|
||||
const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name';
|
||||
const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name';
|
||||
const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name';
|
||||
const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found';
|
||||
const TYPE_CONSTRAINT_DOES_NOT_EXIST = 'constraint-does-not-exist';
|
||||
const TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME = 'multiple-constraints-with-name';
|
||||
|
||||
/**
|
||||
* The exception type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* The data to be passed with the exception.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $type The exception type.
|
||||
* @param string $message The exception message.
|
||||
* @param array $data The data to be passed with the exception.
|
||||
* @param Throwable|null $previous The previous throwable used for the exception chaining.
|
||||
*/
|
||||
public function __construct(
|
||||
string $type,
|
||||
string $message,
|
||||
array $data = array(),
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct( $message, 0, $previous );
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the exception.
|
||||
*
|
||||
* @return string The type of the exception.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated with the exception.
|
||||
*
|
||||
* @return array The data associated with the exception.
|
||||
*/
|
||||
public function get_data(): array {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate table name exception.
|
||||
*
|
||||
* @param string $table_name The name of the affected table.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_table_name( string $table_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_TABLE_NAME,
|
||||
sprintf( "Table '%s' already exists.", $table_name ),
|
||||
array( 'table_name' => $table_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate column name exception.
|
||||
*
|
||||
* @param string $column_name The name of the affected column.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_column_name( string $column_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_COLUMN_NAME,
|
||||
sprintf( "Column '%s' already exists.", $column_name ),
|
||||
array( 'column_name' => $column_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate key name exception.
|
||||
*
|
||||
* @param string $key_name The name of the affected key.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function duplicate_key_name( string $key_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_DUPLICATE_KEY_NAME,
|
||||
sprintf( "Key '%s' already exists.", $key_name ),
|
||||
array( 'key_name' => $key_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a key column not found exception.
|
||||
*
|
||||
* @param string $column_name The name of the affected column.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function key_column_not_found( string $column_name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_KEY_COLUMN_NOT_FOUND,
|
||||
sprintf( "Key column '%s' doesn't exist in table.", $column_name ),
|
||||
array( 'column_name' => $column_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a constraint does not exist exception.
|
||||
*
|
||||
* @param string $name The name of the affected constraint.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function constraint_does_not_exist( string $name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_CONSTRAINT_DOES_NOT_EXIST,
|
||||
sprintf( "Constraint '%s' does not exist.", $name ),
|
||||
array( 'name' => $name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a multiple constraints with name exception.
|
||||
*
|
||||
* @param string $name The name of the affected constraint.
|
||||
* @return self The exception instance.
|
||||
*/
|
||||
public static function multiple_constraints_with_name( string $name ): WP_SQLite_Information_Schema_Exception {
|
||||
return new self(
|
||||
self::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME,
|
||||
sprintf( "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", $name ),
|
||||
array( 'name' => $name )
|
||||
);
|
||||
}
|
||||
}
|
||||
+802
@@ -0,0 +1,802 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The SQLite driver uses PDO. Enable PDO function calls:
|
||||
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
|
||||
*/
|
||||
|
||||
/**
|
||||
* SQLite information schema recconstructor for MySQL.
|
||||
*
|
||||
* This class checks and reconstructs the MySQL INFORMATION_SCHEMA data in SQLite
|
||||
* when it becomes out of sync with the actual SQLite database schema.
|
||||
*
|
||||
* Currently, it reconstructs schema infromation for missing tables, and removes
|
||||
* stale data for tables that no longer exist. When used with WordPress, it uses
|
||||
* the "wp_get_db_schema()" function to reconstruct WordPress table information.
|
||||
*/
|
||||
class WP_SQLite_Information_Schema_Reconstructor {
|
||||
/**
|
||||
* The SQLite driver instance.
|
||||
*
|
||||
* @var WP_PDO_MySQL_On_SQLite
|
||||
*/
|
||||
private $driver;
|
||||
|
||||
/**
|
||||
* An instance of the SQLite connection.
|
||||
*
|
||||
* @var WP_SQLite_Connection
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
|
||||
*
|
||||
* @var WP_SQLite_Information_Schema_Builder
|
||||
*/
|
||||
private $schema_builder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WP_PDO_MySQL_On_SQLite $driver The SQLite driver instance.
|
||||
* @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance.
|
||||
*/
|
||||
public function __construct(
|
||||
$driver,
|
||||
WP_SQLite_Information_Schema_Builder $schema_builder
|
||||
) {
|
||||
$this->driver = $driver;
|
||||
$this->connection = $driver->get_connection();
|
||||
$this->schema_builder = $schema_builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the MySQL INFORMATION_SCHEMA data in SQLite is correct.
|
||||
*
|
||||
* This method checks if the MySQL INFORMATION_SCHEMA data in SQLite is correct,
|
||||
* and if it is not, it will reconstruct missing data and remove stale values.
|
||||
*/
|
||||
public function ensure_correct_information_schema(): void {
|
||||
$sqlite_tables = $this->get_sqlite_table_names();
|
||||
$information_schema_tables = $this->get_information_schema_table_names();
|
||||
|
||||
// In WordPress, use "wp_get_db_schema()" to reconstruct WordPress tables.
|
||||
$wp_tables = $this->get_wp_create_table_statements();
|
||||
|
||||
// Reconstruct information schema records for tables that don't have them.
|
||||
foreach ( $sqlite_tables as $table ) {
|
||||
if ( ! in_array( $table, $information_schema_tables, true ) ) {
|
||||
if ( isset( $wp_tables[ $table ] ) ) {
|
||||
// WordPress core table (as returned by "wp_get_db_schema()").
|
||||
$ast = $wp_tables[ $table ];
|
||||
} else {
|
||||
// Other table (a WordPress plugin or unrelated to WordPress).
|
||||
$sql = $this->generate_create_table_statement( $table );
|
||||
$ast = $this->driver->create_parser( $sql )->parse();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* First, let's make sure we clean up all related data. This fixes
|
||||
* partial data corruption, such as when a table record is missing,
|
||||
* but some related column, index, or constraint records are stored.
|
||||
*/
|
||||
$this->record_drop_table( $table );
|
||||
|
||||
$this->schema_builder->record_create_table( $ast );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove information schema records for tables that don't exist.
|
||||
foreach ( $information_schema_tables as $table ) {
|
||||
if ( ! in_array( $table, $sqlite_tables, true ) ) {
|
||||
$this->record_drop_table( $table );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a DROP TABLE statement in the information schema.
|
||||
*
|
||||
* This removes a table record from the information schema, as well as all
|
||||
* column, index, and constraint records that are related to the table.
|
||||
*
|
||||
* @param string $table_name The name of the table to drop.
|
||||
*/
|
||||
private function record_drop_table( string $table_name ): void {
|
||||
$sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote
|
||||
$ast = $this->driver->create_parser( $sql )->parse();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
$this->schema_builder->record_drop_table(
|
||||
$ast->get_first_descendant_node( 'dropStatement' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all existing tables in the SQLite database.
|
||||
*
|
||||
* @return string[] The names of tables in the SQLite database.
|
||||
*/
|
||||
private function get_sqlite_table_names(): array {
|
||||
return $this->driver->execute_sqlite_query(
|
||||
"
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name != ?
|
||||
AND name NOT LIKE ? ESCAPE '\'
|
||||
AND name NOT LIKE ? ESCAPE '\'
|
||||
ORDER BY name
|
||||
",
|
||||
array(
|
||||
'_mysql_data_types_cache',
|
||||
'sqlite\_%',
|
||||
str_replace( '_', '\_', WP_PDO_MySQL_On_SQLite::RESERVED_PREFIX ) . '%',
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all tables recorded in the information schema.
|
||||
*
|
||||
* @return string[] The names of tables in the information schema.
|
||||
*/
|
||||
private function get_information_schema_table_names(): array {
|
||||
$tables_table = $this->schema_builder->get_table_name( false, 'tables' );
|
||||
return $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT table_name FROM %s ORDER BY table_name',
|
||||
$this->connection->quote_identifier( $tables_table )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of parsed CREATE TABLE statements for WordPress tables.
|
||||
*
|
||||
* When reconstructing the information schema data for WordPress tables, we
|
||||
* can use the "wp_get_db_schema()" function to get accurate CREATE TABLE
|
||||
* statements. This method parses the result of "wp_get_db_schema()" into
|
||||
* an array of parsed CREATE TABLE statements indexed by the table names.
|
||||
*
|
||||
* @return array<string, WP_Parser_Node> The WordPress CREATE TABLE statements.
|
||||
*/
|
||||
private function get_wp_create_table_statements(): array {
|
||||
// Bail out when not in a WordPress environment.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/*
|
||||
* In WP CLI, $wpdb may not be set. In that case, we can't load the schema.
|
||||
* We need to bail out and use the standard non-WordPress-specific behavior.
|
||||
*/
|
||||
global $wpdb;
|
||||
if ( ! isset( $wpdb ) ) {
|
||||
// Outside of WP CLI, let's trigger a warning.
|
||||
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
|
||||
trigger_error( 'The $wpdb global is not initialized.', E_USER_WARNING );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
// Ensure the "wp_get_db_schema()" function is defined.
|
||||
if ( file_exists( ABSPATH . 'wp-admin/includes/schema.php' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/schema.php';
|
||||
}
|
||||
if ( ! function_exists( 'wp_get_db_schema' ) ) {
|
||||
throw new Exception( 'The "wp_get_db_schema()" function was not defined.' );
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point, WPDB may not yet be initialized, as we're configuring
|
||||
* the database connection. Let's only populate the table names using
|
||||
* the "$table_prefix" global so we can get correct table names.
|
||||
*/
|
||||
global $table_prefix;
|
||||
$wpdb->set_prefix( $table_prefix );
|
||||
|
||||
// Get schema for global tables.
|
||||
$schema = wp_get_db_schema( 'global' );
|
||||
|
||||
// For multisite installs, add schema definitions for all sites.
|
||||
if ( is_multisite() ) {
|
||||
/*
|
||||
* We need to use a database query over the "get_sites()" function,
|
||||
* as WPDB may not yet initialized. Moreover, we need to get the IDs
|
||||
* of all existing blogs, independent of any filters and actions that
|
||||
* could possibly alter the results of a "get_sites()" call.
|
||||
*/
|
||||
$blog_ids = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'SELECT blog_id FROM %s',
|
||||
$this->connection->quote_identifier( $wpdb->blogs )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_COLUMN );
|
||||
foreach ( $blog_ids as $blog_id ) {
|
||||
$schema .= wp_get_db_schema( 'blog', (int) $blog_id );
|
||||
}
|
||||
} else {
|
||||
// For single site installs, add schema for the main site.
|
||||
$schema .= wp_get_db_schema( 'blog' );
|
||||
}
|
||||
|
||||
// Parse the schema.
|
||||
$parser = $this->driver->create_parser( $schema );
|
||||
$wp_tables = array();
|
||||
while ( $parser->next_query() ) {
|
||||
$ast = $parser->get_query_ast();
|
||||
if ( null === $ast ) {
|
||||
throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' );
|
||||
}
|
||||
|
||||
$create_node = $ast->get_first_descendant_node( 'createStatement' );
|
||||
if ( $create_node && $create_node->has_child_node( 'createTable' ) ) {
|
||||
$name_node = $create_node->get_first_descendant_node( 'tableName' );
|
||||
$name = $this->unquote_mysql_identifier(
|
||||
substr( $schema, $name_node->get_start(), $name_node->get_length() )
|
||||
);
|
||||
|
||||
$wp_tables[ $name ] = $create_node;
|
||||
}
|
||||
}
|
||||
return $wp_tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL CREATE TABLE statement from an SQLite table definition.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @return string The CREATE TABLE statement.
|
||||
*/
|
||||
private function generate_create_table_statement( string $table_name ): string {
|
||||
// Columns.
|
||||
$columns = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA table_xinfo(%s)',
|
||||
$this->connection->quote_identifier( $table_name )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
|
||||
$definitions = array();
|
||||
$column_types = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column['name'] );
|
||||
if ( null === $mysql_type ) {
|
||||
$mysql_type = $this->get_mysql_column_type( $column['type'] );
|
||||
}
|
||||
$definitions[] = $this->generate_column_definition( $table_name, $column );
|
||||
$column_types[ $column['name'] ] = $mysql_type;
|
||||
}
|
||||
|
||||
// Primary key.
|
||||
$pk_columns = array();
|
||||
foreach ( $columns as $column ) {
|
||||
// A position of the column in the primary key, starting from index 1.
|
||||
// A value of 0 means that the column is not part of the primary key.
|
||||
$pk_position = (int) $column['pk'];
|
||||
if ( 0 !== $pk_position ) {
|
||||
$pk_columns[ $pk_position ] = $column['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the columns by their position in the primary key.
|
||||
ksort( $pk_columns );
|
||||
|
||||
if ( count( $pk_columns ) > 0 ) {
|
||||
$quoted_pk_columns = array();
|
||||
foreach ( $pk_columns as $pk_column ) {
|
||||
$quoted_pk_columns[] = $this->connection->quote_identifier( $pk_column );
|
||||
}
|
||||
$definitions[] = sprintf( 'PRIMARY KEY (%s)', implode( ', ', $quoted_pk_columns ) );
|
||||
}
|
||||
|
||||
// Indexes and keys.
|
||||
$keys = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA index_list(%s)',
|
||||
$this->connection->quote_identifier( $table_name )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
// Skip the internal index that SQLite may create for a primary key.
|
||||
// In MySQL, no explicit index needs to be defined for a primary key.
|
||||
if ( 'pk' === $key['origin'] ) {
|
||||
continue;
|
||||
}
|
||||
$definitions[] = $this->generate_key_definition( $table_name, $key, $column_types );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"CREATE TABLE %s (\n %s\n)",
|
||||
$this->connection->quote_identifier( $table_name ),
|
||||
implode( ",\n ", $definitions )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL column definition from an SQLite column information.
|
||||
*
|
||||
* This method generates a MySQL column definition from SQLite column data.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @param array $column_info The SQLite column information.
|
||||
* @return string The MySQL column definition.
|
||||
*/
|
||||
private function generate_column_definition( string $table_name, array $column_info ): string {
|
||||
$definition = array();
|
||||
$definition[] = $this->connection->quote_identifier( $column_info['name'] );
|
||||
|
||||
// Data type.
|
||||
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column_info['name'] );
|
||||
if ( null === $mysql_type ) {
|
||||
$mysql_type = $this->get_mysql_column_type( $column_info['type'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct some column types based on their default values:
|
||||
* 1. In MySQL, non-datetime columns can't have a timestamp default.
|
||||
* Let's use DATETIME when default is set to CURRENT_TIMESTAMP.
|
||||
* 2. In MySQL, TEXT and BLOB columns can't have a default value.
|
||||
* Let's use VARCHAR(65535) and VARBINARY(65535) when default is set.
|
||||
*/
|
||||
$default = $this->generate_column_default( $mysql_type, $column_info['dflt_value'] );
|
||||
if ( 'CURRENT_TIMESTAMP' === $default ) {
|
||||
$mysql_type = 'datetime';
|
||||
} elseif ( 'text' === $mysql_type && null !== $default ) {
|
||||
$mysql_type = 'varchar(65535)';
|
||||
} elseif ( 'blob' === $mysql_type && null !== $default ) {
|
||||
$mysql_type = 'varbinary(65535)';
|
||||
}
|
||||
|
||||
$definition[] = $mysql_type;
|
||||
|
||||
// NULL/NOT NULL.
|
||||
if ( '1' === $column_info['notnull'] ) {
|
||||
$definition[] = 'NOT NULL';
|
||||
}
|
||||
|
||||
// Auto increment.
|
||||
$is_auto_increment = false;
|
||||
if ( '0' !== $column_info['pk'] ) {
|
||||
$is_auto_increment = $this->driver->execute_sqlite_query(
|
||||
'SELECT 1 FROM sqlite_master WHERE tbl_name = ? AND sql LIKE ?',
|
||||
array( $table_name, '%AUTOINCREMENT%' )
|
||||
)->fetchColumn();
|
||||
|
||||
if ( $is_auto_increment ) {
|
||||
$definition[] = 'AUTO_INCREMENT';
|
||||
}
|
||||
}
|
||||
|
||||
// Default value.
|
||||
if ( null !== $default && ! $is_auto_increment ) {
|
||||
$definition[] = 'DEFAULT ' . $default;
|
||||
}
|
||||
|
||||
return implode( ' ', $definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL key definition from an SQLite key information.
|
||||
*
|
||||
* This method generates a MySQL key definition from SQLite key data.
|
||||
*
|
||||
* @param string $table_name The name of the table.
|
||||
* @param array $key_info The SQLite key information.
|
||||
* @param array $column_types The MySQL data types of the columns.
|
||||
* @return string The MySQL key definition.
|
||||
*/
|
||||
private function generate_key_definition( string $table_name, array $key_info, array $column_types ): string {
|
||||
$definition = array();
|
||||
|
||||
// Key type.
|
||||
$cached_type = $this->get_cached_mysql_data_type( $table_name, $key_info['name'] );
|
||||
if ( 'FULLTEXT' === $cached_type ) {
|
||||
$definition[] = 'FULLTEXT KEY';
|
||||
} elseif ( 'SPATIAL' === $cached_type ) {
|
||||
$definition[] = 'SPATIAL KEY';
|
||||
} elseif ( 'UNIQUE' === $cached_type || '1' === $key_info['unique'] ) {
|
||||
$definition[] = 'UNIQUE KEY';
|
||||
} else {
|
||||
$definition[] = 'KEY';
|
||||
}
|
||||
|
||||
// Key name.
|
||||
$name = $key_info['name'];
|
||||
|
||||
/*
|
||||
* The SQLite driver prefixes index names with "{$table_name}__" to avoid
|
||||
* naming conflicts among tables in SQLite. We need to remove the prefix.
|
||||
*/
|
||||
if ( str_starts_with( $name, "{$table_name}__" ) ) {
|
||||
$name = substr( $name, strlen( "{$table_name}__" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite creates automatic internal indexes for primary and unique keys,
|
||||
* naming them in format "sqlite_autoindex_{$table_name}_{$index_id}".
|
||||
* For these internal indexes, we need to skip their name, so that in
|
||||
* the generated MySQL definition, they follow implicit MySQL naming.
|
||||
*/
|
||||
if ( ! str_starts_with( $name, 'sqlite_autoindex_' ) ) {
|
||||
$definition[] = $this->connection->quote_identifier( $name );
|
||||
}
|
||||
|
||||
// Key columns.
|
||||
$key_columns = $this->driver->execute_sqlite_query(
|
||||
sprintf(
|
||||
'PRAGMA index_info(%s)',
|
||||
$this->connection->quote_identifier( $key_info['name'] )
|
||||
)
|
||||
)->fetchAll( PDO::FETCH_ASSOC );
|
||||
$cols = array();
|
||||
foreach ( $key_columns as $column ) {
|
||||
/*
|
||||
* Extract type and length from column data type definition.
|
||||
*
|
||||
* This is required when the column data type is inferred from the
|
||||
* '_mysql_data_types_cache' table, which stores the data type in
|
||||
* the format "type(length)", such as "varchar(255)".
|
||||
*/
|
||||
$max_prefix_length = 100;
|
||||
$type = strtolower( $column_types[ $column['name'] ] );
|
||||
$parts = explode( '(', $type );
|
||||
$column_type = $parts[0];
|
||||
$column_length = isset( $parts[1] ) ? (int) $parts[1] : null;
|
||||
|
||||
/*
|
||||
* Add an index column prefix length, if needed.
|
||||
*
|
||||
* This is required for "text" and "blob" types for columns inferred
|
||||
* directly from the SQLite schema, and for the following types for
|
||||
* columns inferred from the '_mysql_data_types_cache' table:
|
||||
* char, varchar
|
||||
* text, tinytext, mediumtext, longtext
|
||||
* blob, tinyblob, mediumblob, longblob
|
||||
* varbinary
|
||||
*/
|
||||
if (
|
||||
str_ends_with( $column_type, 'char' )
|
||||
|| str_ends_with( $column_type, 'text' )
|
||||
|| str_ends_with( $column_type, 'blob' )
|
||||
|| str_starts_with( $column_type, 'var' )
|
||||
) {
|
||||
$cols[] = sprintf(
|
||||
'%s(%d)',
|
||||
$this->connection->quote_identifier( $column['name'] ),
|
||||
min( $column_length ?? $max_prefix_length, $max_prefix_length )
|
||||
);
|
||||
} else {
|
||||
$cols[] = $this->connection->quote_identifier( $column['name'] );
|
||||
}
|
||||
}
|
||||
|
||||
$definition[] = '(' . implode( ', ', $cols ) . ')';
|
||||
return implode( ' ', $definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a MySQL default value from an SQLite default value.
|
||||
*
|
||||
* @param string $mysql_type The MySQL data type of the column.
|
||||
* @param string|null $default_value The default value of the SQLite column.
|
||||
* @return string|null The default value, or null if the column has no default value.
|
||||
*/
|
||||
private function generate_column_default( string $mysql_type, ?string $default_value ): ?string {
|
||||
if ( null === $default_value || '' === $default_value ) {
|
||||
return null;
|
||||
}
|
||||
$mysql_type = strtolower( $mysql_type );
|
||||
|
||||
if ( str_starts_with( $mysql_type, 'bit' ) ) {
|
||||
// BIT columns are stored as INTEGER in SQLite.
|
||||
return "b'" . decbin( (int) $default_value ) . "'";
|
||||
}
|
||||
|
||||
/*
|
||||
* In MySQL, geometry columns can't have a default value.
|
||||
*
|
||||
* Geometry columns are saved as TEXT in SQLite, and in an older version
|
||||
* of the SQLite driver, TEXT columns were assigned a default value of ''.
|
||||
*/
|
||||
if ( 'geomcollection' === $mysql_type || 'geometrycollection' === $mysql_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* In MySQL, date/time columns can't have a default value of ''.
|
||||
*
|
||||
* Date/time columns are saved as TEXT in SQLite, and in an older version
|
||||
* of the SQLite driver, TEXT columns were assigned a default value of ''.
|
||||
*/
|
||||
if (
|
||||
"''" === $default_value
|
||||
&& in_array( $mysql_type, array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SQLite default values to MySQL default values.
|
||||
*
|
||||
* See:
|
||||
* - https://www.sqlite.org/syntax/column-constraint.html
|
||||
* - https://www.sqlite.org/syntax/literal-value.html
|
||||
* - https://www.sqlite.org/lang_expr.html#literal_values_constants_
|
||||
*/
|
||||
|
||||
// Quoted string literal. E.g.: 'abc', "abc", `abc`
|
||||
$first_byte = $default_value[0] ?? null;
|
||||
if ( '"' === $first_byte || "'" === $first_byte || '`' === $first_byte ) {
|
||||
$value = substr( $default_value, 1, -1 );
|
||||
$value = str_replace( $first_byte . $first_byte, $first_byte, $value );
|
||||
return $this->quote_mysql_utf8_string_literal( $value );
|
||||
}
|
||||
|
||||
// Normalize the default value for easier comparison.
|
||||
$uppercase_default_value = strtoupper( $default_value );
|
||||
|
||||
// NULL, TRUE, FALSE.
|
||||
if ( 'NULL' === $uppercase_default_value ) {
|
||||
// DEFAULT NULL is the same as no default value.
|
||||
return null;
|
||||
} elseif ( 'TRUE' === $uppercase_default_value ) {
|
||||
return '1';
|
||||
} elseif ( 'FALSE' === $uppercase_default_value ) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Date/time values.
|
||||
if ( 'CURRENT_TIMESTAMP' === $uppercase_default_value ) {
|
||||
return 'CURRENT_TIMESTAMP';
|
||||
} elseif ( 'CURRENT_DATE' === $uppercase_default_value ) {
|
||||
return null; // Not supported in MySQL.
|
||||
} elseif ( 'CURRENT_TIME' === $uppercase_default_value ) {
|
||||
return null; // Not supported in MySQL.
|
||||
}
|
||||
|
||||
// SQLite supports underscores in all numeric literals.
|
||||
$no_underscore_default_value = str_replace( '_', '', $default_value );
|
||||
|
||||
// Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3
|
||||
if ( is_numeric( $no_underscore_default_value ) ) {
|
||||
return $no_underscore_default_value;
|
||||
}
|
||||
|
||||
// HEX literals (numeric). E.g.: 0x1a2f, 0X1A2F
|
||||
$value = filter_var( $no_underscore_default_value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX );
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// BLOB literals (string). E.g.: x'1a2f', X'1A2F'
|
||||
// Checking the prefix is enough as SQLite doesn't allow malformed values.
|
||||
if ( str_starts_with( $uppercase_default_value, "X'" ) ) {
|
||||
// Convert the hex string to ASCII bytes.
|
||||
return "'" . pack( 'H*', substr( $default_value, 2, -1 ) ) . "'";
|
||||
}
|
||||
|
||||
// Unquoted string literal. E.g.: abc
|
||||
return $this->quote_mysql_utf8_string_literal( $default_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MySQL column or index data type from legacy data types cache table.
|
||||
*
|
||||
* This method retrieves MySQL column or index data types from a special table
|
||||
* that was used by an old version of the SQLite driver and that is otherwise
|
||||
* no longer needed. This is more precise than direct inference from SQLite.
|
||||
*
|
||||
* For columns, it returns full column type, including prefix length, e.g.:
|
||||
* int(11), bigint(20) unsigned, varchar(255), longtext
|
||||
*
|
||||
* For indexes, it returns one of:
|
||||
* KEY, PRIMARY, UNIQUE, FULLTEXT, SPATIAL
|
||||
*
|
||||
* @param string $table_name The table name.
|
||||
* @param string $column_or_index_name The column or index name.
|
||||
* @return string|null The MySQL definition, or null when not found.
|
||||
*/
|
||||
private function get_cached_mysql_data_type( string $table_name, string $column_or_index_name ): ?string {
|
||||
try {
|
||||
$mysql_type = $this->driver->execute_sqlite_query(
|
||||
'SELECT mysql_type FROM _mysql_data_types_cache
|
||||
WHERE `table` = ? COLLATE NOCASE
|
||||
AND (
|
||||
-- The old SQLite driver stored the MySQL data types in multiple
|
||||
-- formats - lowercase, uppercase, and, sometimes, with backticks.
|
||||
column_or_index = ? COLLATE NOCASE
|
||||
OR column_or_index = ? COLLATE NOCASE
|
||||
)',
|
||||
array( $table_name, $column_or_index_name, "`$column_or_index_name`" )
|
||||
)->fetchColumn();
|
||||
} catch ( PDOException $e ) {
|
||||
if ( str_contains( $e->getMessage(), 'no such table' ) ) {
|
||||
return null;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
if ( false === $mysql_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the stored type value is a valid MySQL column type.
|
||||
*
|
||||
* Some older versions of the legacy SQLite driver might have stored
|
||||
* invalid MySQL column types in some scenarios:
|
||||
*
|
||||
* 1. Before https://github.com/WordPress/sqlite-database-integration/pull/126,
|
||||
* the legacy SQLite driver incorrectly stored MySQL column types
|
||||
* for columns with multiple type arguments.
|
||||
*
|
||||
* E.g., a column definition like "col_name decimal(26, 8)" would
|
||||
* be stored with invalid type "decimal(26,".
|
||||
*
|
||||
* 2. Before https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee
|
||||
* (see also https://github.com/Automattic/sqlite-database-integration/pull/2),
|
||||
* the legacy SQLite driver incorrectly recognized indexes on columns
|
||||
* with type keywords as additional table column definitions.
|
||||
*
|
||||
* E.g., an index definition like "KEY timestamp (timestamp)" would
|
||||
* be stored as column "KEY" with invalid type "timestamp(timestamp)".
|
||||
*
|
||||
* To address these issues, we need to check whether the stored type looks
|
||||
* like a valid MySQL column type definition.
|
||||
*/
|
||||
$open_par_index = strpos( $mysql_type, '(' );
|
||||
$close_par_index = strpos( $mysql_type, ')' );
|
||||
if ( false !== $open_par_index ) {
|
||||
$end = false !== $close_par_index ? $close_par_index : strlen( $mysql_type );
|
||||
$parts = explode( '(', substr( $mysql_type, 0, $end ) );
|
||||
$type = strtolower( trim( $parts[0] ) );
|
||||
$args = array();
|
||||
foreach ( explode( ',', $parts[1] ) as $arg ) {
|
||||
$args[] = strtolower( trim( $arg ) );
|
||||
}
|
||||
|
||||
// WooCommerce uses decimal(26,8), decimal(19,4), and decimal(3,2)
|
||||
// column types, so we can can fix the invalid column definitions.
|
||||
$looks_like_wc_table = str_contains( $table_name, 'wc_' ) || str_contains( $table_name, 'woocommerce_' );
|
||||
$is_invalid_decimal = 'decimal' === $type && count( $args ) === 2 && '' === $args[1];
|
||||
if ( $looks_like_wc_table && $is_invalid_decimal ) {
|
||||
if ( '26' === $args[0] ) {
|
||||
// Fix "decimal(26,".
|
||||
return 'decimal(26,8)';
|
||||
} elseif ( '19' === $args[0] ) {
|
||||
// Fix "decimal(19,".
|
||||
return 'decimal(19,4)';
|
||||
} elseif ( '3' === $args[0] ) {
|
||||
// Fix "decimal(3,".
|
||||
return 'decimal(3,2)';
|
||||
}
|
||||
}
|
||||
|
||||
// Only numeric arguments are allowed for MySQL column types.
|
||||
// This handles the incorrectly stored index definition case.
|
||||
foreach ( $args as $arg ) {
|
||||
if ( ! is_numeric( $arg ) ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no closing parenthesis, the type is invalid.
|
||||
if ( false === $close_par_index ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize index type for backward compatibility. Some older versions
|
||||
// of the SQLite driver stored index types with a " KEY" suffix, e.g.,
|
||||
// "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix.
|
||||
if ( str_ends_with( $mysql_type, ' KEY' ) ) {
|
||||
$mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) );
|
||||
}
|
||||
return $mysql_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MySQL column type from an SQLite column type.
|
||||
*
|
||||
* This method converts an SQLite column type to a MySQL column type as per
|
||||
* the SQLite column type affinity rules:
|
||||
* https://sqlite.org/datatype3.html#determination_of_column_affinity
|
||||
*
|
||||
* @param string $column_type The SQLite column type.
|
||||
* @return string The MySQL column type.
|
||||
*/
|
||||
private function get_mysql_column_type( string $column_type ): string {
|
||||
$type = strtoupper( $column_type );
|
||||
|
||||
/*
|
||||
* Following the rules of column affinity:
|
||||
* https://sqlite.org/datatype3.html#determination_of_column_affinity
|
||||
*/
|
||||
|
||||
// 1. If the declared type contains the string "INT" then it is assigned
|
||||
// INTEGER affinity.
|
||||
if ( str_contains( $type, 'INT' ) ) {
|
||||
return 'int';
|
||||
}
|
||||
|
||||
// 2. If the declared type of the column contains any of the strings
|
||||
// "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity.
|
||||
if ( str_contains( $type, 'TEXT' ) || str_contains( $type, 'CHAR' ) || str_contains( $type, 'CLOB' ) ) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
// 3. If the declared type for a column contains the string "BLOB" or
|
||||
// if no type is specified then the column has affinity BLOB.
|
||||
if ( str_contains( $type, 'BLOB' ) || '' === $type ) {
|
||||
return 'blob';
|
||||
}
|
||||
|
||||
// 4. If the declared type for a column contains any of the strings
|
||||
// "REAL", "FLOA", or "DOUB" then the column has REAL affinity.
|
||||
if ( str_contains( $type, 'REAL' ) || str_contains( $type, 'FLOA' ) ) {
|
||||
return 'float';
|
||||
}
|
||||
if ( str_contains( $type, 'DOUB' ) ) {
|
||||
return 'double';
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. Otherwise, the affinity is NUMERIC.
|
||||
*
|
||||
* While SQLite defaults to a NUMERIC column affinity, it's better to use
|
||||
* TEXT in this case, because numeric SQLite columns in non-strict tables
|
||||
* can contain any text data as well, when it is not a well-formed number.
|
||||
*
|
||||
* See: https://sqlite.org/datatype3.html#type_affinity
|
||||
*/
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a MySQL UTF-8 string literal for output in a CREATE TABLE statement.
|
||||
*
|
||||
* See WP_PDO_MySQL_On_SQLite::quote_mysql_utf8_string_literal().
|
||||
*
|
||||
* TODO: This is a copy of WP_PDO_MySQL_On_SQLite::quote_mysql_utf8_string_literal().
|
||||
* We may consider extracing it to reusable MySQL helpers.
|
||||
*
|
||||
* @param string $utf8_literal The UTF-8 string literal to escape.
|
||||
* @return string The escaped string literal.
|
||||
*/
|
||||
private function quote_mysql_utf8_string_literal( string $utf8_literal ): string {
|
||||
$backslash = chr( 92 );
|
||||
$replacements = array(
|
||||
"'" => "''", // A single quote character (').
|
||||
$backslash => $backslash . $backslash, // A backslash character (\).
|
||||
chr( 0 ) => $backslash . '0', // An ASCII NULL character (\0).
|
||||
chr( 10 ) => $backslash . 'n', // A newline (linefeed) character (\n).
|
||||
chr( 13 ) => $backslash . 'r', // A carriage return character (\r).
|
||||
);
|
||||
return "'" . strtr( $utf8_literal, $replacements ) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquote a quoted MySQL identifier.
|
||||
*
|
||||
* Remove bounding quotes and replace escaped quotes with their values.
|
||||
*
|
||||
* @param string $quoted_identifier The quoted identifier value.
|
||||
* @return string The unquoted identifier value.
|
||||
*/
|
||||
private function unquote_mysql_identifier( string $quoted_identifier ): string {
|
||||
$first_byte = $quoted_identifier[0] ?? null;
|
||||
if ( '"' === $first_byte || '`' === $first_byte ) {
|
||||
$unquoted = substr( $quoted_identifier, 1, -1 );
|
||||
return str_replace( $first_byte . $first_byte, $first_byte, $unquoted );
|
||||
}
|
||||
return $quoted_identifier;
|
||||
}
|
||||
}
|
||||
+987
@@ -0,0 +1,987 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom functions for the SQLite implementation.
|
||||
*
|
||||
* @package wp-sqlite-integration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class defines user defined functions(UDFs) for PDO library.
|
||||
*
|
||||
* These functions replace those used in the SQL statement with the PHP functions.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <code>
|
||||
* new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj);
|
||||
* </code>
|
||||
*
|
||||
* This automatically enables ref_to_pdo_obj to replace the function in the SQL statement
|
||||
* to the ones defined here.
|
||||
*/
|
||||
class WP_SQLite_PDO_User_Defined_Functions {
|
||||
|
||||
/**
|
||||
* Registers the user defined functions for SQLite to a PDO instance.
|
||||
* The functions are registered using PDO::sqliteCreateFunction().
|
||||
*
|
||||
* @param PDO|PDO\SQLite $pdo The PDO object.
|
||||
*/
|
||||
public static function register_for( $pdo ): self {
|
||||
$instance = new self();
|
||||
foreach ( $instance->functions as $f => $t ) {
|
||||
if ( $pdo instanceof PDO\SQLite ) {
|
||||
$pdo->createFunction( $f, array( $instance, $t ) );
|
||||
} else {
|
||||
$pdo->sqliteCreateFunction( $f, array( $instance, $t ) );
|
||||
}
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array to define MySQL function => function defined with PHP.
|
||||
*
|
||||
* Replaced functions must be public.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $functions = array(
|
||||
'throw' => 'throw',
|
||||
'month' => 'month',
|
||||
'monthnum' => 'month',
|
||||
'year' => 'year',
|
||||
'day' => 'day',
|
||||
'hour' => 'hour',
|
||||
'minute' => 'minute',
|
||||
'second' => 'second',
|
||||
'week' => 'week',
|
||||
'weekday' => 'weekday',
|
||||
'dayofweek' => 'dayofweek',
|
||||
'dayofmonth' => 'dayofmonth',
|
||||
'unix_timestamp' => 'unix_timestamp',
|
||||
'now' => 'now',
|
||||
'md5' => 'md5',
|
||||
'curdate' => 'curdate',
|
||||
'rand' => 'rand',
|
||||
'from_unixtime' => 'from_unixtime',
|
||||
'localtime' => 'now',
|
||||
'localtimestamp' => 'now',
|
||||
'isnull' => 'isnull',
|
||||
'if' => '_if',
|
||||
'regexp' => 'regexp',
|
||||
'field' => 'field',
|
||||
'log' => 'log',
|
||||
'least' => 'least',
|
||||
'greatest' => 'greatest',
|
||||
'get_lock' => 'get_lock',
|
||||
'release_lock' => 'release_lock',
|
||||
'ucase' => 'ucase',
|
||||
'lcase' => 'lcase',
|
||||
'unhex' => 'unhex',
|
||||
'from_base64' => 'from_base64',
|
||||
'to_base64' => 'to_base64',
|
||||
'inet_ntoa' => 'inet_ntoa',
|
||||
'inet_aton' => 'inet_aton',
|
||||
'datediff' => 'datediff',
|
||||
'locate' => 'locate',
|
||||
'utc_date' => 'utc_date',
|
||||
'utc_time' => 'utc_time',
|
||||
'utc_timestamp' => 'utc_timestamp',
|
||||
'version' => 'version',
|
||||
|
||||
// Internal helper functions.
|
||||
'_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.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
*
|
||||
* @throws Exception The error message.
|
||||
* @return void
|
||||
*/
|
||||
public function throw( $message ): void {
|
||||
throw new Exception( $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return the unix timestamp.
|
||||
*
|
||||
* Used without an argument, it returns PHP time() function (total seconds passed
|
||||
* from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value
|
||||
* to the timestamp.
|
||||
*
|
||||
* @param string $field Representing the date formatted as '0000-00-00 00:00:00'.
|
||||
*
|
||||
* @return number of unsigned integer
|
||||
*/
|
||||
public function unix_timestamp( $field = null ) {
|
||||
return is_null( $field ) ? time() : strtotime( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL FROM_UNIXTIME() function.
|
||||
*
|
||||
* @param int $field The unix timestamp.
|
||||
* @param string $format Indicate the way of formatting(optional).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function from_unixtime( $field, $format = null ) {
|
||||
// Convert to ISO time.
|
||||
$date = gmdate( 'Y-m-d H:i:s', $field );
|
||||
|
||||
return is_null( $format ) ? $date : $this->dateformat( $date, $format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL NOW() function.
|
||||
*
|
||||
* @return string representing current time formatted as '0000-00-00 00:00:00'.
|
||||
*/
|
||||
public function now() {
|
||||
return gmdate( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL CURDATE() function.
|
||||
*
|
||||
* @return string representing current time formatted as '0000-00-00'.
|
||||
*/
|
||||
public function curdate() {
|
||||
return gmdate( 'Y-m-d' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL MD5() function.
|
||||
*
|
||||
* @param string $field The string to be hashed.
|
||||
*
|
||||
* @return string of the md5 hash value of the argument.
|
||||
*/
|
||||
public function md5( $field ) {
|
||||
return md5( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL's seeded RAND(N) function.
|
||||
*
|
||||
* Implements MySQL's deterministic LCG (Linear Congruential Generator),
|
||||
* producing bit-exact output for a given seed.
|
||||
*
|
||||
* Known divergences from MySQL:
|
||||
*
|
||||
* 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( $seed ) {
|
||||
// Requires 64-bit PHP. Seed * 0x10000001 can exceed PHP_INT_MAX on 32-bit.
|
||||
$max_value = 0x3FFFFFFF;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DATEFORMAT() function.
|
||||
*
|
||||
* @param string $date Formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'.
|
||||
* @param string $format The string format.
|
||||
*
|
||||
* @return string formatted according to $format
|
||||
*/
|
||||
public function dateformat( $date, $format ) {
|
||||
$mysql_php_date_formats = array(
|
||||
'%a' => 'D',
|
||||
'%b' => 'M',
|
||||
'%c' => 'n',
|
||||
'%D' => 'jS',
|
||||
'%d' => 'd',
|
||||
'%e' => 'j',
|
||||
'%H' => 'H',
|
||||
'%h' => 'h',
|
||||
'%I' => 'h',
|
||||
'%i' => 'i',
|
||||
'%j' => 'z',
|
||||
'%k' => 'G',
|
||||
'%l' => 'g',
|
||||
'%M' => 'F',
|
||||
'%m' => 'm',
|
||||
'%p' => 'A',
|
||||
'%r' => 'h:i:s A',
|
||||
'%S' => 's',
|
||||
'%s' => 's',
|
||||
'%T' => 'H:i:s',
|
||||
'%U' => 'W',
|
||||
'%u' => 'W',
|
||||
'%V' => 'W',
|
||||
'%v' => 'W',
|
||||
'%W' => 'l',
|
||||
'%w' => 'w',
|
||||
'%X' => 'Y',
|
||||
'%x' => 'o',
|
||||
'%Y' => 'Y',
|
||||
'%y' => 'y',
|
||||
);
|
||||
|
||||
$time = strtotime( $date );
|
||||
$format = strtr( $format, $mysql_php_date_formats );
|
||||
|
||||
return gmdate( $format, $time );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the month value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the month between 1 and 12.
|
||||
*/
|
||||
public function month( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for MONTH('0000-00-00') and for dates with
|
||||
* zero month parts like '2020-00-15'. PHP's strtotime() can't
|
||||
* parse these, so we extract the month directly from the string.
|
||||
*/
|
||||
if ( preg_match( '/^\d{4}-(\d{2})/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* n - Numeric representation of a month, without leading zeros.
|
||||
* 1 through 12
|
||||
*/
|
||||
return intval( gmdate( 'n', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the year value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the year.
|
||||
*/
|
||||
public function year( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for YEAR('0000-00-00'). PHP's strtotime()
|
||||
* can't parse zero dates, so we extract the year directly.
|
||||
*/
|
||||
if ( preg_match( '/^(\d{4})-\d{2}/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* Y - A full numeric representation of a year, 4 digits.
|
||||
*/
|
||||
return intval( gmdate( 'Y', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to extract the day value from the date.
|
||||
*
|
||||
* @param string $field Representing the date formatted as 0000-00-00.
|
||||
*
|
||||
* @return string Representing the number of the day of the month from 1 and 31.
|
||||
*/
|
||||
public function day( $field ) {
|
||||
/*
|
||||
* MySQL returns 0 for DAY('0000-00-00') and for dates with
|
||||
* zero day parts like '2020-01-00'. PHP's strtotime() can't
|
||||
* parse these, so we extract the day directly from the string.
|
||||
*/
|
||||
if ( preg_match( '/^\d{4}-\d{2}-(\d{2})/', $field, $matches ) ) {
|
||||
return intval( $matches[1] );
|
||||
}
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* j - Day of the month without leading zeros.
|
||||
* 1 to 31.
|
||||
*/
|
||||
return intval( gmdate( 'j', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL SECOND() function.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
*
|
||||
* @param string $field Representing the time formatted as '00:00:00'.
|
||||
*
|
||||
* @return number Unsigned integer
|
||||
*/
|
||||
public function second( $field ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* s - Seconds, with leading zeros (00 to 59)
|
||||
*/
|
||||
return intval( gmdate( 's', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL MINUTE() function.
|
||||
*
|
||||
* @param string $field Representing the time formatted as '00:00:00'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function minute( $field ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* i - Minutes with leading zeros.
|
||||
* 00 to 59.
|
||||
*/
|
||||
return intval( gmdate( 'i', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL HOUR() function.
|
||||
*
|
||||
* Returns the hour for time, in 24-hour format, from 0 to 23.
|
||||
* Importantly, midnight is 0, not 24.
|
||||
*
|
||||
* @param string $time Representing the time formatted, like '14:08:12'.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function hour( $time ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* H 24-hour format of an hour with leading zeros.
|
||||
* 00 through 23.
|
||||
*/
|
||||
return intval( gmdate( 'H', strtotime( $time ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Covers MySQL WEEK() function.
|
||||
*
|
||||
* Always assumes $mode = 1.
|
||||
*
|
||||
* @TODO: Support other modes.
|
||||
*
|
||||
* From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week:
|
||||
*
|
||||
* > Returns the week number for date. The two-argument form of WEEK()
|
||||
* > enables you to specify whether the week starts on Sunday or Monday
|
||||
* > and whether the return value should be in the range from 0 to 53
|
||||
* > or from 1 to 53. If the mode argument is omitted, the value of the
|
||||
* > default_week_format system variable is used.
|
||||
* >
|
||||
* > The following table describes how the mode argument works:
|
||||
* >
|
||||
* > Mode First day of week Range Week 1 is the first week …
|
||||
* > 0 Sunday 0-53 with a Sunday in this year
|
||||
* > 1 Monday 0-53 with 4 or more days this year
|
||||
* > 2 Sunday 1-53 with a Sunday in this year
|
||||
* > 3 Monday 1-53 with 4 or more days this year
|
||||
* > 4 Sunday 0-53 with 4 or more days this year
|
||||
* > 5 Monday 0-53 with a Monday in this year
|
||||
* > 6 Sunday 1-53 with 4 or more days this year
|
||||
* > 7 Monday 1-53 with a Monday in this year
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
* @param int $mode The mode argument.
|
||||
*/
|
||||
public function week( $field, $mode ) {
|
||||
/*
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* W - ISO-8601 week number of year, weeks starting on Monday.
|
||||
* Example: 42 (the 42nd week in the year)
|
||||
*
|
||||
* Week 1 is the first week with a Thursday in it.
|
||||
*/
|
||||
return intval( gmdate( 'W', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates WEEKDAY() function in MySQL.
|
||||
*
|
||||
* Returns the day of the week as an integer.
|
||||
* The days of the week are numbered 0 to 6:
|
||||
* * 0 for Monday
|
||||
* * 1 for Tuesday
|
||||
* * 2 for Wednesday
|
||||
* * 3 for Thursday
|
||||
* * 4 for Friday
|
||||
* * 5 for Saturday
|
||||
* * 6 for Sunday
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function weekday( $field ) {
|
||||
/*
|
||||
* date('N') returns 1 (for Monday) through 7 (for Sunday)
|
||||
* That's one more than MySQL.
|
||||
* Let's subtract one to make it compatible.
|
||||
*/
|
||||
return intval( gmdate( 'N', strtotime( $field ) ) ) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DAYOFMONTH() function.
|
||||
*
|
||||
* @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int Returns the day of the month for date as a number in the range 1 to 31.
|
||||
*/
|
||||
public function dayofmonth( $field ) {
|
||||
return intval( gmdate( 'j', strtotime( $field ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DAYOFWEEK() function.
|
||||
*
|
||||
* > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
|
||||
* > These index values correspond to the ODBC standard. Returns NULL if date is NULL.
|
||||
*
|
||||
* @param string $field Representing the date.
|
||||
*
|
||||
* @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
|
||||
*/
|
||||
public function dayofweek( $field ) {
|
||||
/**
|
||||
* From https://www.php.net/manual/en/datetime.format.php:
|
||||
*
|
||||
* `w` – Numeric representation of the day of the week
|
||||
* 0 (for Sunday) through 6 (for Saturday)
|
||||
*/
|
||||
return intval( gmdate( 'w', strtotime( $field ) ) ) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DATE() function.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
*
|
||||
* @param string $date formatted as unix time.
|
||||
*
|
||||
* @return string formatted as '0000-00-00'.
|
||||
*/
|
||||
public function date( $date ) {
|
||||
return gmdate( 'Y-m-d', strtotime( $date ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL ISNULL() function.
|
||||
*
|
||||
* This function returns true if the argument is null, and true if not.
|
||||
*
|
||||
* @param mixed $field The field to be tested.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isnull( $field ) {
|
||||
return is_null( $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL IF() function.
|
||||
*
|
||||
* As 'IF' is a reserved word for PHP, function name must be changed.
|
||||
*
|
||||
* @param mixed $expression The statement to be evaluated as true or false.
|
||||
* @param mixed $truthy Statement or value returned if $expression is true.
|
||||
* @param mixed $falsy Statement or value returned if $expression is false.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _if( $expression, $truthy, $falsy ) {
|
||||
return ( true === $expression ) ? $truthy : $falsy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL REGEXP() function.
|
||||
*
|
||||
* @param string $pattern Regular expression to match.
|
||||
* @param string $field Haystack.
|
||||
*
|
||||
* @return integer 1 if matched, 0 if not matched.
|
||||
*/
|
||||
public function regexp( $pattern, $field ) {
|
||||
/*
|
||||
* If the original query says REGEXP BINARY
|
||||
* the comparison is byte-by-byte and letter casing now
|
||||
* matters since lower- and upper-case letters have different
|
||||
* byte codes.
|
||||
*
|
||||
* The REGEXP function can't be easily made to accept two
|
||||
* parameters, so we'll have to use a hack to get around this.
|
||||
*
|
||||
* If the first character of the pattern is a null byte, we'll
|
||||
* remove it and make the comparison case-sensitive. This should
|
||||
* be reasonably safe since PHP does not allow null bytes in
|
||||
* regular expressions anyway.
|
||||
*/
|
||||
if ( "\x00" === $pattern[0] ) {
|
||||
$pattern = substr( $pattern, 1 );
|
||||
$flags = '';
|
||||
} else {
|
||||
// Otherwise, the search is case-insensitive.
|
||||
$flags = 'i';
|
||||
}
|
||||
$pattern = str_replace( '/', '\/', $pattern );
|
||||
$pattern = '/' . $pattern . '/' . $flags;
|
||||
|
||||
return preg_match( $pattern, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL FIELD() function.
|
||||
*
|
||||
* This function gets the list argument and compares the first item to all the others.
|
||||
* If the same value is found, it returns the position of that value. If not, it
|
||||
* returns 0.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function field() {
|
||||
$num_args = func_num_args();
|
||||
if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) {
|
||||
return 0;
|
||||
}
|
||||
$arg_list = func_get_args();
|
||||
$search_string = strtolower( array_shift( $arg_list ) );
|
||||
|
||||
for ( $i = 0; $i < $num_args - 1; $i++ ) {
|
||||
if ( strtolower( $arg_list[ $i ] ) === $search_string ) {
|
||||
return $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL LOG() function.
|
||||
*
|
||||
* Used with one argument, it returns the natural logarithm of X.
|
||||
* <code>
|
||||
* LOG(X)
|
||||
* </code>
|
||||
* Used with two arguments, it returns the natural logarithm of X base B.
|
||||
* <code>
|
||||
* LOG(B, X)
|
||||
* </code>
|
||||
* In this case, it returns the value of log(X) / log(B).
|
||||
*
|
||||
* Used without an argument, it returns false. This returned value will be
|
||||
* rewritten to 0, because SQLite doesn't understand true/false value.
|
||||
*
|
||||
* @return double|null
|
||||
*/
|
||||
public function log() {
|
||||
$num_args = func_num_args();
|
||||
if ( 1 === $num_args ) {
|
||||
$arg1 = func_get_arg( 0 );
|
||||
|
||||
return log( $arg1 );
|
||||
}
|
||||
if ( 2 === $num_args ) {
|
||||
$arg1 = func_get_arg( 0 );
|
||||
$arg2 = func_get_arg( 1 );
|
||||
|
||||
return log( $arg1 ) / log( $arg2 );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL LEAST() function.
|
||||
*
|
||||
* This function rewrites the function name to SQLite compatible function name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function least() {
|
||||
$arg_list = func_get_args();
|
||||
|
||||
return min( $arg_list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL GREATEST() function.
|
||||
*
|
||||
* This function rewrites the function name to SQLite compatible function name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function greatest() {
|
||||
$arg_list = func_get_args();
|
||||
|
||||
return max( $arg_list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dummy out MySQL GET_LOCK() function.
|
||||
*
|
||||
* This function is meaningless in SQLite, so we do nothing.
|
||||
*
|
||||
* @param string $name Not used.
|
||||
* @param integer $timeout Not used.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_lock( $name, $timeout ) {
|
||||
return '1=1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dummy out MySQL RELEASE_LOCK() function.
|
||||
*
|
||||
* This function is meaningless in SQLite, so we do nothing.
|
||||
*
|
||||
* @param string $name Not used.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function release_lock( $name ) {
|
||||
return '1=1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL UCASE() function.
|
||||
*
|
||||
* This is MySQL alias for upper() function. This function rewrites it
|
||||
* to SQLite compatible name upper().
|
||||
*
|
||||
* @param string $content String to be converted to uppercase.
|
||||
*
|
||||
* @return string SQLite compatible function name.
|
||||
*/
|
||||
public function ucase( $content ) {
|
||||
return "upper($content)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL LCASE() function.
|
||||
*
|
||||
* This is MySQL alias for lower() function. This function rewrites it
|
||||
* to SQLite compatible name lower().
|
||||
*
|
||||
* @param string $content String to be converted to lowercase.
|
||||
*
|
||||
* @return string SQLite compatible function name.
|
||||
*/
|
||||
public function lcase( $content ) {
|
||||
return "lower($content)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL UNHEX() function.
|
||||
*
|
||||
* For a string argument str, UNHEX(str) interprets each pair of characters
|
||||
* in the argument as a hexadecimal number and converts it to the byte represented
|
||||
* by the number. The return value is a binary string.
|
||||
*
|
||||
* @param string $number Number to be unhexed.
|
||||
*
|
||||
* @return string Binary string
|
||||
*/
|
||||
public function unhex( $number ) {
|
||||
return pack( 'H*', $number );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL FROM_BASE64() function.
|
||||
*
|
||||
* Takes a base64-encoded string and returns the decoded result as a binary
|
||||
* string. Returns NULL if the argument is NULL or is not a valid base64 string.
|
||||
*
|
||||
* @param string|null $str The base64-encoded string.
|
||||
*
|
||||
* @return string|null Decoded binary string, or NULL.
|
||||
*/
|
||||
public function from_base64( $str ) {
|
||||
if ( null === $str ) {
|
||||
return null;
|
||||
}
|
||||
$decoded = base64_decode( $str, true );
|
||||
if ( false === $decoded ) {
|
||||
return null;
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL TO_BASE64() function.
|
||||
*
|
||||
* Takes a string and returns a base64-encoded result.
|
||||
* Returns NULL if the argument is NULL.
|
||||
*
|
||||
* @param string|null $str The string to encode.
|
||||
*
|
||||
* @return string|null Base64-encoded string, or NULL.
|
||||
*/
|
||||
public function to_base64( $str ) {
|
||||
if ( null === $str ) {
|
||||
return null;
|
||||
}
|
||||
return base64_encode( $str );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL INET_NTOA() function.
|
||||
*
|
||||
* This function gets 4 or 8 bytes integer and turn it into the network address.
|
||||
*
|
||||
* @param integer $num Long integer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function inet_ntoa( $num ) {
|
||||
return long2ip( $num );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL INET_ATON() function.
|
||||
*
|
||||
* This function gets the network address and turns it into integer.
|
||||
*
|
||||
* @param string $addr Network address.
|
||||
*
|
||||
* @return int long integer
|
||||
*/
|
||||
public function inet_aton( $addr ) {
|
||||
return absint( ip2long( $addr ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL DATEDIFF() function.
|
||||
*
|
||||
* This function compares two dates value and returns the difference.
|
||||
*
|
||||
* @param string $start Start date.
|
||||
* @param string $end End date.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function datediff( $start, $end ) {
|
||||
$start_date = new DateTime( $start );
|
||||
$end_date = new DateTime( $end );
|
||||
$interval = $end_date->diff( $start_date, false );
|
||||
|
||||
return $interval->format( '%r%a' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to emulate MySQL LOCATE() function.
|
||||
*
|
||||
* This function returns the position if $substr is found in $str. If not,
|
||||
* it returns 0. If mbstring extension is loaded, mb_strpos() function is
|
||||
* used.
|
||||
*
|
||||
* @param string $substr Needle.
|
||||
* @param string $str Haystack.
|
||||
* @param integer $pos Position.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function locate( $substr, $str, $pos = 0 ) {
|
||||
if ( ! extension_loaded( 'mbstring' ) ) {
|
||||
$val = strpos( $str, $substr, $pos );
|
||||
if ( false !== $val ) {
|
||||
return $val + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
$val = mb_strpos( $str, $substr, $pos );
|
||||
if ( false !== $val ) {
|
||||
return $val + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return GMT date in the string format.
|
||||
*
|
||||
* @return string formatted GMT date 'dddd-mm-dd'
|
||||
*/
|
||||
public function utc_date() {
|
||||
return gmdate( 'Y-m-d', time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return GMT time in the string format.
|
||||
*
|
||||
* @return string formatted GMT time '00:00:00'
|
||||
*/
|
||||
public function utc_time() {
|
||||
return gmdate( 'H:i:s', time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return GMT time stamp in the string format.
|
||||
*
|
||||
* @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00'
|
||||
*/
|
||||
public function utc_timestamp() {
|
||||
return gmdate( 'Y-m-d H:i:s', time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return MySQL version.
|
||||
*
|
||||
* This function only returns the current newest version number of MySQL,
|
||||
* because it is meaningless for SQLite database.
|
||||
*
|
||||
* @return string representing the version number: major_version.minor_version
|
||||
*/
|
||||
public function version() {
|
||||
return '5.5';
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support.
|
||||
|
||||
* @TODO: Some of the MySQL string specifics described below are likely to
|
||||
* affect also other patterns than just "LIKE BINARY". We should
|
||||
* consider applying some of the conversions more broadly.
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return string
|
||||
*/
|
||||
public function _helper_like_to_glob_pattern( $pattern ) {
|
||||
if ( null === $pattern ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Escape characters that have special meaning in GLOB patterns.
|
||||
*
|
||||
* We need to:
|
||||
* 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class.
|
||||
* 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped).
|
||||
* 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped).
|
||||
*/
|
||||
$pattern = str_replace( ']', '[]]', $pattern );
|
||||
$pattern = str_replace( '*', '[*]', $pattern );
|
||||
$pattern = str_replace( '?', '[?]', $pattern );
|
||||
|
||||
/*
|
||||
* 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
|
||||
*
|
||||
* We need to convert them only when they don't follow any backslashes,
|
||||
* or when they follow an even number of backslashes (as "\\" is "\").
|
||||
*/
|
||||
$pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern );
|
||||
$pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern );
|
||||
|
||||
/*
|
||||
* 3. Unescape LIKE escape sequences.
|
||||
*
|
||||
* While in MySQL LIKE patterns, a backslash is usually used to escape
|
||||
* special characters ("%", "_", and "\"), it works with all characters.
|
||||
*
|
||||
* That is:
|
||||
* SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'.
|
||||
*
|
||||
* This is true also for multi-byte characters:
|
||||
* SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'.
|
||||
*
|
||||
* However, the multi-byte behavior is likely to depend on the charset.
|
||||
* For now, we'll assume UTF-8 and thus the "u" modifier for the regex.
|
||||
*/
|
||||
$pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern );
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user