From cd8f5dcfd1e42ae72513e2044c626b0525e5a495 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 4 Jul 2026 23:02:58 +0200 Subject: [PATCH 1/6] test(database): fix state leaks in MySQL random-order execution --- tests/system/Database/Live/ConnectTest.php | 14 +++++- tests/system/Database/Live/ForgeTest.php | 45 ++++++++++++++++--- tests/system/Database/Live/GetVersionTest.php | 3 +- .../Database/Live/MySQLi/FoundRowsTest.php | 16 +++---- .../Database/Live/MySQLi/NumberNativeTest.php | 8 ++-- tests/system/Database/Live/WorkerModeTest.php | 2 +- .../Migrations/MigrationRunnerTest.php | 4 +- 7 files changed, 66 insertions(+), 26 deletions(-) diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php index e41fdbdfc114..5ad69bb26067 100644 --- a/tests/system/Database/Live/ConnectTest.php +++ b/tests/system/Database/Live/ConnectTest.php @@ -46,11 +46,21 @@ protected function setUp(): void $this->group2['DBDriver'] = 'Postgre'; } + protected function tearDown(): void + { + parent::tearDown(); + $this->setPrivateProperty(Database::class, 'instances', []); + } + + public function testConnectWithMultipleCustomGroups(): void { + $this->group1['DBPrefix'] = uniqid('g1_', true); + $this->group2['DBPrefix'] = uniqid('g2_', true); + // We should have our test database connection already. $instances = $this->getPrivateProperty(Database::class, 'instances'); - $this->assertCount(1, $instances); + $initialCount = count($instances); $db1 = Database::connect($this->group1); $db2 = Database::connect($this->group2); @@ -58,7 +68,7 @@ public function testConnectWithMultipleCustomGroups(): void $this->assertNotSame($db1, $db2); $instances = $this->getPrivateProperty(Database::class, 'instances'); - $this->assertCount(3, $instances); + $this->assertCount($initialCount + 2, $instances); } public function testConnectReturnsProvidedConnection(): void diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 39433abde857..70b084ddd7b6 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -36,19 +36,52 @@ final class ForgeTest extends CIUnitTestCase protected $seed = CITestSeeder::class; private Forge $forge; + private function dropAllMockTables(): void + { + $tablesToDrop = [ + 'forge_test_invoices', + 'forge_test_inv', + 'forge_test_users', + 'actions', + 'forge_test_table', + 'test_exists', + 'forge_test_attributes', + 'forge_array_constraint', + 'forge_nullable_table', + 'forge_test_1', + 'forge_test_two', + 'forge_test_three', + 'forge_test_four', + 'forge_test_modify', + 'droptest', + 'key_test_users', + 'test_stores', + 'user2', + 'forge_test_table_dummy', + ]; + foreach ($tablesToDrop as $table) { + $this->forge->dropTable($table, true); + } + } + protected function setUp(): void { $this->forge = Database::forge($this->DBGroup); - // when running locally if one of these tables isn't dropped it may cause error - $this->forge->dropTable('forge_test_invoices', true); - $this->forge->dropTable('forge_test_inv', true); - $this->forge->dropTable('forge_test_users', true); - $this->forge->dropTable('actions', true); + $this->dropAllMockTables(); + + db_connect($this->DBGroup)->resetDataCache(); parent::setUp(); } + protected function tearDown(): void + { + parent::tearDown(); + $this->dropAllMockTables(); + self::$doneMigration = false; + } + public function testCreateDatabase(): void { if ($this->db->DBDriver === 'OCI8') { @@ -75,7 +108,7 @@ public function testCreateDatabaseWithDots(): void // Checks if tableExists() works. $config = config(Database::class)->{$this->DBGroup}; $config['database'] = $dbName; - $db = db_connect($config); + $db = db_connect($config, false); $result = $db->tableExists('not_exist'); $this->assertFalse($result); diff --git a/tests/system/Database/Live/GetVersionTest.php b/tests/system/Database/Live/GetVersionTest.php index 93678e3b8356..ad94134ff659 100644 --- a/tests/system/Database/Live/GetVersionTest.php +++ b/tests/system/Database/Live/GetVersionTest.php @@ -36,7 +36,6 @@ public function testGetVersion(): void $this->db->connID = false; $version = $this->db->getVersion(); - - $this->assertMatchesRegularExpression('/\A\d+(\.\d+)*\z/', $version); + $this->assertMatchesRegularExpression('/\A\d+(\.\d+)*/', $version); } } diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php index b39f8999085d..f5a42b3e5a48 100644 --- a/tests/system/Database/Live/MySQLi/FoundRowsTest.php +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -54,7 +54,7 @@ public function testEnableFoundRows(): void { $this->tests['foundRows'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $this->assertTrue($db1->foundRows); } @@ -63,7 +63,7 @@ public function testDisableFoundRows(): void { $this->tests['foundRows'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $this->assertFalse($db1->foundRows); } @@ -72,7 +72,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithNoChange(): void { $this->tests['foundRows'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('country', 'US') @@ -88,7 +88,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void { $this->tests['foundRows'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('country', 'US') @@ -104,7 +104,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithChange(): void { $this->tests['foundRows'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('country', 'NZ') @@ -120,7 +120,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithChange(): void { $this->tests['foundRows'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('country', 'NZ') @@ -136,7 +136,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void { $this->tests['foundRows'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('name', 'Derek Jones') @@ -152,7 +152,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void { $this->tests['foundRows'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); $db1->table('db_user') ->set('name', 'Derek Jones') diff --git a/tests/system/Database/Live/MySQLi/NumberNativeTest.php b/tests/system/Database/Live/MySQLi/NumberNativeTest.php index 4469e4c3659a..b9186257b6c8 100644 --- a/tests/system/Database/Live/MySQLi/NumberNativeTest.php +++ b/tests/system/Database/Live/MySQLi/NumberNativeTest.php @@ -44,7 +44,7 @@ public function testEnableNumberNative(): void { $this->tests['numberNative'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); if ($db1->DBDriver !== 'MySQLi') { $this->markTestSkipped('Only MySQLi can complete this test.'); @@ -57,7 +57,7 @@ public function testDisableNumberNative(): void { $this->tests['numberNative'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); if ($db1->DBDriver !== 'MySQLi') { $this->markTestSkipped('Only MySQLi can complete this test.'); @@ -70,7 +70,7 @@ public function testQueryDataAfterEnableNumberNative(): void { $this->tests['numberNative'] = true; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); if ($db1->DBDriver !== 'MySQLi') { $this->markTestSkipped('Only MySQLi can complete this test.'); @@ -88,7 +88,7 @@ public function testQueryDataAfterDisableNumberNative(): void { $this->tests['numberNative'] = false; - $db1 = Database::connect($this->tests); + $db1 = Database::connect($this->tests, false); if ($db1->DBDriver !== 'MySQLi') { $this->markTestSkipped('Only MySQLi can complete this test.'); diff --git a/tests/system/Database/Live/WorkerModeTest.php b/tests/system/Database/Live/WorkerModeTest.php index a8c77d756da7..ca18091b9802 100644 --- a/tests/system/Database/Live/WorkerModeTest.php +++ b/tests/system/Database/Live/WorkerModeTest.php @@ -27,10 +27,10 @@ final class WorkerModeTest extends CIUnitTestCase { use DatabaseTestTrait; + protected function tearDown(): void { parent::tearDown(); - $this->setPrivateProperty(Config::class, 'instances', []); } diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index 510c8169fa34..706d4f86fc6e 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -69,9 +69,7 @@ protected function tearDown(): void { parent::tearDown(); - // To delete data with `$this->regressDatabase()`, set it true. - $this->migrate = true; - $this->regressDatabase(); + $this->resetTables(); } public function testLoadsDefaultDatabaseWhenNoneSpecified(): void From 24d5a44812f83c7728ae4512fc28a4182513383f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 4 Jul 2026 23:21:33 +0200 Subject: [PATCH 2/6] test(Database): Fix PostgreSQL random order test failures --- tests/system/Database/Live/ForgeTest.php | 18 ++++++++++++++++++ .../Database/Live/Postgre/ConnectTest.php | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 70b084ddd7b6..aac85ee0867c 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -88,6 +88,12 @@ public function testCreateDatabase(): void $this->markTestSkipped('OCI8 does not support create database.'); } + try { + $this->forge->dropDatabase('test_forge_database'); + } catch (DatabaseException $e) { + // Ignore if doesn't exist + } + $databaseCreated = $this->forge->createDatabase('test_forge_database'); $this->assertTrue($databaseCreated); @@ -101,6 +107,12 @@ public function testCreateDatabaseWithDots(): void $dbName = 'test_com.sitedb.web'; + try { + $this->forge->dropDatabase($dbName); + } catch (DatabaseException $e) { + // Ignore if doesn't exist + } + $databaseCreated = $this->forge->createDatabase($dbName); $this->assertTrue($databaseCreated); @@ -184,6 +196,12 @@ public function testDropDatabase(): void $this->markTestSkipped('SQLite3 requires file path to drop database'); } + try { + $this->forge->createDatabase('test_forge_database'); + } catch (DatabaseException $e) { + // Ignore if exists + } + $databaseDropped = $this->forge->dropDatabase('test_forge_database'); $this->assertTrue($databaseDropped); diff --git a/tests/system/Database/Live/Postgre/ConnectTest.php b/tests/system/Database/Live/Postgre/ConnectTest.php index d616a60b968c..001fa222df20 100644 --- a/tests/system/Database/Live/Postgre/ConnectTest.php +++ b/tests/system/Database/Live/Postgre/ConnectTest.php @@ -47,7 +47,7 @@ public function testShowErrorMessageWhenSettingInvalidCharset(): void $group = $config->tests; // Sets invalid charset. $group['charset'] = 'utf8mb4'; - $db = Database::connect($group); + $db = Database::connect($group, false); // Actually connect to DB. $db->initialize(); From 8cc20d34d9dd54323d29be7de33217ca97c12ee2 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 4 Jul 2026 23:22:34 +0200 Subject: [PATCH 3/6] style(Database): apply cs-fix to test files --- tests/system/Database/Live/ConnectTest.php | 3 +-- tests/system/Database/Live/ForgeTest.php | 1 + tests/system/Database/Live/WorkerModeTest.php | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php index 5ad69bb26067..9b2f261e3bc0 100644 --- a/tests/system/Database/Live/ConnectTest.php +++ b/tests/system/Database/Live/ConnectTest.php @@ -52,14 +52,13 @@ protected function tearDown(): void $this->setPrivateProperty(Database::class, 'instances', []); } - public function testConnectWithMultipleCustomGroups(): void { $this->group1['DBPrefix'] = uniqid('g1_', true); $this->group2['DBPrefix'] = uniqid('g2_', true); // We should have our test database connection already. - $instances = $this->getPrivateProperty(Database::class, 'instances'); + $instances = $this->getPrivateProperty(Database::class, 'instances'); $initialCount = count($instances); $db1 = Database::connect($this->group1); diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index aac85ee0867c..90cb5fa40e1e 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -59,6 +59,7 @@ private function dropAllMockTables(): void 'user2', 'forge_test_table_dummy', ]; + foreach ($tablesToDrop as $table) { $this->forge->dropTable($table, true); } diff --git a/tests/system/Database/Live/WorkerModeTest.php b/tests/system/Database/Live/WorkerModeTest.php index ca18091b9802..f614f8d68df1 100644 --- a/tests/system/Database/Live/WorkerModeTest.php +++ b/tests/system/Database/Live/WorkerModeTest.php @@ -27,7 +27,6 @@ final class WorkerModeTest extends CIUnitTestCase { use DatabaseTestTrait; - protected function tearDown(): void { parent::tearDown(); From fc9e325761db8a7df6ce32e1e091d38b2510b0c0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 6 Jul 2026 00:58:06 +0200 Subject: [PATCH 4/6] fix(Database): essential OCI8 fixes for deterministic test execution - Connection::initialize() sets NLS session formats via ALTER SESSION, preventing ORA-01843 when env vars are not set. - _indexData() handles null CONSTRAINT_TYPE to avoid deprecated null array offset error. - MetadataTest captures prefix before createExtraneousTable() to prevent prefix leak on exception. - ExecuteLogMessageFormatTest removes stale OCI8-specific array_slice. --- system/Database/OCI8/Connection.php | 13 ++++++++++++- .../Database/Live/ExecuteLogMessageFormatTest.php | 2 -- tests/system/Database/Live/MetadataTest.php | 4 +--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index dc884588a251..44f2f48a2a59 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -150,6 +150,17 @@ public function connect(bool $persistent = false) : $func($this->username, $this->password, $this->DSN, $this->charset); } + public function initialize() + { + parent::initialize(); + + if ($this->connID) { + $this->simpleQuery("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + } + } + /** * Close the database connection. * @@ -422,7 +433,7 @@ protected function _indexData(string $table): array $retVal[$row->INDEX_NAME] = new stdClass(); $retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME; $retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME]; - $retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX'; + $retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE ?? ''] ?? 'INDEX'; } return $retVal; diff --git a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php index 9913a2da05c0..ae4aca2398b3 100644 --- a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php +++ b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php @@ -60,8 +60,6 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi if ($db->DBDriver === 'Postgre') { $messageFromLogs = array_slice($messageFromLogs, 2); - } elseif ($db->DBDriver === 'OCI8') { - $messageFromLogs = array_slice($messageFromLogs, 1); } $this->assertMatchesRegularExpression('/^in \S+ on line \d+\.$/', array_shift($messageFromLogs)); diff --git a/tests/system/Database/Live/MetadataTest.php b/tests/system/Database/Live/MetadataTest.php index 5030a6544231..b17a53930900 100644 --- a/tests/system/Database/Live/MetadataTest.php +++ b/tests/system/Database/Live/MetadataTest.php @@ -120,12 +120,10 @@ public function testListTablesConstrainedByPrefixReturnsOnlyTablesWithMatchingPr public function testListTablesConstrainedByExtraneousPrefixReturnsOnlyTheExtraneousTable(): void { - $oldPrefix = ''; + $oldPrefix = $this->db->getPrefix(); try { $this->createExtraneousTable(); - - $oldPrefix = $this->db->getPrefix(); $this->db->setPrefix('tmp_'); $tables = $this->db->listTables(true); From da7a700b137993b17e96b2b09e0ef37fc8e8ac18 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 6 Jul 2026 01:06:41 +0200 Subject: [PATCH 5/6] style(Database): remove unused catch variable in ForgeTest --- tests/system/Database/Live/ForgeTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 90cb5fa40e1e..365e6073bfea 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -91,7 +91,7 @@ public function testCreateDatabase(): void try { $this->forge->dropDatabase('test_forge_database'); - } catch (DatabaseException $e) { + } catch (DatabaseException) { // Ignore if doesn't exist } @@ -110,7 +110,7 @@ public function testCreateDatabaseWithDots(): void try { $this->forge->dropDatabase($dbName); - } catch (DatabaseException $e) { + } catch (DatabaseException) { // Ignore if doesn't exist } @@ -199,7 +199,7 @@ public function testDropDatabase(): void try { $this->forge->createDatabase('test_forge_database'); - } catch (DatabaseException $e) { + } catch (DatabaseException) { // Ignore if exists } From d11cab47e345624e30516c5bed4c0e65201d30cd Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 6 Jul 2026 01:26:38 +0200 Subject: [PATCH 6/6] fix(Database): robustly find 'in ... on line ...' in OCI8 log message --- .../Database/Live/ExecuteLogMessageFormatTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php index ae4aca2398b3..e678fc16ad1a 100644 --- a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php +++ b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php @@ -62,7 +62,16 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi $messageFromLogs = array_slice($messageFromLogs, 2); } - $this->assertMatchesRegularExpression('/^in \S+ on line \d+\.$/', array_shift($messageFromLogs)); + $inLine = null; + + while (($line = array_shift($messageFromLogs)) !== null) { + if (preg_match('/^in \S+ on line \d+\.$/', $line)) { + $inLine = $line; + break; + } + } + + $this->assertNotNull($inLine, 'Could not find "in ... on line ..." in log message'); foreach ($messageFromLogs as $line) { $this->assertMatchesRegularExpression('/^\s*\d* .+(?:\(\d+\))?: \S+(?:(?:\->|::)\S+)?\(.*\)$/', $line);