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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -669,11 +669,16 @@ public function __construct(
$db_name = $args['dbname'] ?? 'sqlite_database';

// Create a new SQLite connection.
$connection_options = array(
'journal_mode' => $options['journal_mode'] ?? null,
'synchronous' => $options['synchronous'] ?? null,
);
if ( isset( $options['pdo'] ) ) {
$this->connection = new WP_SQLite_Connection( array( 'pdo' => $options['pdo'] ) );
$connection_options['pdo'] = $options['pdo'];
} else {
$this->connection = new WP_SQLite_Connection( array( 'path' => $path ) );
$connection_options['path'] = $path;
}
$this->connection = new WP_SQLite_Connection( $connection_options );

$this->mysql_version = $options['mysql_version'] ?? 80038;
$this->main_db_name = $db_name;
Expand Down
98 changes: 86 additions & 12 deletions packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ class WP_SQLite_Connection {
'OFF',
);

/**
* The supported SQLite synchronous settings.
*
* The list is indexed by the corresponding numeric setting values (0 to 3).
*
* See: https://www.sqlite.org/pragma.html#pragma_synchronous
*/
const SQLITE_SYNCHRONOUS_SETTINGS = array(
'OFF',
'NORMAL',
'FULL',
'EXTRA',
);

/**
* The PDO connection for SQLite.
*
Expand All @@ -55,14 +69,16 @@ class WP_SQLite_Connection {
* @param array $options {
* An array of options.
*
* @type string|null $path Optional. SQLite database path.
* For in-memory database, use ':memory:'.
* Must be set when PDO instance is not provided.
* @type PDO|null $pdo Optional. PDO instance with SQLite connection.
* If not provided, a new PDO instance will be created.
* @type int|null $timeout Optional. SQLite timeout in seconds.
* The time to wait for a writable lock.
* @type string|null $journal_mode Optional. SQLite journal mode.
* @type string|null $path Optional. SQLite database path.
* For in-memory database, use ':memory:'.
* Must be set when PDO instance is not provided.
* @type PDO|null $pdo Optional. PDO instance with SQLite connection.
* If not provided, a new PDO instance will be created.
* @type int|null $timeout Optional. SQLite timeout in seconds.
* The time to wait for a writable lock.
* @type string|null $journal_mode Optional. SQLite journal mode. Defaults to WAL.
* @type string|int|null $synchronous Optional. SQLite synchronous setting. Defaults to
* NORMAL when the effective journal mode is WAL.
* }
*
* @throws InvalidArgumentException When some connection options are invalid.
Expand Down Expand Up @@ -91,10 +107,68 @@ public function __construct( array $options ) {
}
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );

// Configure SQLite journal mode.
$journal_mode = $options['journal_mode'] ?? null;
if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
$this->query( 'PRAGMA journal_mode = ' . $journal_mode );
// 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 lose 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 );
}
Comment on lines +170 to 172

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if ( in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) {
$this->query( 'PRAGMA synchronous = ' . $synchronous );
}
if ( in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) {
$this->query( 'PRAGMA synchronous = ' . $synchronous );
} else if ( isset( $synchronous ) ) {
throw new PDOException( sprintf( '%s is not a valid `synchronous` PRAGMA value. ' ) );
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's not accept invalid option values.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ditto for the journal mode, let's not skip that silently.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@adamziel Fixed in 8de3d1a. I used InvalidArgumentException as in the case of missing path, but not sure which is better—vs PDOException.

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,54 @@ public function test_dsn_parsing(): void {
$this->assertSame( 'w', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
}

public function test_journal_mode_defaults_to_wal(): void {
$path = tempnam( sys_get_temp_dir(), 'wp_sqlite_' );
unlink( $path );

try {
$driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=' . $path . ';dbname=wp' );
$connection = $driver->get_connection();
$this->assertSame(
'wal',
strtolower( (string) $connection->query( 'PRAGMA journal_mode' )->fetchColumn() )
);
$this->assertSame(
'1',
(string) $connection->query( 'PRAGMA synchronous' )->fetchColumn()
);
} finally {
$this->remove_database_files( $path );
}
}

public function test_journal_mode_and_synchronous_driver_options(): void {
$path = tempnam( sys_get_temp_dir(), 'wp_sqlite_' );
unlink( $path );

try {
$driver = new WP_PDO_MySQL_On_SQLite(
'mysql-on-sqlite:path=' . $path . ';dbname=wp',
null,
null,
array(
'journal_mode' => 'DELETE',
'synchronous' => 'FULL',
)
);
$connection = $driver->get_connection();
$this->assertSame(
'delete',
strtolower( (string) $connection->query( 'PRAGMA journal_mode' )->fetchColumn() )
);
$this->assertSame(
'2',
(string) $connection->query( 'PRAGMA synchronous' )->fetchColumn()
);
} finally {
$this->remove_database_files( $path );
}
}

public function test_query(): void {
$result = $this->driver->query( "SELECT 1, 'abc'" );
$this->assertInstanceOf( PDOStatement::class, $result );
Expand Down Expand Up @@ -548,4 +596,12 @@ public function data_pdo_fetch_methods(): Generator {
),
);
}

private function remove_database_files( string $path ): void {
foreach ( array( $path, $path . '-wal', $path . '-shm', $path . '-journal' ) as $file ) {
if ( file_exists( $file ) ) {
unlink( $file );
}
}
}
}
Loading
Loading