PluginProbe
SQLite Database Integration / 3.0.2
SQLite Database Integration v3.0.2
3.0.2 3.0.1 trunk 2.1.13 2.1.14 2.1.15 2.1.16 2.2.0 2.2.1 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.2.2 2.2.20 2.2.21 2.2.22 2.2.23 2.2.3 All 32 releases
sqlite-database-integration / wp-includes / database / sqlite / class-wp-sqlite-connection.php

class-wp-sqlite-connection.php in SQLite Database Integration 3.0.2, at wp-includes/database/sqlite/class-wp-sqlite-connection.php

293 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php declare(strict_types = 1);
2
3 /*
4 * The SQLite connection uses PDO. Enable PDO function calls:
5 * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
6 */
7
8 /**
9 * SQLite connection.
10 *
11 * This class configures and encapsulates the connection to an SQLite database.
12 * It requires PDO with the SQLite driver, and currently, it is only a simple
13 * wrapper that leaks some of the PDO APIs (returns PDOStatement values, etc.).
14 * In the future, we may abstract it away from PDO and support SQLite3 as well.
15 *
16 * @access private
17 */
18 class WP_SQLite_Connection {
19 /**
20 * The default timeout in seconds for SQLite to wait for a writable lock.
21 */
22 const DEFAULT_SQLITE_TIMEOUT = 10;
23
24 /**
25 * The supported SQLite journal modes.
26 *
27 * See: https://www.sqlite.org/pragma.html#pragma_journal_mode
28 */
29 const SQLITE_JOURNAL_MODES = array(
30 'DELETE',
31 'TRUNCATE',
32 'PERSIST',
33 'MEMORY',
34 'WAL',
35 'OFF',
36 );
37
38 /**
39 * The supported SQLite synchronous settings.
40 *
41 * The list is indexed by the corresponding numeric setting values (0 to 3).
42 *
43 * See: https://www.sqlite.org/pragma.html#pragma_synchronous
44 */
45 const SQLITE_SYNCHRONOUS_SETTINGS = array(
46 'OFF',
47 'NORMAL',
48 'FULL',
49 'EXTRA',
50 );
51
52 /**
53 * The PDO connection for SQLite.
54 *
55 * @var PDO
56 */
57 private $pdo;
58
59 /**
60 * A query logger callback.
61 *
62 * @var (callable(string, array): void)|null
63 */
64 private $query_logger = null;
65
66 /**
67 * Constructor.
68 *
69 * Set up an SQLite connection.
70 *
71 * @param array $options {
72 * An array of options.
73 *
74 * @type string|null $path Optional. SQLite database path.
75 * For in-memory database, use ':memory:'.
76 * Must be set when PDO instance is not provided.
77 * @type PDO|null $pdo Optional. PDO instance with SQLite connection.
78 * If not provided, a new PDO instance will be created.
79 * @type int|null $timeout Optional. SQLite timeout in seconds.
80 * The time to wait for a writable lock.
81 * @type string|null $journal_mode Optional. SQLite journal mode. Defaults to WAL.
82 * @type string|int|null $synchronous Optional. SQLite synchronous setting. Defaults to
83 * NORMAL when the effective journal mode is WAL.
84 * @type array $pdo_options Optional. PDO constructor options.
85 * }
86 *
87 * @throws InvalidArgumentException When some connection options are invalid.
88 * @throws PDOException When the driver initialization fails.
89 */
90 public function __construct( array $options ) {
91 // Setup PDO connection.
92 if ( isset( $options['pdo'] ) && $options['pdo'] instanceof PDO ) {
93 $this->pdo = $options['pdo'];
94 } else {
95 if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
96 throw new InvalidArgumentException( 'Option "path" is required when "pdo" is not provided.' );
97 }
98 $pdo_class = PHP_VERSION_ID >= 80400 ? Pdo\Sqlite::class : PDO::class;
99 $pdo_options = $options['pdo_options'] ?? array();
100
101 // Internal driver operations require exceptions regardless of the
102 // caller-visible WP_MySQL_On_SQLite::ATTR_ERRMODE setting.
103 $pdo_options[ PDO::ATTR_ERRMODE ] = PDO::ERRMODE_EXCEPTION;
104 $this->pdo = new $pdo_class( 'sqlite:' . $options['path'], null, null, $pdo_options );
105 }
106
107 // Throw exceptions on error.
108 $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
109
110 // Configure SQLite timeout.
111 if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) {
112 $timeout = $options['timeout'];
113 } else {
114 $timeout = self::DEFAULT_SQLITE_TIMEOUT;
115 }
116 $this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
117
118 // Configure SQLite journal mode. Default to WAL for best throughput.
119 $effective_journal_mode = null;
120 $journal_mode = $options['journal_mode'] ?? 'WAL';
121 if ( is_string( $journal_mode ) ) {
122 $journal_mode = strtoupper( $journal_mode );
123 }
124 if ( ! in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
125 throw new InvalidArgumentException(
126 sprintf( 'Invalid SQLite journal mode: %s.', $options['journal_mode'] )
127 );
128 }
129 try {
130 $effective_journal_mode = strtoupper(
131 (string) $this->query( 'PRAGMA journal_mode = ' . $journal_mode )->fetchColumn()
132 );
133 } catch ( PDOException $e ) {
134 // WAL may be unavailable in some environments, such as on network
135 // filesystems. When it is explicitly configured, surface the error.
136 // Otherwise, fall back to the default SQLite behavior.
137 if ( isset( $options['journal_mode'] ) ) {
138 throw $e;
139 }
140 }
141
142 /*
143 * Configure SQLite synchronous setting. Default to NORMAL for WAL mode.
144 *
145 * WAL improves read/write concurrency and "synchronous = NORMAL" avoids
146 * frequent sync to the main database, which could become a bottleneck.
147 * In WAL mode, NORMAL is safe and recommended. From the SQLite docs:
148 *
149 * The synchronous=NORMAL setting provides the best balance between
150 * performance and safety for most applications running in WAL mode.
151 * You lose durability across power loss with synchronous NORMAL in WAL
152 * mode, but that is not important for most applications. Transactions
153 * are still atomic, consistent, and isolated, which are the most
154 * important characteristics in most use cases.
155 *
156 * SQLite defaults to "synchronous = FULL" to avoid data corruption with
157 * other journal modes. With WAL, this is not necessary.
158 *
159 * See: https://sqlite.org/pragma.html#pragma_synchronous
160 */
161 $synchronous = $options['synchronous'] ?? null;
162 if ( isset( $synchronous ) ) {
163 // Validate and normalize explicitly provided synchronous value.
164 if ( is_int( $synchronous ) && isset( self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ] ) ) {
165 $synchronous = self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ];
166 } elseif ( is_string( $synchronous ) ) {
167 $synchronous = strtoupper( $synchronous );
168 }
169 if ( ! in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) {
170 throw new InvalidArgumentException(
171 sprintf( 'Invalid SQLite synchronous setting: %s.', $options['synchronous'] )
172 );
173 }
174 } elseif ( 'WAL' === $effective_journal_mode ) {
175 // Default to NORMAL for WAL mode.
176 $synchronous = 'NORMAL';
177 }
178 if ( in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) {
179 $this->query( 'PRAGMA synchronous = ' . $synchronous );
180 }
181 }
182
183 /**
184 * Execute a query in SQLite.
185 *
186 * @param string $sql The query to execute.
187 * @param array $params The query parameters.
188 * @throws PDOException When the query execution fails.
189 * @return PDOStatement The PDO statement object.
190 */
191 public function query( string $sql, array $params = array() ): PDOStatement {
192 if ( $this->query_logger ) {
193 ( $this->query_logger )( $sql, $params );
194 }
195 $stmt = $this->pdo->prepare( $sql );
196 $stmt->execute( $params );
197 return $stmt;
198 }
199
200 /**
201 * Prepare a SQLite query for execution.
202 *
203 * @param string $sql The query to prepare.
204 * @return PDOStatement The prepared statement.
205 * @throws PDOException When the query preparation fails.
206 */
207 public function prepare( string $sql ): PDOStatement {
208 if ( $this->query_logger ) {
209 ( $this->query_logger )( $sql, array() );
210 }
211 return $this->pdo->prepare( $sql );
212 }
213
214 /**
215 * Returns the ID of the last inserted row.
216 *
217 * @return string The ID of the last inserted row.
218 */
219 public function get_last_insert_id(): string {
220 return $this->pdo->lastInsertId();
221 }
222
223 /**
224 * Quote a value for use in a query.
225 *
226 * @param mixed $value The value to quote.
227 * @param int $type The type of the value.
228 * @return string The quoted value.
229 */
230 public function quote( $value, int $type = PDO::PARAM_STR ): string {
231 return $this->pdo->quote( $value, $type );
232 }
233
234 /**
235 * Quote an SQLite identifier.
236 *
237 * Wraps the identifier in backticks and escapes backtick characters within.
238 *
239 * ---
240 *
241 * Quoted identifiers in SQLite are represented by string constants:
242 *
243 * A string constant is formed by enclosing the string in single quotes (').
244 * A single quote within the string can be encoded by putting two single
245 * quotes in a row - as in Pascal. C-style escapes using the backslash
246 * character are not supported because they are not standard SQL.
247 *
248 * See: https://www.sqlite.org/lang_expr.html#literal_values_constants_
249 *
250 * Although sparsely documented, this applies to backtick and double quoted
251 * string constants as well, so only the quote character needs to be escaped.
252 *
253 * For more details, see the grammar for SQLite table and column names:
254 *
255 * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/tokenize.c#L395-L419
256 * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/parse.y#L321-L338
257 *
258 * ---
259 *
260 * We use backtick quotes instead of the SQL standard double quotes, due to
261 * an SQLite quirk causing double-quoted strings to be accepted as literals:
262 *
263 * This misfeature means that a misspelled double-quoted identifier will
264 * be interpreted as a string literal, rather than generating an error.
265 *
266 * See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
267 *
268 * @param string $unquoted_identifier The unquoted identifier value.
269 * @return string The quoted identifier value.
270 */
271 public function quote_identifier( string $unquoted_identifier ): string {
272 return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
273 }
274
275 /**
276 * Get the PDO object.
277 *
278 * @return PDO
279 */
280 public function get_pdo(): PDO {
281 return $this->pdo;
282 }
283
284 /**
285 * Set or clear a logger for SQLite queries.
286 *
287 * @param (callable(string, array): void)|null $logger A query logger callback, or null to clear it.
288 */
289 public function set_query_logger( ?callable $logger ): void {
290 $this->query_logger = $logger;
291 }
292 }
293