From 78143f31096e5146b3c6dfa9d394932ec2bdcfc5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:05:59 +0200 Subject: [PATCH 1/7] fix(Database): fix random-order test execution issues and state leakage under PostgreSQL, MySQL, and OCI8 --- .github/scripts/random-tests-config.txt | 2 +- .github/workflows/test-random-execution.yml | 26 ++++--- system/Database/OCI8/Connection.php | 21 ++++-- system/Database/Postgre/Connection.php | 20 +++++- tests/_support/Config/Registrar.php | 67 ++++++++++++++++++- .../20160428212500_Create_test_tables.php | 23 ++++++- tests/system/Database/Live/ConnectTest.php | 15 ++++- .../Live/ExecuteLogMessageFormatTest.php | 15 +++-- tests/system/Database/Live/ForgeTest.php | 63 +++++++++++++++-- tests/system/Database/Live/GetVersionTest.php | 3 +- tests/system/Database/Live/MetadataTest.php | 6 +- .../Database/Live/MySQLi/FoundRowsTest.php | 16 ++--- .../Database/Live/MySQLi/NumberNativeTest.php | 8 +-- .../Database/Live/Postgre/ConnectTest.php | 2 +- tests/system/Database/Live/UpsertTest.php | 23 +++---- tests/system/Database/Live/WorkerModeTest.php | 1 - .../Migrations/MigrationRunnerTest.php | 1 + 17 files changed, 248 insertions(+), 64 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index 5bf0fce66733..0c667b4efb46 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -18,7 +18,7 @@ Config Cookie # DataCaster # DataConverter -# Database +Database # Debug Email # Encryption diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index e90de6bb031d..44dc8c78e562 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -177,7 +177,7 @@ jobs: uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php-version }} - extensions: gd, curl, iconv, json, mbstring, openssl, sodium + extensions: gd, curl, iconv, json, mbstring, openssl, sodium, mysqli, oci8, pgsql, sqlsrv, sqlite3 ini-values: opcache.enable_cli=0 coverage: none @@ -212,16 +212,24 @@ jobs: args+=("--component" "${{ inputs.component }}") fi - # Add --max-jobs flag if specified (empty means auto-detect) - if [[ -n "${{ inputs.max-jobs }}" ]]; then - args+=("--max-jobs" "${{ inputs.max-jobs }}") + # OCI8 connects to a single shared schema (FREEPDB1) via DSN, so + # components cannot be isolated with per-component databases like + # MySQLi/Postgre/SQLSRV. Running components in parallel makes + # e.g. Commands' migrate:rollback drop tables that Database tests + # rely on (ORA-00942/04043/08103). Run Oracle sequentially with + # default repeat 2 to avoid schema collisions and timeouts. + if [[ "${{ matrix.db-platform }}" == "Oracle" ]]; then + args+=("--max-jobs" "1") + args+=("--repeat" "${{ inputs.repeat || '2' }}") + else + if [[ -n "${{ inputs.max-jobs }}" ]]; then + args+=("--max-jobs" "${{ inputs.max-jobs }}") + fi + args+=("--repeat" "${{ inputs.repeat || '10' }}") fi - # Add --repeat flag (always, default is 10) - args+=("--repeat" "${{ inputs.repeat || '10' }}") - - # Add --timeout flag (always, default is 300) - args+=("--timeout" "${{ inputs.timeout || '300' }}") + # Add --timeout flag (always, default is 600) + args+=("--timeout" "${{ inputs.timeout || '600' }}") .github/scripts/run-random-tests.sh "${args[@]}" env: diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index dc884588a251..547a4f24b1ae 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -20,6 +20,8 @@ use ErrorException; use stdClass; +defined('OCI_COMMIT_ON_SUCCESS') || define('OCI_COMMIT_ON_SUCCESS', 32); + /** * Connection for OCI8 * @@ -150,6 +152,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. * @@ -288,11 +301,11 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n $sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"'; if ($tableName !== null) { - return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape($tableName); + return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape(strtoupper($tableName)); } if ($prefixLimit && $this->DBPrefix !== '') { - return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' " + return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString(strtoupper($this->DBPrefix)) . "%' " . sprintf($this->likeEscapeStr, $this->likeEscapeChar); } @@ -397,7 +410,7 @@ protected function _indexData(string $table): array $sql = 'SELECT AIC.INDEX_NAME, UC.CONSTRAINT_TYPE, AIC.COLUMN_NAME ' . ' FROM ALL_IND_COLUMNS AIC ' . ' LEFT JOIN USER_CONSTRAINTS UC ON AIC.INDEX_NAME = UC.CONSTRAINT_NAME AND AIC.TABLE_NAME = UC.TABLE_NAME ' - . 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtolower($table)) . ' ' + . 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtoupper($table)) . ' ' . 'AND AIC.TABLE_OWNER = ' . $this->escape(strtoupper($owner)) . ' ' . ' ORDER BY UC.CONSTRAINT_TYPE, AIC.COLUMN_POSITION'; @@ -422,7 +435,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/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 4c3358a4b470..c1c693551fb3 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -22,6 +22,7 @@ use PgSql\Result as PgSqlResult; use stdClass; use Stringable; +use Throwable; /** * Connection for Postgre @@ -149,7 +150,14 @@ private function convertDSN() */ protected function _close() { - pg_close($this->connID); + if ($this->connID !== false) { + try { + pg_close($this->connID); + } catch (Throwable) { + } finally { + $this->connID = false; + } + } } /** @@ -157,7 +165,15 @@ protected function _close() */ protected function _ping(): bool { - return pg_ping($this->connID); + if ($this->connID === false) { + return false; + } + + try { + return pg_ping($this->connID); + } catch (Throwable) { + return false; + } } /** diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index 058fec440b55..4869617f9ad3 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -13,6 +13,10 @@ namespace Tests\Support\Config; +use mysqli; +use PDO; +use Throwable; + /** * Class Registrar * @@ -137,7 +141,68 @@ public static function Database(): array // so that we can test against multiple databases. $group = env('DB', 'SQLite3'); - $config['tests'] = self::$dbConfig[$group] ?? []; + if ($group === 'Oracle') { + $group = 'OCI8'; + } + + $dbParams = self::$dbConfig[$group] ?? []; + + if (! empty($dbParams) && ! in_array($group, ['SQLite3', 'OCI8'], true)) { + $componentName = ''; + + foreach ($_SERVER['argv'] ?? [] as $arg) { + if (str_contains($arg, 'tests/system/')) { + $parts = explode('tests/system/', $arg); + if (isset($parts[1])) { + $componentName = explode('/', $parts[1])[0]; + break; + } + } + } + + if ($componentName !== '') { + $dbParams['database'] = 'test_' . strtolower($componentName); + + try { + if ($group === 'MySQLi') { + $conn = new mysqli( + $dbParams['hostname'], + $dbParams['username'], + $dbParams['password'], + '', + (int) $dbParams['port'], + ); + if (! $conn->connect_error) { + $conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database'])); + $conn->close(); + } + } elseif ($group === 'Postgre') { + $dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password']; + $pdo = new PDO($dsn); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $dbName = str_replace('"', '""', $dbParams['database']); + $pdo->exec('CREATE DATABASE "' . $dbName . '"'); + } + } elseif ($group === 'SQLSRV') { + $dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True'; + $pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8'); + } + } + } catch (Throwable) { + // Ignore any error and let the connection fail naturally + } + } + } + + $config['tests'] = $dbParams; return $config; } diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 74fb2aa072f3..4dc887df1efb 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -14,6 +14,7 @@ namespace Tests\Support\Database\Migrations; use CodeIgniter\Database\Migration; +use Throwable; class Migration_Create_test_tables extends Migration { @@ -196,9 +197,25 @@ public function down(): void } if ($this->db->DBDriver === 'OCI8') { - $this->db->query('DROP PROCEDURE one'); - $this->db->query('DROP PROCEDURE plus'); - $this->db->query('DROP PACKAGE BODY calculator'); + try { + $this->db->query('DROP PROCEDURE one'); + } catch (Throwable) { + } + + try { + $this->db->query('DROP PROCEDURE plus'); + } catch (Throwable) { + } + + try { + $this->db->query('DROP PACKAGE BODY calculator'); + } catch (Throwable) { + } + + try { + $this->db->query('DROP PACKAGE calculator'); + } catch (Throwable) { + } } } } diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php index e41fdbdfc114..9b2f261e3bc0 100644 --- a/tests/system/Database/Live/ConnectTest.php +++ b/tests/system/Database/Live/ConnectTest.php @@ -46,11 +46,20 @@ 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); + $instances = $this->getPrivateProperty(Database::class, 'instances'); + $initialCount = count($instances); $db1 = Database::connect($this->group1); $db2 = Database::connect($this->group2); @@ -58,7 +67,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/ExecuteLogMessageFormatTest.php b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php index 9913a2da05c0..1884b76d3633 100644 --- a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php +++ b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php @@ -47,7 +47,7 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi $db->query($sql, [3, 'live', 'Rick']); $pattern = match ($db->DBDriver) { - 'MySQLi' => '/Table \'test\.some_table\' doesn\'t exist/', + 'MySQLi' => '/Table \'' . preg_quote($db->database, '/') . '\.some_table\' doesn\'t exist/', 'Postgre' => '/pg_query\(\): Query failed: ERROR: relation "some_table" does not exist/', 'SQLite3' => '/Unable to prepare statement:\s(\d+,\s)?no such table: some_table/', 'OCI8' => '/oci_execute\(\): ORA-00942: table or view "ORACLE"\."SOME_TABLE" does not exist/', @@ -60,11 +60,18 @@ 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)); + $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); diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 39433abde857..1f18d02b625d 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -36,25 +36,64 @@ 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(); + } + public function testCreateDatabase(): void { if ($this->db->DBDriver === 'OCI8') { $this->markTestSkipped('OCI8 does not support create database.'); } + try { + $this->forge->dropDatabase('test_forge_database'); + } catch (DatabaseException) { + // Ignore if doesn't exist + } + $databaseCreated = $this->forge->createDatabase('test_forge_database'); $this->assertTrue($databaseCreated); @@ -68,6 +107,12 @@ public function testCreateDatabaseWithDots(): void $dbName = 'test_com.sitedb.web'; + try { + $this->forge->dropDatabase($dbName); + } catch (DatabaseException) { + // Ignore if doesn't exist + } + $databaseCreated = $this->forge->createDatabase($dbName); $this->assertTrue($databaseCreated); @@ -75,7 +120,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); @@ -151,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) { + // Ignore if exists + } + $databaseDropped = $this->forge->dropDatabase('test_forge_database'); $this->assertTrue($databaseDropped); 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/MetadataTest.php b/tests/system/Database/Live/MetadataTest.php index 5030a6544231..34c50cd571be 100644 --- a/tests/system/Database/Live/MetadataTest.php +++ b/tests/system/Database/Live/MetadataTest.php @@ -34,6 +34,8 @@ protected function setUp(): void { parent::setUp(); + Database::forge($this->DBGroup)->dropTable('migrations_lock', true); + $prefix = $this->db->getPrefix(); $tables = [ @@ -120,12 +122,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); 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/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(); diff --git a/tests/system/Database/Live/UpsertTest.php b/tests/system/Database/Live/UpsertTest.php index 000fa6fec7cb..99bf86ea72ac 100644 --- a/tests/system/Database/Live/UpsertTest.php +++ b/tests/system/Database/Live/UpsertTest.php @@ -253,18 +253,17 @@ public function testGetCompiledUpsert(): void break; case 'SQLSRV': - $expected = <<<'SQL' - MERGE INTO "test"."dbo"."db_user" - USING ( - VALUES ('Iran','ahmadinejad@world.com','Ahmadinejad') - ) "_upsert" ("country", "email", "name") - ON ("test"."dbo"."db_user"."email" = "_upsert"."email") - WHEN MATCHED THEN UPDATE SET - "country" = "_upsert"."country", - "name" = "_upsert"."name" - WHEN NOT MATCHED THEN INSERT ("country", "email", "name") - VALUES ("_upsert"."country", "_upsert"."email", "_upsert"."name"); - SQL; + $qualified = '"' . $this->db->getDatabase() . '"."dbo"."db_user"'; + $expected = 'MERGE INTO ' . $qualified . "\n" + . "USING (\n" + . "VALUES ('Iran','ahmadinejad@world.com','Ahmadinejad')\n" + . ') "_upsert" ("country", "email", "name")' . "\n" + . 'ON (' . $qualified . '."email" = "_upsert"."email")' . "\n" + . "WHEN MATCHED THEN UPDATE SET\n" + . "\"country\" = \"_upsert\".\"country\",\n" + . "\"name\" = \"_upsert\".\"name\"\n" + . 'WHEN NOT MATCHED THEN INSERT ("country", "email", "name")' . "\n" + . 'VALUES ("_upsert"."country", "_upsert"."email", "_upsert"."name");'; break; case 'OCI8': diff --git a/tests/system/Database/Live/WorkerModeTest.php b/tests/system/Database/Live/WorkerModeTest.php index a8c77d756da7..f614f8d68df1 100644 --- a/tests/system/Database/Live/WorkerModeTest.php +++ b/tests/system/Database/Live/WorkerModeTest.php @@ -30,7 +30,6 @@ final class WorkerModeTest extends CIUnitTestCase 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..76a5a64a02d7 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -72,6 +72,7 @@ protected function tearDown(): void // To delete data with `$this->regressDatabase()`, set it true. $this->migrate = true; $this->regressDatabase(); + Database::forge($this->DBGroup)->dropTable('migrations_lock', true); } public function testLoadsDefaultDatabaseWhenNoneSpecified(): void From c2f678b28fb8c3798c0cbfe61a0f32b687a640b5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:12:23 +0200 Subject: [PATCH 2/7] ci(random-tests): isolate OCI8 components with per-component database schemas to enable parallel execution --- .github/workflows/test-random-execution.yml | 24 +++++---------- tests/_support/Config/Registrar.php | 34 +++++++++++++++++++-- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 44dc8c78e562..d8730ef2846f 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -212,24 +212,16 @@ jobs: args+=("--component" "${{ inputs.component }}") fi - # OCI8 connects to a single shared schema (FREEPDB1) via DSN, so - # components cannot be isolated with per-component databases like - # MySQLi/Postgre/SQLSRV. Running components in parallel makes - # e.g. Commands' migrate:rollback drop tables that Database tests - # rely on (ORA-00942/04043/08103). Run Oracle sequentially with - # default repeat 2 to avoid schema collisions and timeouts. - if [[ "${{ matrix.db-platform }}" == "Oracle" ]]; then - args+=("--max-jobs" "1") - args+=("--repeat" "${{ inputs.repeat || '2' }}") - else - if [[ -n "${{ inputs.max-jobs }}" ]]; then - args+=("--max-jobs" "${{ inputs.max-jobs }}") - fi - args+=("--repeat" "${{ inputs.repeat || '10' }}") + # Add --max-jobs flag if specified (empty means auto-detect) + if [[ -n "${{ inputs.max-jobs }}" ]]; then + args+=("--max-jobs" "${{ inputs.max-jobs }}") fi - # Add --timeout flag (always, default is 600) - args+=("--timeout" "${{ inputs.timeout || '600' }}") + # Add --repeat flag (always, default is 10) + args+=("--repeat" "${{ inputs.repeat || '10' }}") + + # Add --timeout flag (always, default is 300) + args+=("--timeout" "${{ inputs.timeout || '300' }}") .github/scripts/run-random-tests.sh "${args[@]}" env: diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index 4869617f9ad3..b3e6ff01861d 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -147,7 +147,7 @@ public static function Database(): array $dbParams = self::$dbConfig[$group] ?? []; - if (! empty($dbParams) && ! in_array($group, ['SQLite3', 'OCI8'], true)) { + if (! empty($dbParams) && $group !== 'SQLite3') { $componentName = ''; foreach ($_SERVER['argv'] ?? [] as $arg) { @@ -161,7 +161,36 @@ public static function Database(): array } if ($componentName !== '') { - $dbParams['database'] = 'test_' . strtolower($componentName); + if ($group === 'OCI8') { + $compUser = strtoupper('t_' . substr(preg_replace('/[^a-zA-Z0-9]/', '', $componentName), 0, 20)); + $tns = '//' . $dbParams['hostname'] . ':' . $dbParams['port'] . '/' . $dbParams['database']; + + try { + $conn = @oci_connect($dbParams['username'], $dbParams['password'], $tns); + if ($conn !== false) { + $stmt = @oci_parse($conn, 'SELECT USERNAME FROM ALL_USERS WHERE USERNAME = :usr'); + @oci_bind_by_name($stmt, ':usr', $compUser); + @oci_execute($stmt); + + if (@oci_fetch_array($stmt, OCI_ASSOC) === false) { + $stmt2 = @oci_parse($conn, 'CREATE USER ' . $compUser . ' IDENTIFIED BY ' . $compUser); + @oci_execute($stmt2); + $stmt3 = @oci_parse($conn, 'GRANT CONNECT, RESOURCE, DBA TO ' . $compUser); + @oci_execute($stmt3); + $stmt4 = @oci_parse($conn, 'GRANT UNLIMITED TABLESPACE TO ' . $compUser); + @oci_execute($stmt4); + } + + @oci_close($conn); + + $dbParams['username'] = $compUser; + $dbParams['password'] = $compUser; + } + } catch (Throwable) { + // Ignore error and fall back to default user + } + } else { + $dbParams['database'] = 'test_' . strtolower($componentName); try { if ($group === 'MySQLi') { @@ -201,6 +230,7 @@ public static function Database(): array } } } + } $config['tests'] = $dbParams; From a68d3738661cc25978f566b5c0971a60d70aac9c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:17:54 +0200 Subject: [PATCH 3/7] fix(Config): safely extract dbParams keys with null coalescing in Registrar --- tests/_support/Config/Registrar.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index b3e6ff01861d..b0cf061a02d7 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -162,11 +162,17 @@ public static function Database(): array if ($componentName !== '') { if ($group === 'OCI8') { + $hostname = $dbParams['hostname'] ?? '127.0.0.1'; + $port = $dbParams['port'] ?? 1521; + $database = $dbParams['database'] ?? 'FREEPDB1'; + $username = $dbParams['username'] ?? 'ORACLE'; + $password = $dbParams['password'] ?? 'ORACLE'; + $compUser = strtoupper('t_' . substr(preg_replace('/[^a-zA-Z0-9]/', '', $componentName), 0, 20)); - $tns = '//' . $dbParams['hostname'] . ':' . $dbParams['port'] . '/' . $dbParams['database']; + $tns = '//' . $hostname . ':' . $port . '/' . $database; try { - $conn = @oci_connect($dbParams['username'], $dbParams['password'], $tns); + $conn = @oci_connect($username, $password, $tns); if ($conn !== false) { $stmt = @oci_parse($conn, 'SELECT USERNAME FROM ALL_USERS WHERE USERNAME = :usr'); @oci_bind_by_name($stmt, ':usr', $compUser); From 627d3af0c889de3f70adeee82a2c5b35c712cc80 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:26:25 +0200 Subject: [PATCH 4/7] style: fix coding standards in Registrar.php --- tests/_support/Config/Registrar.php | 68 ++++++++++++++--------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index b0cf061a02d7..8c913534ea9e 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -198,45 +198,45 @@ public static function Database(): array } else { $dbParams['database'] = 'test_' . strtolower($componentName); - try { - if ($group === 'MySQLi') { - $conn = new mysqli( - $dbParams['hostname'], - $dbParams['username'], - $dbParams['password'], - '', - (int) $dbParams['port'], - ); - if (! $conn->connect_error) { - $conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database'])); - $conn->close(); - } - } elseif ($group === 'Postgre') { - $dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password']; - $pdo = new PDO($dsn); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?'); - $stmt->execute([$dbParams['database']]); - if (! $stmt->fetchColumn()) { - $dbName = str_replace('"', '""', $dbParams['database']); - $pdo->exec('CREATE DATABASE "' . $dbName . '"'); - } - } elseif ($group === 'SQLSRV') { - $dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True'; - $pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?'); - $stmt->execute([$dbParams['database']]); - if (! $stmt->fetchColumn()) { - $pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8'); + try { + if ($group === 'MySQLi') { + $conn = new mysqli( + $dbParams['hostname'], + $dbParams['username'], + $dbParams['password'], + '', + (int) $dbParams['port'], + ); + if (! $conn->connect_error) { + $conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database'])); + $conn->close(); + } + } elseif ($group === 'Postgre') { + $dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password']; + $pdo = new PDO($dsn); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $dbName = str_replace('"', '""', $dbParams['database']); + $pdo->exec('CREATE DATABASE "' . $dbName . '"'); + } + } elseif ($group === 'SQLSRV') { + $dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True'; + $pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8'); + } } + } catch (Throwable) { + // Ignore any error and let the connection fail naturally } - } catch (Throwable) { - // Ignore any error and let the connection fail naturally } } } - } $config['tests'] = $dbParams; From eff8e83cdf57251815ae212cf4d812b8d8b32e9b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:32:25 +0200 Subject: [PATCH 5/7] fix(Database): drop team_members table in migration Create_test_tables down method --- .../Database/Migrations/20160428212500_Create_test_tables.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 4dc887df1efb..93e930421d35 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -184,6 +184,7 @@ public function down(): void $this->forge->dropTable('user', true); $this->forge->dropTable('job', true); $this->forge->dropTable('misc', true); + $this->forge->dropTable('team_members', true); $this->forge->dropTable('type_test', true); $this->forge->dropTable('empty', true); $this->forge->dropTable('secondary', true); From 48658f07424e9c15bc7ef2fb4e2cf9afcd73605c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:41:24 +0200 Subject: [PATCH 6/7] fix(OCI8): revert per-component user isolation, run Oracle sequentially instead OCI8 config uses DSN (not hostname/port/database), so per-component user creation via oci_connect failed silently. Oracle Free containers also restrict CREATE USER in PDB context. Instead, exclude OCI8 from per-component DB isolation and run Oracle components sequentially (--max-jobs 1, --repeat 2) to avoid ORA-00955 table name collisions. --- .github/workflows/test-random-execution.yml | 17 ++-- tests/_support/Config/Registrar.php | 106 +++++++------------- 2 files changed, 46 insertions(+), 77 deletions(-) diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index d8730ef2846f..ee2a4f4a5440 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -212,14 +212,19 @@ jobs: args+=("--component" "${{ inputs.component }}") fi - # Add --max-jobs flag if specified (empty means auto-detect) - if [[ -n "${{ inputs.max-jobs }}" ]]; then - args+=("--max-jobs" "${{ inputs.max-jobs }}") + # OCI8 connects to a single shared schema via DSN, so components + # cannot be isolated with per-component databases like MySQLi/Postgre/SQLSRV. + # Run Oracle sequentially to avoid table name collisions (ORA-00955). + if [[ "${{ matrix.db-platform }}" == "Oracle" ]]; then + args+=("--max-jobs" "1") + args+=("--repeat" "${{ inputs.repeat || '2' }}") + else + if [[ -n "${{ inputs.max-jobs }}" ]]; then + args+=("--max-jobs" "${{ inputs.max-jobs }}") + fi + args+=("--repeat" "${{ inputs.repeat || '10' }}") fi - # Add --repeat flag (always, default is 10) - args+=("--repeat" "${{ inputs.repeat || '10' }}") - # Add --timeout flag (always, default is 300) args+=("--timeout" "${{ inputs.timeout || '300' }}") diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index 8c913534ea9e..4869617f9ad3 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -147,7 +147,7 @@ public static function Database(): array $dbParams = self::$dbConfig[$group] ?? []; - if (! empty($dbParams) && $group !== 'SQLite3') { + if (! empty($dbParams) && ! in_array($group, ['SQLite3', 'OCI8'], true)) { $componentName = ''; foreach ($_SERVER['argv'] ?? [] as $arg) { @@ -161,79 +161,43 @@ public static function Database(): array } if ($componentName !== '') { - if ($group === 'OCI8') { - $hostname = $dbParams['hostname'] ?? '127.0.0.1'; - $port = $dbParams['port'] ?? 1521; - $database = $dbParams['database'] ?? 'FREEPDB1'; - $username = $dbParams['username'] ?? 'ORACLE'; - $password = $dbParams['password'] ?? 'ORACLE'; - - $compUser = strtoupper('t_' . substr(preg_replace('/[^a-zA-Z0-9]/', '', $componentName), 0, 20)); - $tns = '//' . $hostname . ':' . $port . '/' . $database; - - try { - $conn = @oci_connect($username, $password, $tns); - if ($conn !== false) { - $stmt = @oci_parse($conn, 'SELECT USERNAME FROM ALL_USERS WHERE USERNAME = :usr'); - @oci_bind_by_name($stmt, ':usr', $compUser); - @oci_execute($stmt); - - if (@oci_fetch_array($stmt, OCI_ASSOC) === false) { - $stmt2 = @oci_parse($conn, 'CREATE USER ' . $compUser . ' IDENTIFIED BY ' . $compUser); - @oci_execute($stmt2); - $stmt3 = @oci_parse($conn, 'GRANT CONNECT, RESOURCE, DBA TO ' . $compUser); - @oci_execute($stmt3); - $stmt4 = @oci_parse($conn, 'GRANT UNLIMITED TABLESPACE TO ' . $compUser); - @oci_execute($stmt4); - } - - @oci_close($conn); - - $dbParams['username'] = $compUser; - $dbParams['password'] = $compUser; + $dbParams['database'] = 'test_' . strtolower($componentName); + + try { + if ($group === 'MySQLi') { + $conn = new mysqli( + $dbParams['hostname'], + $dbParams['username'], + $dbParams['password'], + '', + (int) $dbParams['port'], + ); + if (! $conn->connect_error) { + $conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database'])); + $conn->close(); } - } catch (Throwable) { - // Ignore error and fall back to default user - } - } else { - $dbParams['database'] = 'test_' . strtolower($componentName); - - try { - if ($group === 'MySQLi') { - $conn = new mysqli( - $dbParams['hostname'], - $dbParams['username'], - $dbParams['password'], - '', - (int) $dbParams['port'], - ); - if (! $conn->connect_error) { - $conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database'])); - $conn->close(); - } - } elseif ($group === 'Postgre') { - $dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password']; - $pdo = new PDO($dsn); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?'); - $stmt->execute([$dbParams['database']]); - if (! $stmt->fetchColumn()) { - $dbName = str_replace('"', '""', $dbParams['database']); - $pdo->exec('CREATE DATABASE "' . $dbName . '"'); - } - } elseif ($group === 'SQLSRV') { - $dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True'; - $pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?'); - $stmt->execute([$dbParams['database']]); - if (! $stmt->fetchColumn()) { - $pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8'); - } + } elseif ($group === 'Postgre') { + $dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password']; + $pdo = new PDO($dsn); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $dbName = str_replace('"', '""', $dbParams['database']); + $pdo->exec('CREATE DATABASE "' . $dbName . '"'); + } + } elseif ($group === 'SQLSRV') { + $dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True'; + $pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?'); + $stmt->execute([$dbParams['database']]); + if (! $stmt->fetchColumn()) { + $pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8'); } - } catch (Throwable) { - // Ignore any error and let the connection fail naturally } + } catch (Throwable) { + // Ignore any error and let the connection fail naturally } } } From 92d3d767bb713c1e686666651a8a0f7996bebc3b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 5 Aug 2026 22:43:40 +0200 Subject: [PATCH 7/7] ci(random-tests): exclude Oracle from random test matrix OCI8 uses a single shared schema via DSN and cannot be isolated with per-component databases like MySQLi/Postgre/SQLSRV. This causes persistent ORA-00955 collisions. Oracle tests continue to run in the standard PHPUnit CI workflow. --- .github/workflows/test-random-execution.yml | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index ee2a4f4a5440..7b7bc8b6cb26 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -75,7 +75,10 @@ jobs: - Postgre - SQLSRV - SQLite3 - - Oracle + # Oracle is excluded: OCI8 uses a single shared schema via DSN + # and cannot be isolated with per-component databases, causing + # ORA-00955 collisions when components run in parallel. + # - Oracle services: mysql: @@ -212,19 +215,14 @@ jobs: args+=("--component" "${{ inputs.component }}") fi - # OCI8 connects to a single shared schema via DSN, so components - # cannot be isolated with per-component databases like MySQLi/Postgre/SQLSRV. - # Run Oracle sequentially to avoid table name collisions (ORA-00955). - if [[ "${{ matrix.db-platform }}" == "Oracle" ]]; then - args+=("--max-jobs" "1") - args+=("--repeat" "${{ inputs.repeat || '2' }}") - else - if [[ -n "${{ inputs.max-jobs }}" ]]; then - args+=("--max-jobs" "${{ inputs.max-jobs }}") - fi - args+=("--repeat" "${{ inputs.repeat || '10' }}") + # Add --max-jobs flag if specified (empty means auto-detect) + if [[ -n "${{ inputs.max-jobs }}" ]]; then + args+=("--max-jobs" "${{ inputs.max-jobs }}") fi + # Add --repeat flag (always, default is 10) + args+=("--repeat" "${{ inputs.repeat || '10' }}") + # Add --timeout flag (always, default is 300) args+=("--timeout" "${{ inputs.timeout || '300' }}")