Skip to content
Open
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
75 changes: 65 additions & 10 deletions packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ class WP_SQLite_Connection {
*/
const DEFAULT_SQLITE_TIMEOUT = 10;

/**
* The default SQLite journal mode.
*/
const DEFAULT_SQLITE_JOURNAL_MODE = 'WAL';

/**
* The default SQLite synchronous setting for WAL mode.
*/
const DEFAULT_SQLITE_WAL_SYNCHRONOUS = 'NORMAL';

/**
* The supported SQLite journal modes.
*
Expand All @@ -33,6 +43,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 +79,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 @@ -92,9 +118,38 @@ public function __construct( array $options ) {
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );

// Configure SQLite journal mode.
$journal_mode = $options['journal_mode'] ?? null;
$effective_journal_mode = null;
$journal_mode = $options['journal_mode'] ?? self::DEFAULT_SQLITE_JOURNAL_MODE;
if ( is_string( $journal_mode ) ) {
$journal_mode = strtoupper( $journal_mode );
}
if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
$this->query( 'PRAGMA journal_mode = ' . $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. In WAL mode, default to NORMAL.
// Otherwise, use SQLite's default value.
$synchronous = $options['synchronous'] ?? null;
if ( null === $synchronous && 'WAL' === $effective_journal_mode ) {
$synchronous = self::DEFAULT_SQLITE_WAL_SYNCHRONOUS;

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.

This is super confusing. It's SYNCHRONOUS but it's NORMAL. Let's just stick to SQLite terminology and maybe even let go of constants:

Suggested change
$synchronous = self::DEFAULT_SQLITE_WAL_SYNCHRONOUS;
$synchronous = 'NORMAL';

Or even go super simple:

Suggested change
$synchronous = self::DEFAULT_SQLITE_WAL_SYNCHRONOUS;
$this->query( 'PRAGMA synchronous = NORMAL' );

The comment above explains what we're doing, I assume it's because the logic is so opaque here. I'd love to, instead, understand why we're doing this. Document the NORMAL mode inline and explain why we're defaulting to it when WAL is enabled and not otherwise and what is SQLite default value?

} elseif ( is_int( $synchronous ) && isset( self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ] ) ) {
$synchronous = self::SQLITE_SYNCHRONOUS_SETTINGS[ $synchronous ];
} elseif ( is_string( $synchronous ) ) {
$synchronous = strtoupper( $synchronous );
}
if ( $synchronous && in_array( $synchronous, self::SQLITE_SYNCHRONOUS_SETTINGS, true ) ) {
$this->query( 'PRAGMA synchronous = ' . $synchronous );
}
}

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 );
}
}
}
}
191 changes: 191 additions & 0 deletions packages/mysql-on-sqlite/tests/WP_SQLite_Connection_Tests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

use PHPUnit\Framework\TestCase;

/**
* Tests for the SQLite connection setup.
*/
class WP_SQLite_Connection_Tests extends TestCase {
/**
* Path to the temporary directory holding the SQLite database file.
*
* @var string|null
*/
private $db_dir;

/**
* Path to the temporary SQLite database file used in file-based tests.
*
* @var string|null
*/
private $db_path;

public function setUp(): void {
$this->db_dir = tempnam( sys_get_temp_dir(), 'wp_sqlite_' );
unlink( $this->db_dir );
mkdir( $this->db_dir );
$this->db_path = $this->db_dir . '/database.sqlite';
}

public function tearDown(): void {
chmod( $this->db_dir, 0755 ); // Restore permissions changed by read-only tests.
foreach ( array(
$this->db_path,
$this->db_path . '-wal',
$this->db_path . '-shm',
$this->db_path . '-journal',
) as $path ) {
if ( is_string( $path ) && file_exists( $path ) ) {
unlink( $path );
}
}
rmdir( $this->db_dir );
$this->db_dir = null;
$this->db_path = null;
}

public function testDefaultJournalModeUsesWal(): void {
$connection = new WP_SQLite_Connection( array( 'path' => $this->db_path ) );

$this->assertSame( 'wal', $this->get_journal_mode( $connection ) );
$this->assertSame( '1', $this->get_synchronous( $connection ) );
}

public function testJournalModeCanBeOverridden(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'DELETE',
)
);

$this->assertSame( 'delete', $this->get_journal_mode( $connection ) );
}

public function testSynchronousCanBeOverridden(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'synchronous' => 'FULL',
)
);

$this->assertSame( '2', $this->get_synchronous( $connection ) );
}

public function testRollbackJournalModeKeepsDefaultSynchronous(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'DELETE',
)
);

$this->assertSame( '2', $this->get_synchronous( $connection ) );
}

public function testInMemoryDatabaseKeepsDefaultSynchronous(): void {
$connection = new WP_SQLite_Connection( array( 'path' => ':memory:' ) );

$this->assertSame( 'memory', $this->get_journal_mode( $connection ) );
$this->assertSame( '2', $this->get_synchronous( $connection ) );
}

public function testJournalModeAndSynchronousAreCaseInsensitive(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'delete',
'synchronous' => 'extra',
)
);

$this->assertSame( 'delete', $this->get_journal_mode( $connection ) );
$this->assertSame( '3', $this->get_synchronous( $connection ) );
}

public function testInvalidJournalModeAndSynchronousAreIgnored(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'INVALID',
'synchronous' => 'INVALID',
)
);

$this->assertSame( 'delete', $this->get_journal_mode( $connection ) );
$this->assertSame( '2', $this->get_synchronous( $connection ) );
}

public function testSynchronousAcceptsIntegerValues(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'synchronous' => 3,
)
);

$this->assertSame( '3', $this->get_synchronous( $connection ) );
}

public function testSynchronousAcceptsIntegerZero(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'DELETE',
'synchronous' => 0,
)
);

$this->assertSame( '0', $this->get_synchronous( $connection ) );
}

public function testDefaultJournalModeFallsBackWhenWalIsUnavailable(): void {
$this->make_database_directory_read_only();

$connection = new WP_SQLite_Connection( array( 'path' => $this->db_path ) );

$this->assertSame( 'delete', $this->get_journal_mode( $connection ) );
$this->assertSame( '2', $this->get_synchronous( $connection ) );
}

public function testExplicitJournalModeSurfacesFailureWhenWalIsUnavailable(): void {
$this->make_database_directory_read_only();

$this->expectException( PDOException::class );
new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'WAL',
)
);
}

/**
* Create the database file first, and then make its directory read-only,
* so that the WAL sidecar files ("-wal", "-shm") cannot be created.
*/
private function make_database_directory_read_only(): void {
$connection = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'journal_mode' => 'DELETE',
)
);
$connection->query( 'CREATE TABLE t ( id INTEGER )' );
$connection = null;

chmod( $this->db_dir, 0555 );
if ( is_writable( $this->db_dir ) ) {
$this->markTestSkipped( 'The test requires a non-writable database directory.' );
}
}

private function get_journal_mode( WP_SQLite_Connection $connection ): string {
return strtolower( (string) $connection->query( 'PRAGMA journal_mode' )->fetchColumn() );
}

private function get_synchronous( WP_SQLite_Connection $connection ): string {
return (string) $connection->query( 'PRAGMA synchronous' )->fetchColumn();
}
}
Loading
Loading