pdo = $options['pdo']; } else { if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) { throw new InvalidArgumentException( 'Option "path" is required when "pdo" is not provided.' ); } $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. $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); // Configure SQLite timeout. if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) { $timeout = $options['timeout']; } else { $timeout = self::DEFAULT_SQLITE_TIMEOUT; } $this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout ); // Configure SQLite journal mode. Default to WAL for best throughput. $effective_journal_mode = null; $journal_mode = $options['journal_mode'] ?? 'WAL'; if ( is_string( $journal_mode ) ) { $journal_mode = strtoupper( $journal_mode ); } if ( ! in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) { throw new InvalidArgumentException( sprintf( 'Invalid SQLite journal mode: %s.', $options['journal_mode'] ) ); } try { $effective_journal_mode = strtoupper( (string) $this->query( 'PRAGMA journal_mode = ' . $journal_mode )->fetchColumn() ); } catch ( PDOException $e ) { // WAL may be unavailable in some environments, such as on network // filesystems. When it is explicitly configured, surface the error. // Otherwise, fall back to the default SQLite behavior. if ( isset( $options['journal_mode'] ) ) { throw $e; } } /* * Configure SQLite synchronous setting. Default to NORMAL for WAL mode. * * WAL improves read/write concurrency and "synchronous = NORMAL" avoids * frequent sync to the main database, which could become a bottleneck. * In WAL mode, NORMAL is safe and recommended. From the SQLite docs: * * The synchronous=NORMAL setting provides the best balance between * performance and safety for most applications running in WAL mode. * You lose durability across power loss with synchronous NORMAL in WAL * mode, but that is not important for most applications. Transactions * are still atomic, consistent, and isolated, which are the most * important characteristics in most use cases. * * SQLite defaults to "synchronous = FULL" to avoid data corruption with * other journal modes. With WAL, this is not necessary. * * See: https://sqlite.org/pragma.html#pragma_synchronous */ $synchronous = $options['synchronous'] ?? null; if ( isset( $synchronous ) ) { // Validate and normalize explicitly provided synchronous value. if ( is_int( $synchronous ) && isset( self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ] ) ) { $synchronous = self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ]; } elseif ( is_string( $synchronous ) ) { $synchronous = strtoupper( $synchronous ); } if ( ! in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) { throw new InvalidArgumentException( sprintf( 'Invalid SQLite synchronous setting: %s.', $options['synchronous'] ) ); } } elseif ( 'WAL' === $effective_journal_mode ) { // Default to NORMAL for WAL mode. $synchronous = 'NORMAL'; } if ( in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) { $this->query( 'PRAGMA synchronous = ' . $synchronous ); } } /** * Execute a query in SQLite. * * @param string $sql The query to execute. * @param array $params The query parameters. * @throws PDOException When the query execution fails. * @return PDOStatement The PDO statement object. */ public function query( string $sql, array $params = array() ): PDOStatement { if ( $this->query_logger ) { ( $this->query_logger )( $sql, $params ); } $stmt = $this->pdo->prepare( $sql ); $stmt->execute( $params ); return $stmt; } /** * Prepare a SQLite query for execution. * * @param string $sql The query to prepare. * @return PDOStatement The prepared statement. * @throws PDOException When the query preparation fails. */ public function prepare( string $sql ): PDOStatement { if ( $this->query_logger ) { ( $this->query_logger )( $sql, array() ); } return $this->pdo->prepare( $sql ); } /** * Returns the ID of the last inserted row. * * @return string The ID of the last inserted row. */ public function get_last_insert_id(): string { return $this->pdo->lastInsertId(); } /** * Quote a value for use in a query. * * @param mixed $value The value to quote. * @param int $type The type of the value. * @return string The quoted value. */ public function quote( $value, int $type = PDO::PARAM_STR ): string { return $this->pdo->quote( $value, $type ); } /** * Quote an SQLite identifier. * * Wraps the identifier in backticks and escapes backtick characters within. * * --- * * Quoted identifiers in SQLite are represented by string constants: * * A string constant is formed by enclosing the string in single quotes ('). * A single quote within the string can be encoded by putting two single * quotes in a row - as in Pascal. C-style escapes using the backslash * character are not supported because they are not standard SQL. * * See: https://www.sqlite.org/lang_expr.html#literal_values_constants_ * * Although sparsely documented, this applies to backtick and double quoted * string constants as well, so only the quote character needs to be escaped. * * For more details, see the grammar for SQLite table and column names: * * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/tokenize.c#L395-L419 * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/parse.y#L321-L338 * * --- * * We use backtick quotes instead of the SQL standard double quotes, due to * an SQLite quirk causing double-quoted strings to be accepted as literals: * * This misfeature means that a misspelled double-quoted identifier will * be interpreted as a string literal, rather than generating an error. * * See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted * * @param string $unquoted_identifier The unquoted identifier value. * @return string The quoted identifier value. */ public function quote_identifier( string $unquoted_identifier ): string { return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; } /** * Get the PDO object. * * @return PDO */ public function get_pdo(): PDO { return $this->pdo; } /** * Set or clear a logger for SQLite queries. * * @param (callable(string, array): void)|null $logger A query logger callback, or null to clear it. */ public function set_query_logger( ?callable $logger ): void { $this->query_logger = $logger; } }