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 @@ -883,12 +883,34 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar
null === $child_node
|| 'beginWork' === $child_node->rule_name
|| $child_node->has_child_node( 'transactionOrLockingStatement' )
|| $child_node->has_child_node( 'selectStatement' )
) {
$wrap_in_transaction = false;
} else {
$wrap_in_transaction = true;
}

/*
* Detect read-only statements before opening the wrapper transaction.
*
* [GRAMMAR]
* simpleStatement: selectStatement | showStatement | utilityStatement | ...
*/
$statement_node = $child_node->get_first_child_node();
if ( null !== $statement_node ) {
if (
'selectStatement' === $statement_node->rule_name
|| 'showStatement' === $statement_node->rule_name
) {
$this->is_readonly = true;
} elseif ( 'utilityStatement' === $statement_node->rule_name ) {
$utility_subnode = $statement_node->get_first_child_node();
if ( null !== $utility_subnode && 'describeStatement' === $utility_subnode->rule_name ) {
$this->is_readonly = true;
}
}
}

if ( $wrap_in_transaction ) {
$this->begin_wrapper_transaction();
}
Expand Down Expand Up @@ -1366,7 +1388,6 @@ private function execute_mysql_query( WP_Parser_Node $node ): void {
$this->execute_transaction_or_locking_statement( $node );
break;
case 'selectStatement':
$this->is_readonly = true;
$this->execute_select_statement( $node );
break;
case 'insertStatement':
Expand Down Expand Up @@ -1444,14 +1465,12 @@ private function execute_mysql_query( WP_Parser_Node $node ): void {
$this->execute_set_statement( $node );
break;
case 'showStatement':
$this->is_readonly = true;
$this->execute_show_statement( $node );
break;
case 'utilityStatement':
$subtree = $node->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'describeStatement':
$this->is_readonly = true;
$this->execute_describe_statement( $subtree );
break;
case 'useCommand':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

use PHPUnit\Framework\TestCase;

/**
* Tests for concurrent access to the same SQLite database file.
*/
class WP_SQLite_Driver_Concurrency_Tests extends TestCase {
/**
* Path to the temporary SQLite database file used in file-based tests.
*
* @var string|null
*/
private $db_path;

public function setUp(): void {
$this->db_path = tempnam( sys_get_temp_dir(), 'wp_sqlite_' );
unlink( $this->db_path ); // Remove so SQLite creates a fresh database.
}

public function tearDown(): void {
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 );
}
}
$this->db_path = null;
}

/**
* A SELECT should not be wrapped in a transaction — no BEGIN at all.
*/
public function testSelectQueryIsNotWrappedInTransaction(): void {
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class;
$pdo = new $pdo_class( 'sqlite::memory:' );

$connection = new WP_SQLite_Connection( array( 'pdo' => $pdo ) );
$driver = new WP_SQLite_Driver( $connection, 'wp' );
$driver->query( 'CREATE TABLE t (id INT, name VARCHAR(255))' );

// Capture SQLite queries. The logger must be set on the driver's
// internal connection, not the original one passed to the constructor.
$logged_queries = array();
$driver->get_connection()->set_query_logger(
function ( string $sql, array $params ) use ( &$logged_queries ): void {
$logged_queries[] = $sql;
}
);

$driver->query( 'SELECT * FROM t' );

$this->assertStringStartsNotWith( 'BEGIN', $logged_queries[0] );
}

/**
* A SELECT on one connection should succeed even when another connection
* holds an open write transaction (RESERVED lock).
*/
public function testSelectQuerySucceedsWhileAnotherConnectionHoldsWriteLock(): void {
// Connection A: set up the database.
$conn_a = new WP_SQLite_Connection( array( 'path' => $this->db_path ) );
$driver_a = new WP_SQLite_Driver( $conn_a, 'wp' );
$driver_a->query( 'CREATE TABLE t (id INT, name VARCHAR(255))' );
$driver_a->query( "INSERT INTO t VALUES (1, 'Alice')" );

// Simulate another PHP process holding a write transaction.
$conn_a->get_pdo()->exec( 'BEGIN IMMEDIATE' );

try {
// Connection B with zero timeout — any lock conflict fails immediately.
$conn_b = new WP_SQLite_Connection(
array(
'path' => $this->db_path,
'timeout' => 0,
)
);
$driver_b = new WP_SQLite_Driver( $conn_b, 'wp' );
$conn_b->get_pdo()->setAttribute( PDO::ATTR_TIMEOUT, 0 );

$result = $driver_b->query( 'SELECT * FROM t' );

$this->assertCount( 1, $result );
$this->assertSame( '1', $result[0]->id );
$this->assertSame( 'Alice', $result[0]->name );
} finally {
$conn_a->get_pdo()->exec( 'ROLLBACK' );
}
}
}
Loading