diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-exception.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-exception.php index 83cf80fbc..72112c16e 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-exception.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-exception.php @@ -1,5 +1,10 @@ code = $code; - $this->driver = $driver; + $this->code = $code; + $this->driver = $driver; + $this->errorInfo = $error_info ?? $this->create_error_info( $message, $code, $previous ); } public function get_driver(): WP_MySQL_On_SQLite { return $this->driver; } + + /** + * Create PDO-style error information from an originating exception or from + * the emulated driver error. + * + * @param string $message The exception message. + * @param int|string $code The exception code. + * @param Throwable|null $previous The previous throwable. + * @return array PDO-style error information. + */ + private function create_error_info( string $message, $code, ?Throwable $previous ): array { + if ( $previous instanceof PDOException && is_array( $previous->errorInfo ) ) { + return $previous->errorInfo; + } + + $sqlstate = is_string( $code ) && 5 === strlen( $code ) ? $code : 'HY000'; + return array( $sqlstate, 1105, $message ); + } } diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-statement.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-statement.php index af471f6f8..5890e5e58 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-statement.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite-statement.php @@ -8,6 +8,7 @@ * PDO uses camel case naming, enable non-snake case: * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase * * PDO uses $class as a variable name, enable it: * phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.classFound @@ -64,6 +65,18 @@ public function fetchAll( $mode = null, $class_name = null, $constructor_args = } return $this->fetchAllRows( $mode, $class_name, $constructor_args ); } + + /** + * 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. + */ + #[ReturnTypeWillChange] + public function getColumnMeta( $column ) { + return $this->getColumnMetadata( $column ); + } } } else { /** @@ -94,6 +107,21 @@ public function setFetchMode( $mode, ...$args ): bool { public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array { return $this->fetchAllRows( $mode, ...$args ); } + + /** + * 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. + */ + #[ReturnTypeWillChange] + public function getColumnMeta( int $column ) { + if ( $column < 0 ) { + throw new ValueError( 'PDOStatement::getColumnMeta(): Argument #1 ($column) must be greater than or equal to 0' ); + } + return $this->getColumnMetadata( $column ); + } } } @@ -118,7 +146,7 @@ public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array { * - 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_MySQL_On_SQLite_Statement extends PDOStatement { +class WP_MySQL_On_SQLite_Statement extends PDOStatement implements IteratorAggregate { use WP_MySQL_On_SQLite_Statement_PHP_Compat; /** @@ -128,6 +156,20 @@ class WP_MySQL_On_SQLite_Statement extends PDOStatement { */ private $statement; + /** + * Resolve MySQL-compatible metadata by column index. + * + * @var callable + */ + private $column_meta_resolver; + + /** + * Resolved MySQL-compatible metadata, keyed by column index. + * + * @var array + */ + private $resolved_column_meta = array(); + /** * The number of affected rows. * @@ -138,15 +180,25 @@ class WP_MySQL_On_SQLite_Statement extends PDOStatement { /** * Constructor. * - * @param PDOStatement $statement The original PDO statement. - * @param int $affected_rows The number of affected rows. + * @param PDOStatement $statement The original PDO statement. + * @param string $query The original MySQL query. + * @param callable $column_meta_resolver Resolves metadata by column index. + * @param int|null $affected_rows The number of affected rows. */ public function __construct( PDOStatement $statement, + string $query, + callable $column_meta_resolver, ?int $affected_rows = null ) { - $this->statement = $statement; - $this->affected_rows = $affected_rows; + $this->statement = $statement; + + // Userland can only initialize PDOStatement::$queryString on PHP 8.1+. + if ( PHP_VERSION_ID >= 80100 ) { + $this->queryString = $query; + } + $this->column_meta_resolver = $column_meta_resolver; + $this->affected_rows = $affected_rows; } /** @@ -221,17 +273,6 @@ 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. * @@ -239,7 +280,7 @@ public function getColumnMeta( $column ): array { * or null if there is no error. */ public function errorCode(): ?string { - throw new RuntimeException( 'Not implemented' ); + return $this->statement->errorCode(); } /** @@ -251,7 +292,11 @@ public function errorCode(): ?string { * 2: Driver-specific error message. */ public function errorInfo(): array { - throw new RuntimeException( 'Not implemented' ); + // Normalize successful results. PDO_SQLite may retain stale driver-specific fields on PHP < 8.0. + if ( '00000' === $this->statement->errorCode() ) { + return array( '00000', null, null ); + } + return $this->statement->errorInfo(); } /** @@ -282,7 +327,7 @@ public function setAttribute( $attribute, $value ): bool { * @return Iterator The iterator for the result set. */ public function getIterator(): Iterator { - throw new RuntimeException( 'Not implemented' ); + yield from $this->statement; } /** @@ -300,7 +345,7 @@ public function nextRowset(): bool { * @return bool True on success, false on failure. */ public function closeCursor(): bool { - throw new RuntimeException( 'Not implemented' ); + return $this->statement->closeCursor(); } /** @@ -313,8 +358,8 @@ public function closeCursor(): bool { * @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' ); + public function bindColumn( $column, &$var, $type = PDO::PARAM_STR, $maxLength = 0, $driverOptions = null ): bool { + return $this->statement->bindColumn( $column, $var, $type, $maxLength, $driverOptions ); } /** @@ -354,6 +399,26 @@ public function debugDumpParams(): ?bool { throw new RuntimeException( 'Not implemented' ); } + /** + * Get metadata for a column in a result set. + * + * This is used internally by the "WP_MySQL_On_SQLite_Statement_PHP_Compat" trait, + * that is defined conditionally based on the current PHP version. + * + * @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. + */ + private function getColumnMetadata( $column ) { + if ( ! array_key_exists( $column, $this->resolved_column_meta ) ) { + $this->resolved_column_meta[ $column ] = call_user_func( + $this->column_meta_resolver, + $column + ); + } + return $this->resolved_column_meta[ $column ]; + } + /** * Fetch all remaining rows from the result set. * diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php index f17e30296..9ebf68c47 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php @@ -3,8 +3,31 @@ /* * The SQLite driver uses PDO. Enable PDO function calls: * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO + * + * PDO uses camel case naming, enable non-snake case: + * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + * + * PDO uses $string as a parameter name, enable it: + * phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound + * + * We conditionally define a trait for PHP-version-specific PDO methods: + * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound */ +/* + * The "PDO::connect()" method in PHP 8.4 uses a "static" return type declaration, + * which PHP 7 cannot parse. Therefore, a conditional file import is needed. + */ +if ( PHP_VERSION_ID >= 80400 ) { + require_once __DIR__ . '/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php'; +} else { + /** + * @access private + */ + trait WP_MySQL_On_SQLite_PDO_Compat {} +} + /** * SQLite driver for MySQL. * @@ -15,6 +38,8 @@ * The driver requires PDO with the SQLite driver, and the PCRE engine. */ class WP_MySQL_On_SQLite extends PDO { + use WP_MySQL_On_SQLite_PDO_Compat; + /** * The path to the MySQL SQL grammar file. */ @@ -518,6 +543,47 @@ class WP_MySQL_On_SQLite extends PDO { */ private $connection; + /** + * Caller-visible PDO error mode. + * + * The underlying SQLite connection always uses exceptions so internal + * operations can handle errors reliably. + * + * @var int + */ + private $error_mode = PDO::ERRMODE_EXCEPTION; + + /** + * Whether fetched scalar values should be converted to strings. + * + * PDO SQLite cannot report PDO::ATTR_STRINGIFY_FETCHES on PHP 7.2–8.1, + * so the wrapper tracks its value on those versions. + * + * @var bool + */ + private $stringify_fetches = false; + + /** + * SQLSTATE associated with the last PDO operation. + * + * @var string|null + */ + private $error_code; + + /** + * Error information associated with the last PDO operation. + * + * @var array + */ + private $error_info = array( '', null, null ); + + /** + * ID generated by the last user-issued INSERT or REPLACE statement. + * + * @var string + */ + private $last_insert_id = '0'; + /** * User-defined functions registered on the SQLite connection. * @@ -697,9 +763,11 @@ class WP_MySQL_On_SQLite extends PDO { * @param string $dsn MySQL-on-SQLite DSN containing the SQLite path and database name. * @param string|null $username Optional. Ignored by this driver. * @param string|null $password Optional. Ignored by this driver. - * @param array $options { + * @param array|null $options { * Optional driver options. * + * Numeric keys are handled as standard PDO constructor options. + * * @type int $mysql_version Optional. MySQL version to emulate. Default 80038. * @type PDO|null $pdo Optional. Existing SQLite PDO connection. * @type string|null $journal_mode Optional. SQLite journal mode. Default 'WAL'. @@ -712,8 +780,10 @@ public function __construct( string $dsn, ?string $username = null, ?string $password = null, - array $options = array() + ?array $options = null ) { + $options = $options ?? array(); + // PDO DSN can't include "\0" bytes; parsing stops at the first one. $first_null_byte_index = strpos( $dsn, "\0" ); if ( false !== $first_null_byte_index ) { @@ -754,9 +824,18 @@ public function __construct( $db_name = $args['dbname'] ?? 'sqlite_database'; // Create a new SQLite connection. + $pdo_options = array_filter( + $options, + function ( $key ) { + return is_int( $key ); + }, + ARRAY_FILTER_USE_KEY + ); + $connection_options = array( 'journal_mode' => $options['journal_mode'] ?? null, 'synchronous' => $options['synchronous'] ?? null, + 'pdo_options' => $pdo_options, ); if ( isset( $options['pdo'] ) ) { $connection_options['pdo'] = $options['pdo']; @@ -854,6 +933,40 @@ function ( string $sql, array $params ) { ); } ); + + foreach ( $pdo_options as $attribute => $value ) { + // Persistence is a connection-time-only option and was already passed + // to the underlying PDO constructor when creating a new connection. + if ( PDO::ATTR_PERSISTENT === $attribute ) { + continue; + } + $this->setAttribute( $attribute, $value ); + } + } + + /** + * PDO API: Prepare a MySQL statement for execution. + * + * Prepared statements are not implemented yet. Report the standard PDO + * unsupported-function diagnostic instead of using uninitialized parent + * PDO state. + * + * @param string $query The MySQL statement to prepare. + * @param array|null $options Optional statement options. + * @return PDOStatement|false False when exceptions are disabled. + * + * @throws WP_MySQL_On_SQLite_Exception When exception mode is enabled. + */ + #[ReturnTypeWillChange] + public function prepare( $query, $options = null ) { + $driver_message = 'driver does not support prepared statements'; + $exception = $this->new_driver_exception( + 'SQLSTATE[IM001]: Driver does not support this function: ' . $driver_message, + 'IM001', + null, + array( 'IM001', 0, $driver_message ) + ); + return $this->handle_pdo_error( $exception ); } /** @@ -899,11 +1012,6 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar ); return false; } - - // When the default FETCH_BOTH is not set explicitly, additional - // arguments are ignored, and the argument count is not validated. - $fetch_mode = $this->connection->get_pdo()->getAttribute( PDO::ATTR_DEFAULT_FETCH_MODE ); - $fetch_mode_args = array(); } elseif ( PDO::FETCH_COLUMN === $fetch_mode ) { if ( 3 !== $arg_count ) { throw new ArgumentCountError( @@ -1037,8 +1145,17 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar $this->last_result_statement = $this->create_result_statement_from_data( array(), array() ); } - $stmt = new WP_MySQL_On_SQLite_Statement( $this->last_result_statement, $this->last_affected_rows ); - $stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args ); + $stmt = new WP_MySQL_On_SQLite_Statement( + $this->last_result_statement, + $query, + $this->create_column_meta_resolver( $this->last_column_meta ), + $this->last_affected_rows + ); + if ( null !== $fetch_mode ) { + $stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args ); + } + $this->error_code = '00000'; + $this->error_info = array( '00000', null, null ); return $stmt; } catch ( Throwable $e ) { try { @@ -1047,12 +1164,14 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar } catch ( Throwable $rollback_exception ) { // Ignore rollback errors. } - if ( $e instanceof WP_MySQL_On_SQLite_Exception ) { - throw $e; - } elseif ( $e instanceof WP_SQLite_Information_Schema_Exception ) { - throw $this->convert_information_schema_exception( $e ); + if ( $e instanceof WP_SQLite_Information_Schema_Exception ) { + $e = $this->convert_information_schema_exception( $e ); + } + if ( ! ( $e instanceof WP_MySQL_On_SQLite_Exception ) ) { + $e = $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); } - throw $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); + + return $this->handle_pdo_error( $e ); } finally { // A query that doesn't return any rows or fails sets found rows to 0. if ( ! $this->is_readonly || isset( $e ) ) { @@ -1069,9 +1188,61 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar #[ReturnTypeWillChange] public function exec( $query ) { $stmt = $this->query( $query ); + if ( false === $stmt ) { + return false; + } return $stmt->rowCount(); } + /** + * PDO API: Return the ID of the last inserted row. + * + * @param string|null $name Optional sequence name. Ignored by SQLite. + * @return string|false The last insert ID, or false on failure. + */ + #[ReturnTypeWillChange] + public function lastInsertId( $name = null ) { + if ( + is_array( $name ) + || is_resource( $name ) + || ( is_object( $name ) && ! method_exists( $name, '__toString' ) ) + ) { + if ( PHP_VERSION_ID >= 80000 ) { + throw new TypeError( + sprintf( + 'PDO::lastInsertId(): Argument #1 ($name) must be of type ?string, %s given', + get_debug_type( $name ) + ) + ); + } + trigger_error( + sprintf( 'PDO::lastInsertId() expects parameter 1 to be string, %s given', strtolower( gettype( $name ) ) ), + E_USER_WARNING + ); + return false; + } + + return $this->last_insert_id; + } + + /** + * PDO API: Fetch the SQLSTATE associated with the last operation. + * + * @return string|null The SQLSTATE error code, or null when unavailable. + */ + public function errorCode(): ?string { + return $this->error_code; + } + + /** + * PDO API: Fetch error information associated with the last operation. + * + * @return array Error information from the last PDO operation. + */ + public function errorInfo(): array { + return $this->error_info; + } + /** * PDO API: Quote a string for use in a MySQL query. * @@ -1080,7 +1251,6 @@ public function exec( $query ) { * @return string The quoted string. */ #[ReturnTypeWillChange] - // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound public function quote( $string, $type = PDO::PARAM_STR ) { // Mirror PDO\MySQL::quote() value validation. if ( @@ -1146,11 +1316,11 @@ public function quote( $string, $type = PDO::PARAM_STR ) { * * @return bool True on success, false on failure. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function beginTransaction(): bool { if ( $this->inTransaction() ) { throw $this->new_driver_exception( 'There is already an active transaction' ); } + $this->flush(); $this->begin_user_transaction(); return true; } @@ -1164,6 +1334,7 @@ public function commit(): bool { if ( ! $this->inTransaction() ) { throw $this->new_driver_exception( 'There is no active transaction' ); } + $this->flush(); $this->commit_user_transaction(); return true; } @@ -1173,11 +1344,11 @@ public function commit(): bool { * * @return bool True on success, false on failure. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function rollBack(): bool { if ( ! $this->inTransaction() ) { throw $this->new_driver_exception( 'There is no active transaction' ); } + $this->flush(); $this->rollback_user_transaction(); return true; } @@ -1187,7 +1358,6 @@ public function rollBack(): bool { * * @return bool True if a transaction is active, false otherwise. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function inTransaction(): bool { if ( PHP_VERSION_ID < 80400 ) { /* @@ -1213,7 +1383,23 @@ public function inTransaction(): bool { * @return bool True on success, false on failure. */ public function setAttribute( $attribute, $value ): bool { - return $this->connection->get_pdo()->setAttribute( $attribute, $value ); + // Track the caller's error mode while keeping internal SQLite operations in exception mode. + if ( PDO::ATTR_ERRMODE === $attribute ) { + $pdo = $this->connection->get_pdo(); + $result = $pdo->setAttribute( $attribute, $value ); + if ( ! $result ) { + return false; + } + $this->error_mode = $pdo->getAttribute( PDO::ATTR_ERRMODE ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + return true; + } + + $result = $this->connection->get_pdo()->setAttribute( $attribute, $value ); + if ( $result && PDO::ATTR_STRINGIFY_FETCHES === $attribute ) { + $this->stringify_fetches = (bool) $value; + } + return $result; } /** @@ -1228,6 +1414,14 @@ public function setAttribute( $attribute, $value ): bool { */ #[ReturnTypeWillChange] public function getAttribute( $attribute ) { + // Return the caller's error mode instead of the exception mode used internally. + if ( PDO::ATTR_ERRMODE === $attribute ) { + return $this->error_mode; + } + if ( PDO::ATTR_STRINGIFY_FETCHES === $attribute && PHP_VERSION_ID < 80200 ) { + // PDO SQLite cannot report this attribute before PHP 8.2. + return $this->stringify_fetches; + } return $this->connection->get_pdo()->getAttribute( $attribute ); } @@ -1334,19 +1528,6 @@ public function get_last_sqlite_queries(): array { return $this->last_sqlite_queries; } - /** - * Get the auto-increment value generated for the last query. - * - * @return int|string - */ - public function get_insert_id() { - $last_insert_id = $this->connection->get_last_insert_id(); - if ( is_numeric( $last_insert_id ) ) { - $last_insert_id = (int) $last_insert_id; - } - return $last_insert_id; - } - /** * Tokenize a MySQL query and initialize a parser. * @@ -1384,191 +1565,222 @@ private function reset_or_create_parser( $tokens ): WP_MySQL_Parser { } /** - * Get the number of columns returned by the last emulated query. + * Execute a query in SQLite. * * @access private * - * @return int + * @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 get_last_column_count(): int { - return count( $this->last_column_meta ); + public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { + return $this->connection->query( $sql, $params ); } /** - * Get column metadata for results of the last emulated query. + * Create a lazy MySQL-compatible column metadata resolver. * - * @access private + * Only raw SQLite metadata and database context are snapshotted here. + * The INFORMATION_SCHEMA metadata is resolved lazily and may reflect + * schema changes made after statement execution. This is a trade-off + * that avoids schema queries when column metadata is not requested. * - * @return array - */ - public function get_last_column_meta(): array { - // Build the column metadata as per "PDOStatement::getColumnMeta()". - $column_meta = array(); - foreach ( $this->last_column_meta as $meta ) { - $table = $meta['table'] ?? null; - $name = $meta['name']; - $type = strtoupper( $meta['sqlite:decl_type'] ?? $meta['native_type'] ?? '' ); - - // When table is known, we can get data from the information schema. - $column_info = null; - if ( null !== $table ) { - $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table ); - $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); - $column_info = $this->execute_sqlite_query( - sprintf( - ' - SELECT - IS_NULLABLE, - DATA_TYPE, - COLUMN_TYPE, - COLUMN_KEY, - CHARACTER_MAXIMUM_LENGTH, - NUMERIC_PRECISION, - NUMERIC_SCALE - FROM %s - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? - ', - $this->quote_sqlite_identifier( $columns_table ) - ), - array( $this->get_saved_db_name(), $table, $name ) - )->fetch( PDO::FETCH_ASSOC ); + * @param array $raw_column_meta Raw SQLite result column metadata. + * @return callable The column metadata resolver. + */ + private function create_column_meta_resolver( array $raw_column_meta ): callable { + $db_name = $this->db_name; + return function ( $column ) use ( $raw_column_meta, $db_name ) { + if ( ! array_key_exists( $column, $raw_column_meta ) ) { + return false; + } - if ( false === $column_info ) { - $column_info = null; - } + $last_sqlite_queries = $this->last_sqlite_queries; + try { + return $this->resolve_column_meta( $raw_column_meta[ $column ], $db_name ); + } finally { + $this->last_sqlite_queries = $last_sqlite_queries; } + }; + } - // If we have information schema data, we can use it. - if ( null !== $column_info ) { - $type_info = self::COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP[ $column_info['DATA_TYPE'] ] ?? null; - if ( null === $type_info ) { - $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ] ?? null; - } - $native_type = $type_info[0]; - $mysqli_type = $type_info[1]; - $len = $type_info[2]; - $precision = $type_info[3]; + /** + * Resolve raw SQLite column metadata into MySQL-compatible metadata. + * + * @param array $meta Raw SQLite column metadata. + * @param string $db_name Database selected when the query was executed. + * @return array MySQL-compatible column metadata. + */ + private function resolve_column_meta( array $meta, string $db_name ): array { + $table = $meta['table'] ?? null; + $name = $meta['name']; + $type = strtoupper( $meta['sqlite:decl_type'] ?? $meta['native_type'] ?? '' ); - if ( 'tinyint(1)' === $column_info['COLUMN_TYPE'] ) { - $len = 1; - } + // When table is known, we can get data from the information schema. + $column_info = null; + if ( null !== $table ) { + $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table ); + $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); + $column_info = $this->execute_sqlite_query( + sprintf( + ' + SELECT + IS_NULLABLE, + DATA_TYPE, + COLUMN_TYPE, + COLUMN_KEY, + CHARACTER_MAXIMUM_LENGTH, + NUMERIC_PRECISION, + NUMERIC_SCALE + FROM %s + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? + ', + $this->quote_sqlite_identifier( $columns_table ) + ), + array( $this->get_saved_db_name( $db_name ), $table, $name ) + )->fetch( PDO::FETCH_ASSOC ); - if ( 'decimal' === $column_info['DATA_TYPE'] ) { - $len = (int) $column_info['NUMERIC_PRECISION'] + (int) $column_info['NUMERIC_SCALE']; - $precision = (int) $column_info['NUMERIC_SCALE']; - } + if ( false === $column_info ) { + $column_info = null; + } + } - if ( - str_contains( $column_info['COLUMN_TYPE'], 'unsigned' ) - && ! str_contains( $column_info['COLUMN_TYPE'], 'bigint' ) - ) { - $len -= 1; - } + // If we have information schema data, we can use it. + if ( null !== $column_info ) { + $type_info = self::COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP[ $column_info['DATA_TYPE'] ] ?? null; + if ( null === $type_info ) { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ] ?? null; + } + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2]; + $precision = $type_info[3]; - // If set, lenght can be taken from the information schema. - if ( isset( $column_info['CHARACTER_MAXIMUM_LENGTH'] ) ) { - $len = (int) $column_info['CHARACTER_MAXIMUM_LENGTH']; - } + if ( 'tinyint(1)' === $column_info['COLUMN_TYPE'] ) { + $len = 1; + } - // For string types, the length is multiplied by the maximum number - // of bytes per character for the used connection encoding. In our - // case, it's always "utf8mb4" and therefore 4 bytes per character. - if ( - str_contains( $column_info['DATA_TYPE'], 'text' ) - || str_contains( $column_info['DATA_TYPE'], 'char' ) - || 'enum' === $column_info['DATA_TYPE'] - || 'set' === $column_info['DATA_TYPE'] - ) { - // Except for "longtext" - this might be a MySQL bug. - if ( 'longtext' !== $column_info['DATA_TYPE'] ) { - $len = 4 * $len; - } - } + if ( 'decimal' === $column_info['DATA_TYPE'] ) { + $len = (int) $column_info['NUMERIC_PRECISION'] + (int) $column_info['NUMERIC_SCALE']; + $precision = (int) $column_info['NUMERIC_SCALE']; + } - // Flags. - $flags = array(); - if ( 'NO' === $column_info['IS_NULLABLE'] ) { - $flags[] = 'not_null'; - } - if ( 'PRI' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'primary_key'; - } elseif ( 'UNI' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'unique_key'; - } elseif ( 'MUL' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'multiple_key'; - } - } else { - $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ]; - $native_type = $type_info[0]; - $mysqli_type = $type_info[1]; - $len = $type_info[2] ?? 0; - $precision = $type_info[3]; - - // Flags. - $flags = array(); - if ( 'NULL' !== $type ) { - $flags[] = 'not_null'; - } + if ( + str_contains( $column_info['COLUMN_TYPE'], 'unsigned' ) + && ! str_contains( $column_info['COLUMN_TYPE'], 'bigint' ) + ) { + $len -= 1; } - if ( 'BLOB' === $native_type || 'GEOMETRY' === $native_type ) { - $flags[] = 'blob'; + // If set, length can be taken from the information schema. + if ( isset( $column_info['CHARACTER_MAXIMUM_LENGTH'] ) ) { + $len = (int) $column_info['CHARACTER_MAXIMUM_LENGTH']; } - // PDO type. - if ( 'INT' === $type || 'INTEGER' === $type ) { - $pdo_type = PDO::PARAM_INT; - } else { - $pdo_type = PDO::PARAM_STR; + // For string types, the length is multiplied by the maximum number + // of bytes per character for the used connection encoding. In our + // case, it's always "utf8mb4" and therefore 4 bytes per character. + if ( + str_contains( $column_info['DATA_TYPE'], 'text' ) + || str_contains( $column_info['DATA_TYPE'], 'char' ) + || 'enum' === $column_info['DATA_TYPE'] + || 'set' === $column_info['DATA_TYPE'] + ) { + // Except for "longtext" - this might be a MySQL bug. + if ( 'longtext' !== $column_info['DATA_TYPE'] ) { + $len = 4 * $len; + } } - // MySQLi charset number. - $is_string = 'STRING' === $type || 'TEXT' === $type; - $is_binary = 'BLOB' === $type || 'GEOMETRY' === $native_type; - $is_datetime = str_contains( $native_type, 'DATE' ) || str_contains( $native_type, 'TIME' ) || 'YEAR' === $native_type; - if ( $is_string && ! $is_binary && ! $is_datetime ) { - $mysqli_charsetnr = 255; // utf8mb4_0900_ai_ci - } else { - $mysqli_charsetnr = 63; // binary + // Flags. + $flags = array(); + if ( 'NO' === $column_info['IS_NULLABLE'] ) { + $flags[] = 'not_null'; + } + if ( 'PRI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'primary_key'; + } elseif ( 'UNI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'unique_key'; + } elseif ( 'MUL' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'multiple_key'; } + } else { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ]; + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2] ?? 0; + $precision = $type_info[3]; - $column_meta[] = array( - 'native_type' => $native_type, - 'pdo_type' => $pdo_type, - 'flags' => $flags, - 'table' => $meta['table'] ?? '', - 'name' => $meta['name'], - 'len' => $len, - 'precision' => $precision, - 'sqlite:decl_type' => $meta['sqlite:decl_type'] ?? '', + // Flags. + $flags = array(); + if ( 'NULL' !== $type ) { + $flags[] = 'not_null'; + } + } - /* - * The MySQLi PHP extension exposes more MySQL column metadata than PDO. - * We'll add the data here for use cases such as "wpdb::get_col_info()". - */ - 'mysqli:orgname' => $meta['name'], // TODO: Use correct original name when alias is used. - 'mysqli:orgtable' => $meta['table'] ?? '', // TODO: Use correct original name when table alias is used. - 'mysqli:db' => $this->db_name, // TODO: Use correct DB for queries to information schema. - 'mysqli:charsetnr' => $mysqli_charsetnr, - 'mysqli:flags' => 0, // TODO: We can compute correct MySQL flags. - 'mysqli:type' => $mysqli_type, - ); + if ( 'BLOB' === $native_type || 'GEOMETRY' === $native_type ) { + $flags[] = 'blob'; } - return $column_meta; + + // PDO type. + if ( 'INT' === $type || 'INTEGER' === $type ) { + $pdo_type = PDO::PARAM_INT; + } else { + $pdo_type = PDO::PARAM_STR; + } + + // MySQLi charset number. + $is_string = 'STRING' === $type || 'TEXT' === $type; + $is_binary = 'BLOB' === $type || 'GEOMETRY' === $native_type; + $is_datetime = str_contains( $native_type, 'DATE' ) || str_contains( $native_type, 'TIME' ) || 'YEAR' === $native_type; + if ( $is_string && ! $is_binary && ! $is_datetime ) { + $mysqli_charsetnr = 255; // utf8mb4_0900_ai_ci + } else { + $mysqli_charsetnr = 63; // binary + } + + return array( + 'native_type' => $native_type, + 'pdo_type' => $pdo_type, + 'flags' => $flags, + 'table' => $meta['table'] ?? '', + 'name' => $meta['name'], + 'len' => $len, + 'precision' => $precision, + 'sqlite:decl_type' => $meta['sqlite:decl_type'] ?? '', + + /* + * The MySQLi PHP extension exposes more MySQL column metadata than PDO. + * We'll add the data here for use cases such as "wpdb::get_col_info()". + */ + 'mysqli:orgname' => $meta['name'], // TODO: Use correct original name when alias is used. + 'mysqli:orgtable' => $meta['table'] ?? '', // TODO: Use correct original name when table alias is used. + 'mysqli:db' => $db_name, // TODO: Use correct DB for queries to information schema. + 'mysqli:charsetnr' => $mysqli_charsetnr, + 'mysqli:flags' => 0, // TODO: We can compute correct MySQL flags. + 'mysqli:type' => $mysqli_type, + ); } /** - * Execute a query in SQLite. - * - * @access private + * Record and report a PDO operation error according to the configured mode. * - * @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. + * @param WP_MySQL_On_SQLite_Exception $exception The operation error. + * @return false Always false when the error is not thrown. + * @throws WP_MySQL_On_SQLite_Exception When exception mode is enabled. */ - public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { - return $this->connection->query( $sql, $params ); + private function handle_pdo_error( WP_MySQL_On_SQLite_Exception $exception ): bool { + $this->error_info = $exception->errorInfo; + $this->error_code = $this->error_info[0]; + + if ( PDO::ERRMODE_EXCEPTION === $this->error_mode ) { + throw $exception; + } + if ( PDO::ERRMODE_WARNING === $this->error_mode ) { + trigger_error( $exception->getMessage(), E_USER_WARNING ); + } + return false; } /** @@ -2188,10 +2400,12 @@ function ( $column ) { throw $e; } } + $this->last_insert_id = $this->connection->get_last_insert_id(); return; } $this->last_result_statement = $this->execute_sqlite_query( $query ); + $this->last_insert_id = $this->connection->get_last_insert_id(); } /** @@ -7379,6 +7593,7 @@ private function flush(): void { $this->last_sqlite_queries = array(); $this->last_result_statement = null; $this->last_affected_rows = null; + $this->last_insert_id = '0'; $this->last_column_meta = array(); $this->is_readonly = false; $this->wrapper_transaction_type = null; @@ -7486,17 +7701,19 @@ private function create_result_statement_from_data( array $columns, array $rows /** * Create a new MySQL-on-SQLite driver exception. * - * @param string $message The exception message. - * @param int|string $code The exception code. For PDO errors, a string representing SQLSTATE. - * @param Throwable|null $previous The previous exception. + * @param string $message The exception message. + * @param int|string $code The exception code. For PDO errors, a string representing SQLSTATE. + * @param Throwable|null $previous The previous exception. + * @param array|null $error_info PDO-style error information. * @return WP_MySQL_On_SQLite_Exception */ private function new_driver_exception( string $message, $code = 0, - ?Throwable $previous = null + ?Throwable $previous = null, + ?array $error_info = null ): WP_MySQL_On_SQLite_Exception { - return new WP_MySQL_On_SQLite_Exception( $this, $message, $code, $previous ); + return new WP_MySQL_On_SQLite_Exception( $this, $message, $code, $previous, $error_info ); } /** @@ -7568,52 +7785,55 @@ private function new_access_denied_to_information_schema_exception(): WP_MySQL_O private function convert_information_schema_exception( WP_SQLite_Information_Schema_Exception $e ): Throwable { switch ( $e->get_type() ) { case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_TABLE_NAME: + $driver_message = sprintf( "Table '%s' already exists", $e->get_data()['table_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42S01]: Base table or view already exists: 1050 Table '%s' already exists", - $e->get_data()['table_name'] - ), - '42S01' + 'SQLSTATE[42S01]: Base table or view already exists: 1050 ' . $driver_message, + '42S01', + null, + array( '42S01', 1050, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_COLUMN_NAME: + $driver_message = sprintf( "Duplicate column name '%s'", $e->get_data()['column_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name '%s'", - $e->get_data()['column_name'] - ), - '42S21' + 'SQLSTATE[42S21]: Column already exists: 1060 ' . $driver_message, + '42S21', + null, + array( '42S21', 1060, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_KEY_NAME: + $driver_message = sprintf( "Duplicate key name '%s'", $e->get_data()['key_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name '%s'", - $e->get_data()['key_name'] - ), - '42S21' + 'SQLSTATE[42000]: Syntax error or access violation: 1061 ' . $driver_message, + '42000', + null, + array( '42000', 1061, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_KEY_COLUMN_NOT_FOUND: + $driver_message = sprintf( "Key column '%s' doesn't exist in table", $e->get_data()['column_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42000]: Syntax error or access violation: 1072 Key column '%s' doesn't exist in table", - $e->get_data()['column_name'] - ), - '42000' + 'SQLSTATE[42000]: Syntax error or access violation: 1072 ' . $driver_message, + '42000', + null, + array( '42000', 1072, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_CONSTRAINT_DOES_NOT_EXIST: + $driver_message = sprintf( "Constraint '%s' does not exist.", $e->get_data()['name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[HY000]: General error: 3940 Constraint '%s' does not exist.", - $e->get_data()['name'] - ), - 'HY000' + 'SQLSTATE[HY000]: General error: 3940 ' . $driver_message, + 'HY000', + null, + array( 'HY000', 3940, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME: + $driver_message = sprintf( + "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", + $e->get_data()['name'] + ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", - $e->get_data()['name'] - ), - 'HY000' + 'SQLSTATE[HY000]: General error: 3939 ' . $driver_message, + 'HY000', + null, + array( 'HY000', 3939, $driver_message ) ); default: return $e; diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php index 829072ca2..60792103b 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php @@ -81,6 +81,7 @@ class WP_SQLite_Connection { * @type string|null $journal_mode Optional. SQLite journal mode. Defaults to WAL. * @type string|int|null $synchronous Optional. SQLite synchronous setting. Defaults to * NORMAL when the effective journal mode is WAL. + * @type array $pdo_options Optional. PDO constructor options. * } * * @throws InvalidArgumentException When some connection options are invalid. @@ -94,8 +95,13 @@ public function __construct( array $options ) { 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'] ); + $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; + $pdo_options = $options['pdo_options'] ?? array(); + + // Internal driver operations require exceptions regardless of the + // caller-visible WP_MySQL_On_SQLite::ATTR_ERRMODE setting. + $pdo_options[ PDO::ATTR_ERRMODE ] = PDO::ERRMODE_EXCEPTION; + $this->pdo = new $pdo_class( 'sqlite:' . $options['path'], null, null, $pdo_options ); } // Throw exceptions on error. diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php index 5adaf2406..dacb0c759 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-driver.php @@ -3,6 +3,9 @@ /* * The SQLite driver uses PDO. Enable PDO function calls: * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO + * + * PDO uses camel case naming, enable non-snake case: + * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid */ /** @@ -44,6 +47,13 @@ class WP_SQLite_Driver { */ private $mysql_on_sqlite_driver; + /** + * Statement returned for the last emulated query. + * + * @var WP_MySQL_On_SQLite_Statement|null + */ + private $last_statement; + /** * Results of the last emulated query. * @@ -79,7 +89,7 @@ public function __construct( ); $this->client_info = $this->mysql_on_sqlite_driver->client_info; - $connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $this->mysql_on_sqlite_driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); } /** @@ -147,7 +157,11 @@ public function get_last_sqlite_queries(): array { * @return int|string */ public function get_insert_id() { - return $this->mysql_on_sqlite_driver->get_insert_id(); + $last_insert_id = $this->mysql_on_sqlite_driver->lastInsertId(); + if ( is_numeric( $last_insert_id ) ) { + $last_insert_id = (int) $last_insert_id; + } + return $last_insert_id; } /** @@ -160,7 +174,9 @@ public function get_insert_id() { * @throws WP_MySQL_On_SQLite_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 ); + $this->last_statement = null; + $stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args ); + $this->last_statement = $stmt; if ( $stmt->columnCount() > 0 ) { $this->last_result = $stmt->fetchAll( $fetch_mode ); @@ -204,7 +220,7 @@ public function get_last_return_value() { * @return int */ public function get_last_column_count(): int { - return $this->mysql_on_sqlite_driver->get_last_column_count(); + return null === $this->last_statement ? 0 : $this->last_statement->columnCount(); } /** @@ -213,7 +229,16 @@ public function get_last_column_count(): int { * @return array */ public function get_last_column_meta(): array { - return $this->mysql_on_sqlite_driver->get_last_column_meta(); + if ( null === $this->last_statement ) { + return array(); + } + + $column_meta = array(); + $column_count = $this->last_statement->columnCount(); + for ( $i = 0; $i < $column_count; $i++ ) { + $column_meta[] = $this->last_statement->getColumnMeta( $i ); + } + return $column_meta; } /** @@ -231,7 +256,7 @@ public function execute_sqlite_query( string $sql, array $params = array() ): PD /** * Begin a new transaction or nested transaction. */ - public function beginTransaction(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + public function beginTransaction(): void { $this->mysql_on_sqlite_driver->beginTransaction(); } diff --git a/packages/mysql-on-sqlite/src/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php b/packages/mysql-on-sqlite/src/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php new file mode 100644 index 000000000..f59d71322 --- /dev/null +++ b/packages/mysql-on-sqlite/src/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php @@ -0,0 +1,31 @@ +assertInstanceOf( PDO::class, $driver ); } + public function test_static_connect(): void { + if ( PHP_VERSION_ID < 80400 ) { + $this->markTestSkipped( 'PDO::connect() requires PHP 8.4 or newer.' ); + } + + $driver = WP_MySQL_On_SQLite::connect( + 'mysql-on-sqlite:path=:memory:;dbname=WordPress;', + null, + null, + array( PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ) + ); + + $this->assertInstanceOf( WP_MySQL_On_SQLite::class, $driver ); + $this->assertSame( array( 'value' => 1 ), $driver->query( 'SELECT 1 AS value' )->fetch() ); + } + + public function test_constructor_accepts_null_options(): void { + $driver = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:path=:memory:;dbname=WordPress;', + null, + null, + null + ); + + $this->assertInstanceOf( PDO::class, $driver ); + } + + public function test_constructor_applies_pdo_options(): void { + $driver = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:path=:memory:;dbname=WordPress;', + null, + null, + array( + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, + PDO::ATTR_STRINGIFY_FETCHES => true, + ) + ); + + $this->assertSame( PDO::FETCH_ASSOC, $driver->getAttribute( PDO::ATTR_DEFAULT_FETCH_MODE ) ); + $this->assertSame( PDO::ERRMODE_SILENT, $driver->getAttribute( PDO::ATTR_ERRMODE ) ); + $this->assertTrue( $driver->getAttribute( PDO::ATTR_STRINGIFY_FETCHES ) ); + $this->assertSame( array( 'value' => '1' ), $driver->query( 'SELECT 1 AS value' )->fetch() ); + + // Internal operations always retain exception mode. + $this->assertSame( PDO::ERRMODE_EXCEPTION, $driver->get_sqlite_pdo()->getAttribute( PDO::ATTR_ERRMODE ) ); + } + + public function test_constructor_applies_fetch_column_default(): void { + $driver = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:path=:memory:;dbname=WordPress;', + null, + null, + array( PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_COLUMN ) + ); + + $this->assertSame( 'value', $driver->query( "SELECT 'value'" )->fetch() ); + } + + public function test_constructor_applies_persistent_option(): void { + $path = tempnam( sys_get_temp_dir(), 'wp_sqlite_' ); + unlink( $path ); + + try { + $driver = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:path=' . $path . ';dbname=WordPress;', + null, + null, + array( PDO::ATTR_PERSISTENT => true ) + ); + + $this->assertTrue( $driver->getAttribute( PDO::ATTR_PERSISTENT ) ); + } finally { + $this->remove_database_files( $path ); + } + } + + public function test_constructor_reports_stringify_fetches_from_injected_pdo(): void { + if ( PHP_VERSION_ID < 80200 ) { + $this->markTestSkipped( 'PDO SQLite cannot report PDO::ATTR_STRINGIFY_FETCHES before PHP 8.2.' ); + } + + $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; + $pdo = new $pdo_class( 'sqlite::memory:' ); + $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $driver = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:dbname=wp', + null, + null, + array( 'pdo' => $pdo ) + ); + + $this->assertTrue( $driver->getAttribute( PDO::ATTR_STRINGIFY_FETCHES ) ); + } + public function test_driver_exception_exposes_originating_driver(): void { $exception = new WP_MySQL_On_SQLite_Exception( $this->driver, 'Test error.' ); $this->assertSame( $this->driver, $exception->get_driver() ); + $this->assertSame( array( 'HY000', 1105, 'Test error.' ), $exception->errorInfo ); + } + + public function test_driver_exception_preserves_pdo_error_information(): void { + try { + $this->driver->query( 'SELECT * FROM missing_table' ); + $this->fail( 'Expected query() to throw an exception.' ); + } catch ( WP_MySQL_On_SQLite_Exception $exception ) { + $this->assertSame( 'HY000', $exception->errorInfo[0] ); + $this->assertSame( 1, $exception->errorInfo[1] ); + $this->assertSame( 'no such table: missing_table', $exception->errorInfo[2] ); + } + } + + public function test_emulated_driver_exception_exposes_mysql_error_information(): void { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + + try { + $this->driver->query( 'CREATE TABLE t (id INT)' ); + $this->fail( 'Expected query() to throw an exception.' ); + } catch ( WP_MySQL_On_SQLite_Exception $exception ) { + $this->assertSame( '42S01', $exception->getCode() ); + $this->assertSame( + array( '42S01', 1050, "Table 't' already exists" ), + $exception->errorInfo + ); + } } public function test_exposes_underlying_sqlite_pdo(): void { @@ -161,6 +283,152 @@ public function test_query(): void { } } + public function test_statement_query_string(): void { + $query = 'SELECT 1 AS value'; + $stmt = $this->driver->query( $query ); + + // Userland cannot initialize PDOStatement::$queryString before PHP 8.1. + $this->assertSame( PHP_VERSION_ID < 80100 ? null : $query, $stmt->queryString ); + } + + public function test_statement_column_metadata_is_snapshotted(): void { + $stmt = $this->driver->query( "SELECT 1 AS first, 'value' AS second" ); + + $this->assertSame( 'first', $stmt->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'second', $stmt->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'second', $stmt->getColumnMeta( '1' )['name'] ); + $this->assertFalse( $stmt->getColumnMeta( 2 ) ); + + $this->driver->query( 'SELECT 3 AS third' ); + + $this->assertSame( 'first', $stmt->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'second', $stmt->getColumnMeta( 1 )['name'] ); + } + + public function test_statement_column_metadata_rejects_negative_index(): void { + if ( PHP_VERSION_ID < 80000 ) { + $this->markTestSkipped( 'PDOStatement::getColumnMeta() throws ValueError on PHP 8.0 or newer.' ); + } + + $stmt = $this->driver->query( 'SELECT 1' ); + + $this->expectException( ValueError::class ); + $stmt->getColumnMeta( -1 ); + } + + public function test_statement_column_metadata_rejects_invalid_index_type(): void { + if ( PHP_VERSION_ID < 80000 ) { + $this->markTestSkipped( 'PDOStatement::getColumnMeta() throws TypeError on PHP 8.0 or newer.' ); + } + + $stmt = $this->driver->query( 'SELECT 1' ); + + $this->expectException( TypeError::class ); + $stmt->getColumnMeta( 'invalid' ); + } + + public function test_statement_column_metadata_is_resolved_lazily(): void { + $resolved_columns = array(); + $raw_column_meta = array( + array( 'name' => 'first' ), + array( 'name' => 'second' ), + ); + $stmt = new WP_MySQL_On_SQLite_Statement( + $this->driver->get_sqlite_pdo()->query( 'SELECT 1, 2' ), + 'SELECT 1, 2', + function ( $column ) use ( &$resolved_columns, $raw_column_meta ) { + if ( ! array_key_exists( $column, $raw_column_meta ) ) { + return false; + } + + $column_meta = $raw_column_meta[ $column ]; + $resolved_columns[] = $column_meta['name']; + return $column_meta; + } + ); + + $this->assertSame( array(), $resolved_columns ); + $this->assertSame( 'second', $stmt->getColumnMeta( 1 )['name'] ); + $this->assertSame( array( 'second' ), $resolved_columns ); + + $this->assertSame( 'second', $stmt->getColumnMeta( 1 )['name'] ); + $this->assertFalse( $stmt->getColumnMeta( 2 ) ); + $this->assertSame( array( 'second' ), $resolved_columns ); + + $this->assertSame( 'first', $stmt->getColumnMeta( 0 )['name'] ); + $this->assertSame( array( 'second', 'first' ), $resolved_columns ); + } + + public function test_statement_column_metadata_resolution_preserves_the_query_log(): void { + $this->driver->exec( 'CREATE TABLE metadata_test (id INT)' ); + $this->driver->exec( 'INSERT INTO metadata_test VALUES (1)' ); + $stmt = $this->driver->query( 'SELECT id FROM metadata_test' ); + $last_sqlite_queries = $this->driver->get_last_sqlite_queries(); + + $this->assertSame( 'id', $stmt->getColumnMeta( 0 )['name'] ); + $this->assertSame( $last_sqlite_queries, $this->driver->get_last_sqlite_queries() ); + } + + public function test_statement_column_metadata_snapshots_database_context(): void { + $stmt = $this->driver->query( 'SELECT 1 AS value' ); + $this->driver->exec( 'USE information_schema' ); + + $this->assertSame( 'wp', $stmt->getColumnMeta( 0 )['mysqli:db'] ); + } + + public function test_statement_error_information(): void { + $stmt = $this->driver->query( 'SELECT 1' ); + + $this->assertSame( '00000', $stmt->errorCode() ); + $this->assertSame( array( '00000', null, null ), $stmt->errorInfo() ); + } + + public function test_statement_error_information_discards_stale_sqlite_error(): void { + $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; + $pdo = new $pdo_class( 'sqlite::memory:' ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + $pdo->query( 'SELECT * FROM missing_table' ); + + $stmt = new WP_MySQL_On_SQLite_Statement( + $pdo->query( 'SELECT 1' ), + 'SELECT 1', + function () { + return false; + } + ); + + $this->assertSame( '00000', $stmt->errorCode() ); + $this->assertSame( array( '00000', null, null ), $stmt->errorInfo() ); + } + + public function test_statement_iteration(): void { + $stmt = $this->driver->query( 'SELECT 1 AS value UNION ALL SELECT 2', PDO::FETCH_ASSOC ); + + $this->assertSame( + array( + array( 'value' => '1' ), + array( 'value' => '2' ), + ), + iterator_to_array( $stmt ) + ); + } + + public function test_statement_close_cursor(): void { + $stmt = $this->driver->query( 'SELECT 1 UNION ALL SELECT 2' ); + + $this->assertTrue( $stmt->closeCursor() ); + $this->assertFalse( $stmt->fetch() ); + } + + public function test_statement_bind_column(): void { + $stmt = $this->driver->query( 'SELECT 1 AS value' ); + $value = null; + + $this->assertTrue( $stmt->bindColumn( 'value', $value ) ); + $this->assertTrue( $stmt->fetch( PDO::FETCH_BOUND ) ); + $this->assertSame( '1', $value ); + } + /** * @dataProvider data_pdo_fetch_methods */ @@ -319,6 +587,83 @@ public function test_exec(): void { $this->assertEquals( 0, $result ); } + public function test_last_insert_id(): void { + $this->assertSame( '0', $this->driver->lastInsertId() ); + + $this->driver->query( 'CREATE TABLE t (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY)' ); + $this->assertSame( '0', $this->driver->lastInsertId() ); + + $this->driver->query( 'INSERT INTO t (id) VALUES (NULL)' ); + + $this->assertSame( '1', $this->driver->lastInsertId() ); + $this->assertSame( '1', $this->driver->lastInsertId( 'ignored_sequence_name' ) ); + + $this->driver->query( 'CREATE TABLE another_table (id INT)' ); + $this->assertSame( '0', $this->driver->lastInsertId() ); + } + + public function test_last_insert_id_rejects_invalid_sequence_name(): void { + if ( PHP_VERSION_ID < 80000 ) { + $result = @$this->driver->lastInsertId( array() ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + + $this->assertFalse( $result ); + $this->assertSame( 'PDO::lastInsertId() expects parameter 1 to be string, array given', error_get_last()['message'] ); + return; + } + + $this->expectException( TypeError::class ); + $this->expectExceptionMessage( 'PDO::lastInsertId(): Argument #1 ($name) must be of type ?string, array given' ); + $this->driver->lastInsertId( array() ); + } + + public function test_connection_error_information(): void { + $this->assertNull( $this->driver->errorCode() ); + $this->assertSame( array( '', null, null ), $this->driver->errorInfo() ); + + $this->driver->query( 'SELECT 1' ); + + $this->assertSame( '00000', $this->driver->errorCode() ); + $this->assertSame( + array( '00000', null, null ), + $this->driver->errorInfo() + ); + } + + public function test_silent_error_mode(): void { + $this->driver->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + + $this->assertFalse( $this->driver->query( 'SELECT * FROM missing_table' ) ); + $this->assertSame( 'HY000', $this->driver->errorCode() ); + $this->assertSame( 1, $this->driver->errorInfo()[1] ); + $this->assertSame( 'no such table: missing_table', $this->driver->errorInfo()[2] ); + $this->assertFalse( $this->driver->exec( 'SELECT * FROM missing_table' ) ); + + $this->assertInstanceOf( PDOStatement::class, $this->driver->query( 'SELECT 1' ) ); + $this->assertSame( '00000', $this->driver->errorCode() ); + } + + public function test_warning_error_mode(): void { + $this->driver->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING ); + $warning = null; + set_error_handler( + function ( $level, $message ) use ( &$warning ) { + if ( E_USER_WARNING === $level ) { + $warning = $message; + return true; + } + return false; + } + ); + + try { + $this->assertFalse( $this->driver->query( 'SELECT * FROM missing_table' ) ); + } finally { + restore_error_handler(); + } + + $this->assertStringContainsString( 'no such table: missing_table', $warning ); + } + public function test_quote_matches_mysql_escaping(): void { $backslash = chr( 92 ); $value = chr( 0 ) . "\n\r{$backslash}'\"" . chr( 26 ) . "\tƮềʂᴛ🙂"; @@ -417,6 +762,34 @@ public function test_rollback_no_active_transaction(): void { $this->driver->rollBack(); } + public function test_transaction_methods_flush_operation_state(): void { + $this->driver->query( 'CREATE TABLE t (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY)' ); + $this->driver->query( 'INSERT INTO t (id) VALUES (NULL)' ); + + $this->assertSame( '1', $this->driver->lastInsertId() ); + $this->assertTrue( $this->driver->beginTransaction() ); + $this->assertSame( '0', $this->driver->lastInsertId() ); + $this->assertSame( '', $this->driver->get_last_mysql_query() ); + $this->assertSame( array( 'BEGIN IMMEDIATE' ), array_column( $this->driver->get_last_sqlite_queries(), 'sql' ) ); + + $this->driver->query( 'INSERT INTO t (id) VALUES (NULL)' ); + + $this->assertSame( '2', $this->driver->lastInsertId() ); + $this->assertTrue( $this->driver->commit() ); + $this->assertSame( '0', $this->driver->lastInsertId() ); + $this->assertSame( '', $this->driver->get_last_mysql_query() ); + $this->assertSame( array( 'COMMIT' ), array_column( $this->driver->get_last_sqlite_queries(), 'sql' ) ); + + $this->driver->beginTransaction(); + $this->driver->query( 'INSERT INTO t (id) VALUES (NULL)' ); + + $this->assertSame( '3', $this->driver->lastInsertId() ); + $this->assertTrue( $this->driver->rollBack() ); + $this->assertSame( '0', $this->driver->lastInsertId() ); + $this->assertSame( '', $this->driver->get_last_mysql_query() ); + $this->assertSame( array( 'ROLLBACK' ), array_column( $this->driver->get_last_sqlite_queries(), 'sql' ) ); + } + public function test_fetch_default(): void { // Default fetch mode is PDO::FETCH_BOTH. $result = $this->driver->query( "SELECT 1, 'abc', 2" ); @@ -586,6 +959,7 @@ public function test_attr_default_fetch_mode(): void { public function test_attr_stringify_fetches(): void { $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $this->assertTrue( $this->driver->getAttribute( PDO::ATTR_STRINGIFY_FETCHES ) ); $result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" ); $this->assertSame( array( '123', '1.23', 'abc', '1', '0' ), @@ -593,6 +967,7 @@ public function test_attr_stringify_fetches(): void { ); $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false ); + $this->assertFalse( $this->driver->getAttribute( PDO::ATTR_STRINGIFY_FETCHES ) ); $result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" ); $this->assertSame( /* diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php index bd6d1351c..f39b9da0f 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php @@ -25,6 +25,9 @@ class WP_MySQL_On_SQLite_Tests extends TestCase { /** @var mixed */ private $last_result; + /** @var WP_MySQL_On_SQLite_Statement */ + private $last_statement; + // Before each test, we create a new database public function setUp(): void { $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; @@ -71,7 +74,8 @@ private function assertQueryError( $sql, $error_message ) { } private function query( $sql ) { - $statement = $this->engine->query( $sql, PDO::FETCH_OBJ ); + $statement = $this->engine->query( $sql, PDO::FETCH_OBJ ); + $this->last_statement = $statement; if ( $statement->columnCount() > 0 ) { $this->last_result = $statement->fetchAll(); } else { @@ -80,6 +84,15 @@ private function query( $sql ) { return $this->last_result; } + private function getLastColumnMeta(): array { + $column_meta = array(); + $column_count = $this->last_statement->columnCount(); + for ( $i = 0; $i < $column_count; $i++ ) { + $column_meta[] = $this->last_statement->getColumnMeta( $i ); + } + return $column_meta; + } + public function testRegexp() { $this->assertQuery( "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');" @@ -5093,10 +5106,10 @@ public function testLastInsertId(): void { ); $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" ); - $this->assertEquals( 1, $this->engine->get_insert_id() ); + $this->assertSame( '1', $this->engine->lastInsertId() ); $this->assertQuery( "INSERT INTO t (name) VALUES ('b')" ); - $this->assertEquals( 2, $this->engine->get_insert_id() ); + $this->assertSame( '2', $this->engine->lastInsertId() ); } public function testCharLength(): void { @@ -6284,7 +6297,7 @@ public function testCreateTableDuplicateKeyName(): void { $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); $this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() ); - $this->assertSame( '42S21', $exception->getCode() ); + $this->assertSame( '42000', $exception->getCode() ); } public function testCreateTableDuplicateKeyNameWithUnique(): void { @@ -6297,7 +6310,7 @@ public function testCreateTableDuplicateKeyNameWithUnique(): void { $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); $this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() ); - $this->assertSame( '42S21', $exception->getCode() ); + $this->assertSame( '42000', $exception->getCode() ); } public function testCreateTableDuplicateKeyNameWithPrimaryKey(): void { @@ -6344,7 +6357,7 @@ public function testAlterTableDuplicateKeyName(): void { $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); $this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() ); - $this->assertSame( '42S21', $exception->getCode() ); + $this->assertSame( '42000', $exception->getCode() ); } public function testAlterTableDuplicateKeyNameWithMultipleOperations(): void { @@ -6358,7 +6371,7 @@ public function testAlterTableDuplicateKeyNameWithMultipleOperations(): void { $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); $this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() ); - $this->assertSame( '42S21', $exception->getCode() ); + $this->assertSame( '42000', $exception->getCode() ); } public function testAlterTableDuplicateKeyNameWithUnique(): void { @@ -6372,7 +6385,7 @@ public function testAlterTableDuplicateKeyNameWithUnique(): void { $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); $this->assertSame( "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name 'idx'", $exception->getMessage() ); - $this->assertSame( '42S21', $exception->getCode() ); + $this->assertSame( '42000', $exception->getCode() ); } public function testConstraintName(): void { @@ -8303,9 +8316,9 @@ public function testColumnInfo(): void { $this->assertQuery( "INSERT INTO t VALUES (1, 'name', 1.1, B'01101001')" ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 4, $this->engine->get_last_column_count() ); + $this->assertEquals( 4, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertCount( 4, $column_info ); $this->assertSame( @@ -8410,9 +8423,9 @@ public function testColumnInfoWithConstraints(): void { $this->assertQuery( 'INSERT INTO t VALUES (1, "slug", 1)' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 3, $this->engine->get_last_column_count() ); + $this->assertEquals( 3, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -8493,9 +8506,9 @@ public function testColumnInfoForIntegerDataTypes(): void { $this->assertQuery( 'INSERT INTO t VALUES (0, 1, 2, 3, 4, 5, 6)' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 7, $this->engine->get_last_column_count() ); + $this->assertEquals( 7, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -8644,9 +8657,9 @@ public function testColumnInfoForUnsignedIntegerDataTypes(): void { $this->assertQuery( 'INSERT INTO t VALUES (1, 2, 3, 4, 5)' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 5, $this->engine->get_last_column_count() ); + $this->assertEquals( 5, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -8761,9 +8774,9 @@ public function testColumnInfoForFloatingPointDataTypes(): void { $this->assertQuery( 'INSERT INTO t VALUES (1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7)' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 7, $this->engine->get_last_column_count() ); + $this->assertEquals( 7, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -8918,9 +8931,9 @@ public function testColumnInfoForStringDataTypes(): void { $this->assertQuery( 'INSERT INTO t VALUES ("a", "b", "c", "d", "e", "f", "g", "h", "a", "b", "{}")' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 11, $this->engine->get_last_column_count() ); + $this->assertEquals( 11, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -9141,9 +9154,9 @@ public function testColumnInfoForDateAndTimeDataTypes(): void { $this->assertQuery( 'INSERT INTO t VALUES ("2024-01-01", "12:00:00", "2024-01-01 12:00:00", "2024-01-01 12:00:00", 2024)' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 5, $this->engine->get_last_column_count() ); + $this->assertEquals( 5, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -9257,9 +9270,9 @@ public function testColumnInfoForBinaryDataTypes(): void { $this->assertQuery( "INSERT INTO t VALUES (B'01000001', B'01101001', B'10101010', B'01010101', B'10000000', B'11111111')" ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 6, $this->engine->get_last_column_count() ); + $this->assertEquals( 6, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -9406,9 +9419,9 @@ public function testColumnInfoForSpatialDataTypes(): void { ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 9, $this->engine->get_last_column_count() ); + $this->assertEquals( 9, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -9606,9 +9619,9 @@ public function testColumnInfoForExpressions(): void { (SELECT 1) AS col_expr_20 FROM t" ); - $this->assertEquals( 20, $this->engine->get_last_column_count() ); + $this->assertEquals( 20, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertSame( array( @@ -10005,9 +10018,9 @@ public function testColumnInfoWithZeroRows(): void { CASE WHEN col_int < 5 THEN 'string' ELSE 123 END AS col_expr_4 FROM t" ); - $this->assertEquals( 16, $this->engine->get_last_column_count() ); + $this->assertEquals( 16, $this->last_statement->columnCount() ); - $column_info = $this->engine->get_last_column_meta(); + $column_info = $this->getLastColumnMeta(); $this->assertCount( 16, $column_info ); $this->assertSame( @@ -10313,8 +10326,8 @@ public function testColumnInfoWithZeroRowsPhpBug(): void { $this->assertQuery( 'CREATE TABLE t ( id INT )' ); $this->assertQuery( 'SELECT * FROM t' ); - $this->assertEquals( 1, $this->engine->get_last_column_count() ); - $column_info = $this->engine->get_last_column_meta(); + $this->assertEquals( 1, $this->last_statement->columnCount() ); + $column_info = $this->getLastColumnMeta(); $this->assertCount( 1, $column_info ); $this->assertSame( array( @@ -11284,242 +11297,242 @@ public function testNonEmptyColumnMeta(): void { // SELECT $this->assertQuery( 'SELECT * FROM t' ); - $this->assertSame( 1, $this->engine->get_last_column_count() ); - $this->assertSame( 'id', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 1, $this->last_statement->columnCount() ); + $this->assertSame( 'id', $this->last_statement->getColumnMeta( 0 )['name'] ); // SHOW COLLATION $this->assertQuery( 'SHOW COLLATION' ); - $this->assertSame( 7, $this->engine->get_last_column_count() ); - $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Charset', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Id', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Default', $this->engine->get_last_column_meta()[3]['name'] ); - $this->assertSame( 'Compiled', $this->engine->get_last_column_meta()[4]['name'] ); - $this->assertSame( 'Sortlen', $this->engine->get_last_column_meta()[5]['name'] ); - $this->assertSame( 'Pad_attribute', $this->engine->get_last_column_meta()[6]['name'] ); + $this->assertSame( 7, $this->last_statement->columnCount() ); + $this->assertSame( 'Collation', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Charset', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Id', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Default', $this->last_statement->getColumnMeta( 3 )['name'] ); + $this->assertSame( 'Compiled', $this->last_statement->getColumnMeta( 4 )['name'] ); + $this->assertSame( 'Sortlen', $this->last_statement->getColumnMeta( 5 )['name'] ); + $this->assertSame( 'Pad_attribute', $this->last_statement->getColumnMeta( 6 )['name'] ); // SHOW DATABASES $this->assertQuery( 'SHOW DATABASES' ); - $this->assertSame( 1, $this->engine->get_last_column_count() ); - $this->assertSame( 'Database', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 1, $this->last_statement->columnCount() ); + $this->assertSame( 'Database', $this->last_statement->getColumnMeta( 0 )['name'] ); // SHOW CREATE TABLE $this->assertQuery( 'SHOW CREATE TABLE t' ); - $this->assertSame( 2, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Create Table', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 2, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Create Table', $this->last_statement->getColumnMeta( 1 )['name'] ); // SHOW TABLE STATUS $this->assertQuery( 'SHOW TABLE STATUS' ); - $this->assertSame( 18, $this->engine->get_last_column_count() ); - $this->assertSame( 'Name', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Engine', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Version', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Row_format', $this->engine->get_last_column_meta()[3]['name'] ); - $this->assertSame( 'Rows', $this->engine->get_last_column_meta()[4]['name'] ); - $this->assertSame( 'Avg_row_length', $this->engine->get_last_column_meta()[5]['name'] ); - $this->assertSame( 'Data_length', $this->engine->get_last_column_meta()[6]['name'] ); - $this->assertSame( 'Max_data_length', $this->engine->get_last_column_meta()[7]['name'] ); - $this->assertSame( 'Index_length', $this->engine->get_last_column_meta()[8]['name'] ); - $this->assertSame( 'Data_free', $this->engine->get_last_column_meta()[9]['name'] ); - $this->assertSame( 'Auto_increment', $this->engine->get_last_column_meta()[10]['name'] ); - $this->assertSame( 'Create_time', $this->engine->get_last_column_meta()[11]['name'] ); - $this->assertSame( 'Update_time', $this->engine->get_last_column_meta()[12]['name'] ); - $this->assertSame( 'Check_time', $this->engine->get_last_column_meta()[13]['name'] ); - $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[14]['name'] ); - $this->assertSame( 'Checksum', $this->engine->get_last_column_meta()[15]['name'] ); - $this->assertSame( 'Create_options', $this->engine->get_last_column_meta()[16]['name'] ); - $this->assertSame( 'Comment', $this->engine->get_last_column_meta()[17]['name'] ); + $this->assertSame( 18, $this->last_statement->columnCount() ); + $this->assertSame( 'Name', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Engine', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Version', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Row_format', $this->last_statement->getColumnMeta( 3 )['name'] ); + $this->assertSame( 'Rows', $this->last_statement->getColumnMeta( 4 )['name'] ); + $this->assertSame( 'Avg_row_length', $this->last_statement->getColumnMeta( 5 )['name'] ); + $this->assertSame( 'Data_length', $this->last_statement->getColumnMeta( 6 )['name'] ); + $this->assertSame( 'Max_data_length', $this->last_statement->getColumnMeta( 7 )['name'] ); + $this->assertSame( 'Index_length', $this->last_statement->getColumnMeta( 8 )['name'] ); + $this->assertSame( 'Data_free', $this->last_statement->getColumnMeta( 9 )['name'] ); + $this->assertSame( 'Auto_increment', $this->last_statement->getColumnMeta( 10 )['name'] ); + $this->assertSame( 'Create_time', $this->last_statement->getColumnMeta( 11 )['name'] ); + $this->assertSame( 'Update_time', $this->last_statement->getColumnMeta( 12 )['name'] ); + $this->assertSame( 'Check_time', $this->last_statement->getColumnMeta( 13 )['name'] ); + $this->assertSame( 'Collation', $this->last_statement->getColumnMeta( 14 )['name'] ); + $this->assertSame( 'Checksum', $this->last_statement->getColumnMeta( 15 )['name'] ); + $this->assertSame( 'Create_options', $this->last_statement->getColumnMeta( 16 )['name'] ); + $this->assertSame( 'Comment', $this->last_statement->getColumnMeta( 17 )['name'] ); // SHOW TABLES $this->assertQuery( 'SHOW TABLES' ); - $this->assertSame( 1, $this->engine->get_last_column_count() ); - $this->assertSame( 'Tables_in_wp', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 1, $this->last_statement->columnCount() ); + $this->assertSame( 'Tables_in_wp', $this->last_statement->getColumnMeta( 0 )['name'] ); // SHOW FULL TABLES $this->assertQuery( 'SHOW FULL TABLES' ); - $this->assertSame( 2, $this->engine->get_last_column_count() ); - $this->assertSame( 'Tables_in_wp', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Table_type', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 2, $this->last_statement->columnCount() ); + $this->assertSame( 'Tables_in_wp', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Table_type', $this->last_statement->getColumnMeta( 1 )['name'] ); // SHOW COLUMNS $this->assertQuery( 'SHOW COLUMNS FROM t' ); - $this->assertSame( 6, $this->engine->get_last_column_count() ); - $this->assertSame( 'Field', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Type', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Null', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Key', $this->engine->get_last_column_meta()[3]['name'] ); - $this->assertSame( 'Default', $this->engine->get_last_column_meta()[4]['name'] ); - $this->assertSame( 'Extra', $this->engine->get_last_column_meta()[5]['name'] ); + $this->assertSame( 6, $this->last_statement->columnCount() ); + $this->assertSame( 'Field', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Type', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Null', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Key', $this->last_statement->getColumnMeta( 3 )['name'] ); + $this->assertSame( 'Default', $this->last_statement->getColumnMeta( 4 )['name'] ); + $this->assertSame( 'Extra', $this->last_statement->getColumnMeta( 5 )['name'] ); // SHOW INDEX $this->assertQuery( 'SHOW INDEX FROM t' ); - $this->assertSame( 15, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Non_unique', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Key_name', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Seq_in_index', $this->engine->get_last_column_meta()[3]['name'] ); - $this->assertSame( 'Column_name', $this->engine->get_last_column_meta()[4]['name'] ); - $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[5]['name'] ); - $this->assertSame( 'Cardinality', $this->engine->get_last_column_meta()[6]['name'] ); - $this->assertSame( 'Sub_part', $this->engine->get_last_column_meta()[7]['name'] ); - $this->assertSame( 'Packed', $this->engine->get_last_column_meta()[8]['name'] ); - $this->assertSame( 'Null', $this->engine->get_last_column_meta()[9]['name'] ); - $this->assertSame( 'Index_type', $this->engine->get_last_column_meta()[10]['name'] ); - $this->assertSame( 'Comment', $this->engine->get_last_column_meta()[11]['name'] ); - $this->assertSame( 'Index_comment', $this->engine->get_last_column_meta()[12]['name'] ); - $this->assertSame( 'Visible', $this->engine->get_last_column_meta()[13]['name'] ); - $this->assertSame( 'Expression', $this->engine->get_last_column_meta()[14]['name'] ); + $this->assertSame( 15, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Non_unique', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Key_name', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Seq_in_index', $this->last_statement->getColumnMeta( 3 )['name'] ); + $this->assertSame( 'Column_name', $this->last_statement->getColumnMeta( 4 )['name'] ); + $this->assertSame( 'Collation', $this->last_statement->getColumnMeta( 5 )['name'] ); + $this->assertSame( 'Cardinality', $this->last_statement->getColumnMeta( 6 )['name'] ); + $this->assertSame( 'Sub_part', $this->last_statement->getColumnMeta( 7 )['name'] ); + $this->assertSame( 'Packed', $this->last_statement->getColumnMeta( 8 )['name'] ); + $this->assertSame( 'Null', $this->last_statement->getColumnMeta( 9 )['name'] ); + $this->assertSame( 'Index_type', $this->last_statement->getColumnMeta( 10 )['name'] ); + $this->assertSame( 'Comment', $this->last_statement->getColumnMeta( 11 )['name'] ); + $this->assertSame( 'Index_comment', $this->last_statement->getColumnMeta( 12 )['name'] ); + $this->assertSame( 'Visible', $this->last_statement->getColumnMeta( 13 )['name'] ); + $this->assertSame( 'Expression', $this->last_statement->getColumnMeta( 14 )['name'] ); // SHOW GRANTS $this->assertQuery( 'SHOW GRANTS' ); - $this->assertSame( 1, $this->engine->get_last_column_count() ); - $this->assertSame( 'Grants for root@%', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 1, $this->last_statement->columnCount() ); + $this->assertSame( 'Grants for root@%', $this->last_statement->getColumnMeta( 0 )['name'] ); // SHOW VARIABLES $this->assertQuery( 'SHOW VARIABLES' ); - $this->assertSame( 2, $this->engine->get_last_column_count() ); - $this->assertSame( 'Variable_name', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Value', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 2, $this->last_statement->columnCount() ); + $this->assertSame( 'Variable_name', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Value', $this->last_statement->getColumnMeta( 1 )['name'] ); // DESCRIBE/EXPLAIN $this->assertQuery( 'DESCRIBE t' ); - $this->assertSame( 6, $this->engine->get_last_column_count() ); - $this->assertSame( 'Field', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Type', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Null', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Key', $this->engine->get_last_column_meta()[3]['name'] ); - $this->assertSame( 'Default', $this->engine->get_last_column_meta()[4]['name'] ); - $this->assertSame( 'Extra', $this->engine->get_last_column_meta()[5]['name'] ); + $this->assertSame( 6, $this->last_statement->columnCount() ); + $this->assertSame( 'Field', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Type', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Null', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Key', $this->last_statement->getColumnMeta( 3 )['name'] ); + $this->assertSame( 'Default', $this->last_statement->getColumnMeta( 4 )['name'] ); + $this->assertSame( 'Extra', $this->last_statement->getColumnMeta( 5 )['name'] ); // ANALYZE TABLE $this->assertQuery( 'ANALYZE TABLE t' ); - $this->assertSame( 4, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 4, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Op', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Msg_type', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Msg_text', $this->last_statement->getColumnMeta( 3 )['name'] ); // CHECK TABLE $this->assertQuery( 'CHECK TABLE t' ); - $this->assertSame( 4, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 4, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Op', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Msg_type', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Msg_text', $this->last_statement->getColumnMeta( 3 )['name'] ); // OPTIMIZE TABLE $this->assertQuery( 'OPTIMIZE TABLE t' ); - $this->assertSame( 4, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 4, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Op', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Msg_type', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Msg_text', $this->last_statement->getColumnMeta( 3 )['name'] ); // REPAIR TABLE $this->assertQuery( 'REPAIR TABLE t' ); - $this->assertSame( 4, $this->engine->get_last_column_count() ); - $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); - $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); - $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); - $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 4, $this->last_statement->columnCount() ); + $this->assertSame( 'Table', $this->last_statement->getColumnMeta( 0 )['name'] ); + $this->assertSame( 'Op', $this->last_statement->getColumnMeta( 1 )['name'] ); + $this->assertSame( 'Msg_type', $this->last_statement->getColumnMeta( 2 )['name'] ); + $this->assertSame( 'Msg_text', $this->last_statement->getColumnMeta( 3 )['name'] ); } public function testEmptyColumnMeta(): void { // CREATE TABLE $this->assertQuery( 'CREATE TABLE t (id INT)' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // INSERT $this->assertQuery( 'INSERT INTO t (id) VALUES (1)' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // REPLACE $this->assertQuery( 'UPDATE t SET id = 1' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // DELETE $this->assertQuery( 'DELETE FROM t' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // TRUNCATE TABLE $this->assertQuery( 'TRUNCATE TABLE t' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // START TRANSACTION $this->assertQuery( 'START TRANSACTION' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // COMMIT $this->assertQuery( 'COMMIT' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // ROLLBACK $this->assertQuery( 'ROLLBACK' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // SAVEPOINT $this->assertQuery( 'SAVEPOINT s1' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // ROLLBACK TO SAVEPOINT $this->assertQuery( 'ROLLBACK TO SAVEPOINT s1' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // RELEASE SAVEPOINT $this->assertQuery( 'RELEASE SAVEPOINT s1' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // LOCK TABLE $this->assertQuery( 'LOCK TABLES t READ' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // UNLOCK TABLE $this->assertQuery( 'UNLOCK TABLES' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // ALTER TABLE $this->assertQuery( 'ALTER TABLE t ADD COLUMN name VARCHAR(255)' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // CREATE INDEX $this->assertQuery( 'CREATE INDEX idx_name ON t (name)' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // DROP INDEX $this->assertQuery( 'DROP INDEX idx_name ON t' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // DROP TABLE $this->assertQuery( 'DROP TABLE t' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // USE $this->assertQuery( 'USE wp' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); // SET $this->assertQuery( 'SET @my_var = 1' ); - $this->assertSame( 0, $this->engine->get_last_column_count() ); - $this->assertSame( array(), $this->engine->get_last_column_meta() ); + $this->assertSame( 0, $this->last_statement->columnCount() ); + $this->assertSame( array(), $this->getLastColumnMeta() ); } public function testCastValuesOnInsert(): void { diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_DB_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_DB_Tests.php index 637bff457..c2ba0b8e1 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_DB_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_DB_Tests.php @@ -43,6 +43,33 @@ public function __construct() { $wpdb->get_driver(); } + public function test_load_col_info_without_result(): void { + $wpdb = new class( $this->driver ) extends WP_SQLite_DB { + public $col_info; + public $last_error; + public $last_query; + public $last_result; + public $num_rows; + public $result; + public $rows_affected; + + public function __construct( WP_MySQL_On_SQLite $driver ) { + $this->dbh = $driver; + } + + public function get_loaded_col_info(): array { + $this->load_col_info(); + return $this->col_info; + } + }; + + $this->assertSame( array(), $wpdb->get_loaded_col_info() ); + + $wpdb->flush(); + + $this->assertSame( array(), $wpdb->get_loaded_col_info() ); + } + /** * @dataProvider dataMysqlEscaping */ diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Compatibility_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Compatibility_Tests.php index 4db88f7a9..fe25e6604 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Compatibility_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Compatibility_Tests.php @@ -28,7 +28,10 @@ function () { WP_SQLite_Driver::class ); - $this->assertInstanceOf( WP_MySQL_On_SQLite::class, $get_driver() ); + $mysql_on_sqlite_driver = $get_driver(); + + $this->assertInstanceOf( WP_MySQL_On_SQLite::class, $mysql_on_sqlite_driver ); + $this->assertTrue( $mysql_on_sqlite_driver->getAttribute( PDO::ATTR_STRINGIFY_FETCHES ) ); $this->assertSame( $this->sqlite, $this->driver->get_connection()->get_pdo() ); $this->assertSame( $this->driver->get_sqlite_version(), $this->driver->client_info ); $this->assertSame( SQLITE_DRIVER_VERSION, $this->driver->get_saved_driver_version() ); @@ -52,7 +55,9 @@ public function test_preserves_legacy_query_results(): void { $this->assertSame( $result, $this->driver->get_query_results() ); $this->assertSame( $result, $this->driver->get_last_return_value() ); $this->assertSame( 2, $this->driver->get_last_column_count() ); - $this->assertCount( 2, $this->driver->get_last_column_meta() ); + $this->assertSame( array( 'id', 'value' ), array_column( $this->driver->get_last_column_meta(), 'name' ) ); + $this->assertFalse( method_exists( WP_MySQL_On_SQLite::class, 'get_last_column_count' ) ); + $this->assertFalse( method_exists( WP_MySQL_On_SQLite::class, 'get_last_column_meta' ) ); } public function test_delegates_diagnostics_and_native_queries(): void { diff --git a/packages/mysql-proxy/src/Adapter/class-sqlite-adapter.php b/packages/mysql-proxy/src/Adapter/class-sqlite-adapter.php index bd76f561a..5b785d860 100644 --- a/packages/mysql-proxy/src/Adapter/class-sqlite-adapter.php +++ b/packages/mysql-proxy/src/Adapter/class-sqlite-adapter.php @@ -4,6 +4,7 @@ use PDO; use PDOException; +use PDOStatement; use Throwable; use WP_MySQL_Proxy\MySQL_Result; use WP_MySQL_On_SQLite; @@ -33,14 +34,14 @@ public function handle_query( string $query ): MySQL_Result { try { $statement = $this->sqlite_driver->query( $query, PDO::FETCH_OBJ ); - $last_insert_id = $this->sqlite_driver->get_insert_id() ?? null; + $last_insert_id = (int) $this->sqlite_driver->lastInsertId(); if ( $statement->columnCount() > 0 ) { $rows = $statement->fetchAll(); } else { $affected_rows = $statement->rowCount(); } - if ( $this->sqlite_driver->get_last_column_count() > 0 ) { - $columns = $this->computeColumnInfo(); + if ( $statement->columnCount() > 0 ) { + $columns = $this->computeColumnInfo( $statement ); } return MySQL_Result::from_data( $affected_rows, $last_insert_id, $columns, $rows ?? array() ); } catch ( Throwable $e ) { @@ -52,10 +53,13 @@ public function handle_query( string $query ): MySQL_Result { } } - public function computeColumnInfo() { + public function computeColumnInfo( PDOStatement $statement ) { $columns = array(); - $column_meta = $this->sqlite_driver->get_last_column_meta(); + $column_meta = array(); + for ( $i = 0; $i < $statement->columnCount(); $i++ ) { + $column_meta[] = $statement->getColumnMeta( $i ); + } $types = array( 'DECIMAL' => MySQL_Protocol::FIELD_TYPE_DECIMAL, diff --git a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php index 8b936219a..ada0cf207 100644 --- a/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/packages/plugin-sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php @@ -617,7 +617,7 @@ public function query( $query ) { // Take note of the insert_id. if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { - $this->insert_id = $this->dbh->get_insert_id(); + $this->insert_id = (int) $this->dbh->lastInsertId(); } // Return number of rows affected. @@ -708,7 +708,11 @@ protected function load_col_info() { return; } $this->col_info = array(); - foreach ( $this->dbh->get_last_column_meta() as $column ) { + if ( null === $this->result ) { + return; + } + for ( $i = 0; $i < $this->result->columnCount(); $i++ ) { + $column = $this->result->getColumnMeta( $i ); $this->col_info[] = (object) array( 'name' => $column['name'], 'orgname' => $column['mysqli:orgname'],