Skip to content
Draft
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
30 changes: 26 additions & 4 deletions src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ class Connection implements ConnectionInterface
*/
protected static $resolvers = [];

/**
* The last retrieved PDO read / write type.
*
* @var null|'read'|'write'
*/
protected $latestPdoTypeRetrieved = null;

/**
* Create a new database connection instance.
*
Expand Down Expand Up @@ -819,12 +826,12 @@ protected function runQueryCallback($query, $bindings, Closure $callback)
catch (Exception $e) {
if ($this->isUniqueConstraintError($e)) {
throw new UniqueConstraintViolationException(
$this->getName(), $query, $this->prepareBindings($bindings), $e
$this->getName(), $query, $this->prepareBindings($bindings), $e, $this->latestReadWriteTypeUsed()
);
}

throw new QueryException(
$this->getName(), $query, $this->prepareBindings($bindings), $e
$this->getName(), $query, $this->prepareBindings($bindings), $e, $this->latestReadWriteTypeUsed()
);
}
}
Expand All @@ -851,15 +858,16 @@ protected function isUniqueConstraintError(Exception $exception)
public function logQuery($query, $bindings, $time = null)
{
$this->totalQueryDuration += $time ?? 0.0;
$readWriteType = $this->latestReadWriteTypeUsed();

$this->event(new QueryExecuted($query, $bindings, $time, $this));
$this->event(new QueryExecuted($query, $bindings, $time, $this, $readWriteType));

$query = $this->pretending === true
? $this->queryGrammar?->substituteBindingsIntoRawSql($query, $bindings) ?? $query
: $query;

if ($this->loggingQueries) {
$this->queryLog[] = compact('query', 'bindings', 'time');
$this->queryLog[] = compact('query', 'bindings', 'time', 'readWriteType');
}
}

Expand Down Expand Up @@ -1232,6 +1240,8 @@ public function useWriteConnectionWhenReading($value = true)
*/
public function getPdo()
{
$this->latestPdoTypeRetrieved = 'write';

if ($this->pdo instanceof Closure) {
return $this->pdo = call_user_func($this->pdo);
}
Expand Down Expand Up @@ -1265,6 +1275,8 @@ public function getReadPdo()
return $this->getPdo();
}

$this->latestPdoTypeRetrieved = 'read';

if ($this->readPdo instanceof Closure) {
return $this->readPdo = call_user_func($this->readPdo);
}
Expand Down Expand Up @@ -1621,6 +1633,16 @@ public function setReadWriteType($readWriteType)
return $this;
}

/**
* Retrieve the latest read / write type used.
*
* @return 'read'|'write'|null
*/
protected function latestReadWriteTypeUsed()
{
return $this->readWriteType ?? $this->latestPdoTypeRetrieved;
}

/**
* Get the table prefix for the connection.
*
Expand Down
11 changes: 10 additions & 1 deletion src/Illuminate/Database/Events/QueryExecuted.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,30 @@ class QueryExecuted
*/
public $connectionName;

/**
* The PDO read / write type for the executed query.
*
* @var null|'read'|'write'
*/
public $readWriteType;

/**
* Create a new event instance.
*
* @param string $sql
* @param array $bindings
* @param float|null $time
* @param \Illuminate\Database\Connection $connection
* @param null|'read'|'write' $readWriteType
*/
public function __construct($sql, $bindings, $time, $connection)
public function __construct($sql, $bindings, $time, $connection, $readWriteType = null)
Comment on lines -50 to +58
Copy link
Member Author

@timacdonald timacdonald Dec 18, 2025

Choose a reason for hiding this comment

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

Constructor remains backwards compatible.

{
$this->sql = $sql;
$this->time = $time;
$this->bindings = $bindings;
$this->connection = $connection;
$this->connectionName = $connection->getName();
$this->readWriteType = $readWriteType;
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/Illuminate/Database/QueryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,30 @@ class QueryException extends PDOException
*/
protected $bindings;

/**
* The PDO read / write type for the executed query.
*
* @var null|'read'|'write'
*/
public $readWriteType;

/**
* Create a new query exception instance.
*
* @param string $connectionName
* @param string $sql
* @param array $bindings
* @param \Throwable $previous
* @param null|'read'|'write' $readWriteType
*/
public function __construct($connectionName, $sql, array $bindings, Throwable $previous)
public function __construct($connectionName, $sql, array $bindings, Throwable $previous, $readWriteType = null)
{
parent::__construct('', 0, $previous);

$this->connectionName = $connectionName;
$this->sql = $sql;
$this->bindings = $bindings;
$this->readWriteType = $readWriteType;
$this->code = $previous->getCode();
$this->message = $this->formatMessage($connectionName, $sql, $bindings, $previous);

Expand Down
194 changes: 194 additions & 0 deletions tests/Integration/Database/DatabaseConnectionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Events\ConnectionEstablished;
use Illuminate\Database\QueryException;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;

class DatabaseConnectionsTest extends DatabaseTestCase
Expand Down Expand Up @@ -172,4 +176,194 @@ public function testDynamicConnectionWithNoNameDoesntFailOnReconnect()
}
}
}

