Skip to content

Commit 78143f3

Browse files
committed
fix(Database): fix random-order test execution issues and state leakage under PostgreSQL, MySQL, and OCI8
1 parent 0d1d224 commit 78143f3

17 files changed

Lines changed: 248 additions & 64 deletions

.github/scripts/random-tests-config.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Config
1818
Cookie
1919
# DataCaster
2020
# DataConverter
21-
# Database
21+
Database
2222
# Debug
2323
Email
2424
# Encryption

.github/workflows/test-random-execution.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ jobs:
177177
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
178178
with:
179179
php-version: ${{ matrix.php-version }}
180-
extensions: gd, curl, iconv, json, mbstring, openssl, sodium
180+
extensions: gd, curl, iconv, json, mbstring, openssl, sodium, mysqli, oci8, pgsql, sqlsrv, sqlite3
181181
ini-values: opcache.enable_cli=0
182182
coverage: none
183183

@@ -212,16 +212,24 @@ jobs:
212212
args+=("--component" "${{ inputs.component }}")
213213
fi
214214
215-
# Add --max-jobs flag if specified (empty means auto-detect)
216-
if [[ -n "${{ inputs.max-jobs }}" ]]; then
217-
args+=("--max-jobs" "${{ inputs.max-jobs }}")
215+
# OCI8 connects to a single shared schema (FREEPDB1) via DSN, so
216+
# components cannot be isolated with per-component databases like
217+
# MySQLi/Postgre/SQLSRV. Running components in parallel makes
218+
# e.g. Commands' migrate:rollback drop tables that Database tests
219+
# rely on (ORA-00942/04043/08103). Run Oracle sequentially with
220+
# default repeat 2 to avoid schema collisions and timeouts.
221+
if [[ "${{ matrix.db-platform }}" == "Oracle" ]]; then
222+
args+=("--max-jobs" "1")
223+
args+=("--repeat" "${{ inputs.repeat || '2' }}")
224+
else
225+
if [[ -n "${{ inputs.max-jobs }}" ]]; then
226+
args+=("--max-jobs" "${{ inputs.max-jobs }}")
227+
fi
228+
args+=("--repeat" "${{ inputs.repeat || '10' }}")
218229
fi
219230
220-
# Add --repeat flag (always, default is 10)
221-
args+=("--repeat" "${{ inputs.repeat || '10' }}")
222-
223-
# Add --timeout flag (always, default is 300)
224-
args+=("--timeout" "${{ inputs.timeout || '300' }}")
231+
# Add --timeout flag (always, default is 600)
232+
args+=("--timeout" "${{ inputs.timeout || '600' }}")
225233
226234
.github/scripts/run-random-tests.sh "${args[@]}"
227235
env:

system/Database/OCI8/Connection.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use ErrorException;
2121
use stdClass;
2222

23+
defined('OCI_COMMIT_ON_SUCCESS') || define('OCI_COMMIT_ON_SUCCESS', 32);
24+
2325
/**
2426
* Connection for OCI8
2527
*
@@ -150,6 +152,17 @@ public function connect(bool $persistent = false)
150152
: $func($this->username, $this->password, $this->DSN, $this->charset);
151153
}
152154

155+
public function initialize()
156+
{
157+
parent::initialize();
158+
159+
if ($this->connID) {
160+
$this->simpleQuery("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
161+
$this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
162+
$this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS'");
163+
}
164+
}
165+
153166
/**
154167
* Close the database connection.
155168
*
@@ -288,11 +301,11 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n
288301
$sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"';
289302

290303
if ($tableName !== null) {
291-
return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape($tableName);
304+
return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape(strtoupper($tableName));
292305
}
293306

294307
if ($prefixLimit && $this->DBPrefix !== '') {
295-
return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' "
308+
return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString(strtoupper($this->DBPrefix)) . "%' "
296309
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
297310
}
298311

@@ -397,7 +410,7 @@ protected function _indexData(string $table): array
397410
$sql = 'SELECT AIC.INDEX_NAME, UC.CONSTRAINT_TYPE, AIC.COLUMN_NAME '
398411
. ' FROM ALL_IND_COLUMNS AIC '
399412
. ' LEFT JOIN USER_CONSTRAINTS UC ON AIC.INDEX_NAME = UC.CONSTRAINT_NAME AND AIC.TABLE_NAME = UC.TABLE_NAME '
400-
. 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtolower($table)) . ' '
413+
. 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtoupper($table)) . ' '
401414
. 'AND AIC.TABLE_OWNER = ' . $this->escape(strtoupper($owner)) . ' '
402415
. ' ORDER BY UC.CONSTRAINT_TYPE, AIC.COLUMN_POSITION';
403416

@@ -422,7 +435,7 @@ protected function _indexData(string $table): array
422435
$retVal[$row->INDEX_NAME] = new stdClass();
423436
$retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME;
424437
$retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME];
425-
$retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX';
438+
$retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE ?? ''] ?? 'INDEX';
426439
}
427440

428441
return $retVal;

system/Database/Postgre/Connection.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PgSql\Result as PgSqlResult;
2323
use stdClass;
2424
use Stringable;
25+
use Throwable;
2526

2627
/**
2728
* Connection for Postgre
@@ -149,15 +150,30 @@ private function convertDSN()
149150
*/
150151
protected function _close()
151152
{
152-
pg_close($this->connID);
153+
if ($this->connID !== false) {
154+
try {
155+
pg_close($this->connID);
156+
} catch (Throwable) {
157+
} finally {
158+
$this->connID = false;
159+
}
160+
}
153161
}
154162

