From 64a6cc4383c010a67f0257a1fbfdb38e583e21f8 Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Tue, 7 Oct 2025 13:18:29 +0900 Subject: [PATCH 1/7] Capture whether query was executed using read connection or not --- src/Records/Query.php | 1 + src/Sensors/QuerySensor.php | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Records/Query.php b/src/Records/Query.php index 3a79e2203..b7845e24f 100644 --- a/src/Records/Query.php +++ b/src/Records/Query.php @@ -10,6 +10,7 @@ public function __construct( public readonly int $line, public readonly int $duration, public readonly string $connection, + public readonly bool $usingReadConnection, ) { // } diff --git a/src/Sensors/QuerySensor.php b/src/Sensors/QuerySensor.php index 2aaf78dd9..8bff94dcf 100644 --- a/src/Sensors/QuerySensor.php +++ b/src/Sensors/QuerySensor.php @@ -2,6 +2,7 @@ namespace Laravel\Nightwatch\Sensors; +use Illuminate\Database\Connection; use Illuminate\Database\Events\QueryExecuted; use Laravel\Nightwatch\Clock; use Laravel\Nightwatch\Location; @@ -9,6 +10,7 @@ use Laravel\Nightwatch\State\CommandState; use Laravel\Nightwatch\State\RequestState; use Laravel\Nightwatch\Types\Str; +use PDO; use function hash; use function in_array; @@ -46,6 +48,7 @@ public function __invoke(QueryExecuted $event, array $trace): array line: $line ?? 0, duration: $durationInMicroseconds, connection: $event->connectionName ?? '', // @phpstan-ignore nullCoalesce.property + usingReadConnection: $this->usingReadConnection($event), ), function () use ($event, $record) { $this->executionState->queries++; @@ -68,6 +71,7 @@ function () use ($event, $record) { 'line' => $record->line, 'duration' => $record->duration, 'connection' => Str::tinyText($record->connection), + 'using_read_connection' => $record->usingReadConnection, ]; }, ]; @@ -87,4 +91,25 @@ private function hash(QueryExecuted $event, Query $record): string return hash('xxh128', "{$record->connection},{$sql}"); } + + /** + * Determine if the query was executed using the read connection. + */ + private function usingReadConnection(QueryExecuted $event): bool + { + $connection = $event->connection; + + $readPdo = $connection->getRawReadPdo(); + $writePdo = $connection->getRawPdo(); + + if (! $readPdo instanceof PDO) { + return false; + } + + if (! $writePdo instanceof PDO) { + return true; + } + + return $connection->getReadPdo() !== $writePdo; + } } From 63bcdfd69374048177607c99d97b088234171123 Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Tue, 7 Oct 2025 16:07:48 +0900 Subject: [PATCH 2/7] Add tests to capture using_read_connection property --- tests/Feature/Sensors/QuerySensorTest.php | 161 ++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/Feature/Sensors/QuerySensorTest.php b/tests/Feature/Sensors/QuerySensorTest.php index 545bb9861..9379070ac 100644 --- a/tests/Feature/Sensors/QuerySensorTest.php +++ b/tests/Feature/Sensors/QuerySensorTest.php @@ -21,9 +21,11 @@ use SingleStore\Laravel\Connect\SingleStoreConnection; use Tests\TestCase; +use function array_merge; use function base64_encode; use function class_exists; use function dirname; +use function fake; use function hash; use function hex2bin; use function in_array; @@ -93,6 +95,7 @@ public function test_it_can_ingest_queries(): void 'line' => $line, 'duration' => 4321, 'connection' => $connection, + 'using_read_connection' => false, ], ]); } @@ -352,4 +355,162 @@ public function test_it_can_capture_null_connection_name() $ingest->assertWrittenTimes(1); $ingest->assertLatestWrite('query:0.connection', ''); } + + public function test_it_captures_using_read_connection_as_false_when_read_and_write_connections_are_not_configured() + { + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + return DB::table('users')->get(); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); + } + + public function test_it_captures_using_read_connection_as_true_for_select_query() + { + $this->configureReadWriteConnection(); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + return DB::table('users')->get(); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', true); + } + + public function test_it_captures_using_read_connection_as_false_for_write_query() + { + $this->configureReadWriteConnection(); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + return DB::table('users')->insert([ + 'name' => fake()->name(), + 'email' => fake()->email(), + 'password' => fake()->password(), + ]); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); + } + + public function test_it_captures_using_read_connection_as_false_when_records_have_been_modified_and_sticky_connection_is_enabled() + { + $this->configureReadWriteConnection(['sticky' => true]); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + DB::table('users')->insert([ + 'name' => fake()->name(), + 'email' => fake()->email(), + 'password' => fake()->password(), + ]); + + return DB::table('users')->get(); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); // insert + $ingest->assertLatestWrite('query:1.using_read_connection', false); // select + } + + public function test_it_captures_using_read_connection_as_false_for_insert_and_true_for_select_when_sticky_connection_is_disabled() + { + $this->configureReadWriteConnection(['sticky' => false]); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + DB::table('users')->insert([ + 'name' => fake()->name(), + 'email' => fake()->email(), + 'password' => fake()->password(), + ]); + + return DB::table('users')->get(); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); // insert + $ingest->assertLatestWrite('query:1.using_read_connection', true); // select + } + + public function test_it_captures_using_read_connection_as_false_when_it_should_use_write_connection_when_reading() + { + $this->configureReadWriteConnection(); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + return DB::useWriteConnectionWhenReading()->table('users')->get(); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); + } + + public function test_it_captures_using_read_connection_as_false_when_in_a_transaction() + { + $this->configureReadWriteConnection(); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + DB::beginTransaction(); + + $users = DB::table('users')->get(); + + DB::rollBack(); + + return $users; + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.using_read_connection', false); + } + + private function configureReadWriteConnection(array $options = []): void + { + $connection = Config::get('database.default'); + $config = Config::get("database.connections.{$connection}"); + + Config::set("database.connections.{$connection}", array_merge($config, [ + 'read' => [ + 'database' => $config['database'], + ], + 'write' => [ + 'database' => $config['database'], + ], + ], $options)); + + DB::purge($connection); + } } From d931ad044493c7aead85572f2b4c7c698475fd7b Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Tue, 7 Oct 2025 16:20:39 +0900 Subject: [PATCH 3/7] Move ext-pdo to non-dev dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1abcf4b97..14eca39fc 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ ], "require": { "php": "^8.2", + "ext-pdo": "*", "ext-zlib": "*", "guzzlehttp/promises": "^2.0", "laravel/framework": "^10.0|^11.0|^12.0", @@ -35,7 +36,6 @@ }, "require-dev": { "ext-pcntl": "*", - "ext-pdo": "*", "aws/aws-sdk-php": "^3.349", "guzzlehttp/guzzle": "^7.0", "guzzlehttp/psr7": "^2.0", From dceddd71a2f6a638cd414962a1f683f39595ea58 Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Wed, 8 Oct 2025 14:33:47 +0900 Subject: [PATCH 4/7] Capture connection type instead of boolean --- src/Records/Query.php | 5 +++- src/Sensors/QuerySensor.php | 25 +++++++++++------ tests/Feature/Sensors/QuerySensorTest.php | 34 +++++++++++------------ 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/Records/Query.php b/src/Records/Query.php index b7845e24f..3deed40be 100644 --- a/src/Records/Query.php +++ b/src/Records/Query.php @@ -4,13 +4,16 @@ final class Query { + /** + * @param 'read'|'write'|'' $connectionType + */ public function __construct( public string $sql, public readonly string $file, public readonly int $line, public readonly int $duration, public readonly string $connection, - public readonly bool $usingReadConnection, + public readonly string $connectionType, ) { // } diff --git a/src/Sensors/QuerySensor.php b/src/Sensors/QuerySensor.php index 8bff94dcf..c5517a3d7 100644 --- a/src/Sensors/QuerySensor.php +++ b/src/Sensors/QuerySensor.php @@ -48,7 +48,7 @@ public function __invoke(QueryExecuted $event, array $trace): array line: $line ?? 0, duration: $durationInMicroseconds, connection: $event->connectionName ?? '', // @phpstan-ignore nullCoalesce.property - usingReadConnection: $this->usingReadConnection($event), + connectionType: $this->connectionType($event), ), function () use ($event, $record) { $this->executionState->queries++; @@ -71,7 +71,7 @@ function () use ($event, $record) { 'line' => $record->line, 'duration' => $record->duration, 'connection' => Str::tinyText($record->connection), - 'using_read_connection' => $record->usingReadConnection, + 'connection_type' => $record->connectionType, ]; }, ]; @@ -93,23 +93,32 @@ private function hash(QueryExecuted $event, Query $record): string } /** - * Determine if the query was executed using the read connection. + * Get the read or write connection type if configured. + * + * @return 'read'|'write'|'' */ - private function usingReadConnection(QueryExecuted $event): bool + private function connectionType(QueryExecuted $event): string { $connection = $event->connection; - $readPdo = $connection->getRawReadPdo(); $writePdo = $connection->getRawPdo(); + if ($readPdo === null) { + return ''; + } + if (! $readPdo instanceof PDO) { - return false; + return 'write'; } if (! $writePdo instanceof PDO) { - return true; + return 'read'; + } + + if ($connection->getReadPdo() === $writePdo) { + return 'write'; } - return $connection->getReadPdo() !== $writePdo; + return 'read'; } } diff --git a/tests/Feature/Sensors/QuerySensorTest.php b/tests/Feature/Sensors/QuerySensorTest.php index 9379070ac..0af8a3486 100644 --- a/tests/Feature/Sensors/QuerySensorTest.php +++ b/tests/Feature/Sensors/QuerySensorTest.php @@ -95,7 +95,7 @@ public function test_it_can_ingest_queries(): void 'line' => $line, 'duration' => 4321, 'connection' => $connection, - 'using_read_connection' => false, + 'connection_type' => '', ], ]); } @@ -356,7 +356,7 @@ public function test_it_can_capture_null_connection_name() $ingest->assertLatestWrite('query:0.connection', ''); } - public function test_it_captures_using_read_connection_as_false_when_read_and_write_connections_are_not_configured() + public function test_it_captures_connection_type_as_empty_string_when_read_and_write_connections_are_not_configured() { $ingest = $this->fakeIngest(); @@ -368,10 +368,10 @@ public function test_it_captures_using_read_connection_as_false_when_read_and_wr $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); + $ingest->assertLatestWrite('query:0.connection_type', ''); } - public function test_it_captures_using_read_connection_as_true_for_select_query() + public function test_it_captures_connection_type_as_read_for_select_query() { $this->configureReadWriteConnection(); @@ -385,10 +385,10 @@ public function test_it_captures_using_read_connection_as_true_for_select_query( $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', true); + $ingest->assertLatestWrite('query:0.connection_type', 'read'); } - public function test_it_captures_using_read_connection_as_false_for_write_query() + public function test_it_captures_connection_type_as_write_for_write_query() { $this->configureReadWriteConnection(); @@ -406,10 +406,10 @@ public function test_it_captures_using_read_connection_as_false_for_write_query( $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); + $ingest->assertLatestWrite('query:0.connection_type', 'write'); } - public function test_it_captures_using_read_connection_as_false_when_records_have_been_modified_and_sticky_connection_is_enabled() + public function test_it_captures_connection_type_as_write_when_records_have_been_modified_and_sticky_connection_is_enabled() { $this->configureReadWriteConnection(['sticky' => true]); @@ -429,11 +429,11 @@ public function test_it_captures_using_read_connection_as_false_when_records_hav $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); // insert - $ingest->assertLatestWrite('query:1.using_read_connection', false); // select + $ingest->assertLatestWrite('query:0.connection_type', 'write'); // insert + $ingest->assertLatestWrite('query:1.connection_type', 'write'); // select } - public function test_it_captures_using_read_connection_as_false_for_insert_and_true_for_select_when_sticky_connection_is_disabled() + public function test_it_captures_connection_type_as_write_for_insert_and_read_for_select_when_sticky_connection_is_disabled() { $this->configureReadWriteConnection(['sticky' => false]); @@ -453,11 +453,11 @@ public function test_it_captures_using_read_connection_as_false_for_insert_and_t $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); // insert - $ingest->assertLatestWrite('query:1.using_read_connection', true); // select + $ingest->assertLatestWrite('query:0.connection_type', 'write'); // insert + $ingest->assertLatestWrite('query:1.connection_type', 'read'); // select } - public function test_it_captures_using_read_connection_as_false_when_it_should_use_write_connection_when_reading() + public function test_it_captures_connection_type_as_write_when_it_should_use_write_connection_when_reading() { $this->configureReadWriteConnection(); @@ -471,10 +471,10 @@ public function test_it_captures_using_read_connection_as_false_when_it_should_u $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); + $ingest->assertLatestWrite('query:0.connection_type', 'write'); } - public function test_it_captures_using_read_connection_as_false_when_in_a_transaction() + public function test_it_captures_connection_type_as_write_when_in_a_transaction() { $this->configureReadWriteConnection(); @@ -494,7 +494,7 @@ public function test_it_captures_using_read_connection_as_false_when_in_a_transa $response->assertOk(); $ingest->assertWrittenTimes(1); - $ingest->assertLatestWrite('query:0.using_read_connection', false); + $ingest->assertLatestWrite('query:0.connection_type', 'write'); } private function configureReadWriteConnection(array $options = []): void From edc39b99f39edbe0dae1bb109cf9e5081d54428f Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Wed, 8 Oct 2025 15:06:05 +0900 Subject: [PATCH 5/7] Return null instead of an empty string --- src/Sensors/QuerySensor.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Sensors/QuerySensor.php b/src/Sensors/QuerySensor.php index c5517a3d7..4ce9079ce 100644 --- a/src/Sensors/QuerySensor.php +++ b/src/Sensors/QuerySensor.php @@ -48,7 +48,7 @@ public function __invoke(QueryExecuted $event, array $trace): array line: $line ?? 0, duration: $durationInMicroseconds, connection: $event->connectionName ?? '', // @phpstan-ignore nullCoalesce.property - connectionType: $this->connectionType($event), + connectionType: $this->connectionType($event) ?? '', ), function () use ($event, $record) { $this->executionState->queries++; @@ -95,16 +95,16 @@ private function hash(QueryExecuted $event, Query $record): string /** * Get the read or write connection type if configured. * - * @return 'read'|'write'|'' + * @return 'read'|'write'|null */ - private function connectionType(QueryExecuted $event): string + private function connectionType(QueryExecuted $event): ?string { $connection = $event->connection; $readPdo = $connection->getRawReadPdo(); $writePdo = $connection->getRawPdo(); if ($readPdo === null) { - return ''; + return null; } if (! $readPdo instanceof PDO) { From de3c09c7f8dc4cfd9d1fcf74ca199379b88ca7a7 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 9 Oct 2025 10:25:50 +1100 Subject: [PATCH 6/7] Add some failing tests --- tests/Feature/Sensors/QuerySensorTest.php | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Feature/Sensors/QuerySensorTest.php b/tests/Feature/Sensors/QuerySensorTest.php index 0af8a3486..a0660c8b7 100644 --- a/tests/Feature/Sensors/QuerySensorTest.php +++ b/tests/Feature/Sensors/QuerySensorTest.php @@ -497,6 +497,45 @@ public function test_it_captures_connection_type_as_write_when_in_a_transaction( $ingest->assertLatestWrite('query:0.connection_type', 'write'); } + public function test_it_captures_write_connection_when_forcing_select_from_write_after_read_pdo_is_resolved() + { + $this->configureReadWriteConnection(); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + DB::select('select 1'); + DB::selectFromWriteConnection('select 1'); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.connection_type', 'read'); + $ingest->assertLatestWrite('query:0.connection_type', 'write'); + } + + public function test_it_captures_connection_type_when_forgetting_modified_records_state() + { + $this->configureReadWriteConnection(['sticky' => true]); + + $ingest = $this->fakeIngest(); + + Route::get('/users', function () { + DB::statement('select 1'); + DB::forgetRecordModificationState(); + DB::select('select 1'); + }); + + $response = $this->get('/users'); + + $response->assertOk(); + $ingest->assertWrittenTimes(1); + $ingest->assertLatestWrite('query:0.connection_type', 'write'); + $ingest->assertLatestWrite('query:0.connection_type', 'read'); + } + private function configureReadWriteConnection(array $options = []): void { $connection = Config::get('database.default'); From 69035c4a21ef4d36c169981b42d17f207dacac36 Mon Sep 17 00:00:00 2001 From: Ryuta Hamasaki Date: Thu, 9 Oct 2025 17:20:21 +0900 Subject: [PATCH 7/7] Fix query index --- tests/Feature/Sensors/QuerySensorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Sensors/QuerySensorTest.php b/tests/Feature/Sensors/QuerySensorTest.php index a0660c8b7..24314e12f 100644 --- a/tests/Feature/Sensors/QuerySensorTest.php +++ b/tests/Feature/Sensors/QuerySensorTest.php @@ -513,7 +513,7 @@ public function test_it_captures_write_connection_when_forcing_select_from_write $response->assertOk(); $ingest->assertWrittenTimes(1); $ingest->assertLatestWrite('query:0.connection_type', 'read'); - $ingest->assertLatestWrite('query:0.connection_type', 'write'); + $ingest->assertLatestWrite('query:1.connection_type', 'write'); } public function test_it_captures_connection_type_when_forgetting_modified_records_state() @@ -533,7 +533,7 @@ public function test_it_captures_connection_type_when_forgetting_modified_record $response->assertOk(); $ingest->assertWrittenTimes(1); $ingest->assertLatestWrite('query:0.connection_type', 'write'); - $ingest->assertLatestWrite('query:0.connection_type', 'read'); + $ingest->assertLatestWrite('query:1.connection_type', 'read'); } private function configureReadWriteConnection(array $options = []): void