#[DataProvider('readWriteExpectations')]
public function testReadWriteTypeIsProvidedInQueryExecutedEventAndQueryLog(string $connectionName, array $expectedTypes, ?string $loggedType)
{
$readPath = __DIR__.'/read.sqlite';
$writePath = __DIR__.'/write.sqlite';
Config::set('database.connections.sqlite', [
'driver' => 'sqlite',
'read' => [
'database' => $readPath,
],
'write' => [
'database' => $writePath,
],
]);
$events = collect();
DB::listen($events->push(...));

try {
touch($readPath);
touch($writePath);

$connection = DB::connection($connectionName);
$connection->enableQueryLog();

$connection->statement('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);

$connection->statement('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);

$this->assertEmpty($events);
$this->assertSame([
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'write'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'write'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'read'],
], Arr::select($connection->getQueryLog(), [
'query', 'readWriteType',
]));
} finally {
@unlink($readPath);
@unlink($writePath);
}
}

public static function readWriteExpectations(): iterable
{
yield 'sqlite' => ['sqlite', ['write', 'read', 'write', 'read'], null];
yield 'sqlite::read' => ['sqlite::read', ['read', 'read', 'read', 'read'], 'read'];
yield 'sqlite::write' => ['sqlite::write', ['write', 'write', 'write', 'write'], 'write'];
}

public function testConnectionsWithoutReadWriteConfigurationAlwaysShowAsWrite()
{
$writePath = __DIR__.'/write.sqlite';
Config::set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => $writePath,
]);
$events = collect();
DB::listen($events->push(...));

try {
touch($writePath);

$connection = DB::connection('sqlite');

$connection->statement('select 1');
$this->assertSame('write', $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame('write', $events->shift()->readWriteType);

$connection->statement('select 1');
$this->assertSame('write', $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame('write', $events->shift()->readWriteType);
} finally {
@unlink($writePath);
}
}

public function testQueryExceptionsProviderReadWriteType()
{
$readPath = __DIR__.'/read.sqlite';
$writePath = __DIR__.'/write.sqlite';
Config::set('database.connections.sqlite', [
'driver' => 'sqlite',
'read' => [
'database' => $readPath,
],
'write' => [
'database' => $writePath,
],
]);

try {
touch($readPath);
touch($writePath);

try {
DB::connection('sqlite::write')->statement('xxxx');
$this->fail();
} catch (QueryException $exception) {
$this->assertSame('write', $exception->readWriteType);
}

try {
DB::connection('sqlite::read')->statement('xxxx');
$this->fail();
} catch (QueryException $exception) {
$this->assertSame('read', $exception->readWriteType);
}
} finally {
@unlink($writePath);
@unlink($readPath);
}
}

#[DataProvider('readWriteExpectations')]
public function testQueryInEventListenerCannotInterfereWithReadWriteType(string $connectionName, array $expectedTypes, ?string $loggedType)
{
$readPath = __DIR__.'/read.sqlite';
$writePath = __DIR__.'/write.sqlite';
Config::set('database.connections.sqlite', [
'driver' => 'sqlite',
'read' => [
'database' => $readPath,
],
'write' => [
'database' => $writePath,
],
]);
$events = collect();
DB::listen($events->push(...));

try {
touch($readPath);
touch($writePath);

$connection = DB::connection($connectionName);
$connection->enableQueryLog();

$connection->listen(function ($query) use ($connection) {
if ($query->sql === 'select 1') {
$connection->select('select 2');
}
});

$connection->statement('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);
$this->assertSame($loggedType ?? 'read', $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);
$this->assertSame($loggedType ?? 'read', $events->shift()->readWriteType);

$connection->statement('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);
$this->assertSame($loggedType ?? 'read', $events->shift()->readWriteType);

$connection->select('select 1');
$this->assertSame(array_shift($expectedTypes), $events->shift()->readWriteType);
$this->assertSame($loggedType ?? 'read', $events->shift()->readWriteType);

$this->assertSame([
['query' => 'select 2', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'write'],
['query' => 'select 2', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 2', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'write'],
['query' => 'select 2', 'readWriteType' => $loggedType ?? 'read'],
['query' => 'select 1', 'readWriteType' => $loggedType ?? 'read'],
], Arr::select($connection->getQueryLog(), [
'query', 'readWriteType',
]));
} finally {
@unlink($readPath);
@unlink($writePath);
}
}
}
Loading