155163
/**
156164
* Ping the database connection.
157165
*/
158166
protected function _ping(): bool
159167
{
160-
return pg_ping($this->connID);
168+
if ($this->connID === false) {
169+
return false;
170+
}
171+
172+
try {
173+
return pg_ping($this->connID);
174+
} catch (Throwable) {
175+
return false;
176+
}
161177
}
162178

163179
/**

tests/_support/Config/Registrar.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
namespace Tests\Support\Config;
1515

16+
use mysqli;
17+
use PDO;
18+
use Throwable;
19+
1620
/**
1721
* Class Registrar
1822
*
@@ -137,7 +141,68 @@ public static function Database(): array
137141
// so that we can test against multiple databases.
138142
$group = env('DB', 'SQLite3');
139143

140-
$config['tests'] = self::$dbConfig[$group] ?? [];
144+
if ($group === 'Oracle') {
145+
$group = 'OCI8';
146+
}
147+
148+
$dbParams = self::$dbConfig[$group] ?? [];
149+
150+
if (! empty($dbParams) && ! in_array($group, ['SQLite3', 'OCI8'], true)) {
151+
$componentName = '';
152+
153+
foreach ($_SERVER['argv'] ?? [] as $arg) {
154+
if (str_contains($arg, 'tests/system/')) {
155+
$parts = explode('tests/system/', $arg);
156+
if (isset($parts[1])) {
157+
$componentName = explode('/', $parts[1])[0];
158+
break;
159+
}
160+
}
161+
}
162+
163+
if ($componentName !== '') {
164+
$dbParams['database'] = 'test_' . strtolower($componentName);
165+
166+
try {
167+
if ($group === 'MySQLi') {
168+
$conn = new mysqli(
169+
$dbParams['hostname'],
170+
$dbParams['username'],
171+
$dbParams['password'],
172+
'',
173+
(int) $dbParams['port'],
174+
);
175+
if (! $conn->connect_error) {
176+
$conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database']));
177+
$conn->close();
178+
}
179+
} elseif ($group === 'Postgre') {
180+
$dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password'];
181+
$pdo = new PDO($dsn);
182+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
183+
$stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?');
184+
$stmt->execute([$dbParams['database']]);
185+
if (! $stmt->fetchColumn()) {
186+
$dbName = str_replace('"', '""', $dbParams['database']);
187+
$pdo->exec('CREATE DATABASE "' . $dbName . '"');
188+
}
189+
} elseif ($group === 'SQLSRV') {
190+
$dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True';
191+
$pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']);
192+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
193+
$stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?');
194+
$stmt->execute([$dbParams['database']]);
195+
if (! $stmt->fetchColumn()) {
196+
$pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . '] COLLATE Latin1_General_100_CS_AS_SC_UTF8');
197+
}
198+
}
199+
} catch (Throwable) {
200+
// Ignore any error and let the connection fail naturally
201+
}
202+
}
203+
}
204+
205+
$config['tests'] = $dbParams;
141206

142207
return $config;
143208
}

tests/_support/Database/Migrations/20160428212500_Create_test_tables.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Tests\Support\Database\Migrations;
1515

1616
use CodeIgniter\Database\Migration;
17+
use Throwable;
1718

1819
class Migration_Create_test_tables extends Migration
1920
{
@@ -196,9 +197,25 @@ public function down(): void
196197
}
197198

198199
if ($this->db->DBDriver === 'OCI8') {
199-
$this->db->query('DROP PROCEDURE one');
200-
$this->db->query('DROP PROCEDURE plus');
201-
$this->db->query('DROP PACKAGE BODY calculator');
200+
try {
201+
$this->db->query('DROP PROCEDURE one');
202+
} catch (Throwable) {
203+
}
204+
205+
try {
206+
$this->db->query('DROP PROCEDURE plus');
207+
} catch (Throwable) {
208+
}
209+
210+
try {
211+
$this->db->query('DROP PACKAGE BODY calculator');
212+
} catch (Throwable) {
213+
}
214+
215+
try {
216+
$this->db->query('DROP PACKAGE calculator');
217+
} catch (Throwable) {
218+
}
202219
}
203220
}
204221
}

tests/system/Database/Live/ConnectTest.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,28 @@ protected function setUp(): void
4646
$this->group2['DBDriver'] = 'Postgre';
4747
}
4848

49+
protected function tearDown(): void
50+
{
51+
parent::tearDown();
52+
$this->setPrivateProperty(Database::class, 'instances', []);
53+
}
54+
4955
public function testConnectWithMultipleCustomGroups(): void
5056
{
57+
$this->group1['DBPrefix'] = uniqid('g1_', true);
58+
$this->group2['DBPrefix'] = uniqid('g2_', true);
59+
5160
// We should have our test database connection already.
52-
$instances = $this->getPrivateProperty(Database::class, 'instances');
53-
$this->assertCount(1, $instances);
61+
$instances = $this->getPrivateProperty(Database::class, 'instances');
62+
$initialCount = count($instances);
5463

5564
$db1 = Database::connect($this->group1);
5665
$db2 = Database::connect($this->group2);
5766

5867
$this->assertNotSame($db1, $db2);
5968

6069
$instances = $this->getPrivateProperty(Database::class, 'instances');
61-
$this->assertCount(3, $instances);
70+
$this->assertCount($initialCount + 2, $instances);
6271
}
6372

6473
public function testConnectReturnsProvidedConnection(): void

tests/system/Database/Live/ExecuteLogMessageFormatTest.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi
4747
$db->query($sql, [3, 'live', 'Rick']);
4848

4949
$pattern = match ($db->DBDriver) {
50-
'MySQLi' => '/Table \'test\.some_table\' doesn\'t exist/',
50+
'MySQLi' => '/Table \'' . preg_quote($db->database, '/') . '\.some_table\' doesn\'t exist/',
5151
'Postgre' => '/pg_query\(\): Query failed: ERROR: relation "some_table" does not exist/',
5252
'SQLite3' => '/Unable to prepare statement:\s(\d+,\s)?no such table: some_table/',
5353
'OCI8' => '/oci_execute\(\): ORA-00942: table or view "ORACLE"\."SOME_TABLE" does not exist/',
@@ -60,11 +60,18 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi
6060

6161
if ($db->DBDriver === 'Postgre') {
6262
$messageFromLogs = array_slice($messageFromLogs, 2);
63-
} elseif ($db->DBDriver === 'OCI8') {
64-
$messageFromLogs = array_slice($messageFromLogs, 1);
6563
}
6664

67-
$this->assertMatchesRegularExpression('/^in \S+ on line \d+\.$/', array_shift($messageFromLogs));
65+
$inLine = null;
66+
67+
while (($line = array_shift($messageFromLogs)) !== null) {
68+
if (preg_match('/^in \S+ on line \d+\.$/', $line)) {
69+
$inLine = $line;
70+
break;
71+
}
72+
}
73+
74+
$this->assertNotNull($inLine, 'Could not find "in ... on line ..." in log message');
6875

6976
foreach ($messageFromLogs as $line) {
7077
$this->assertMatchesRegularExpression('/^\s*\d* .+(?:\(\d+\))?: \S+(?:(?:\->|::)\S+)?\(.*\)$/', $line);

0 commit comments

Comments
 (0)