Skip to content

Commit e6dcf5b

Browse files
committed
Merge branch 'develop' into 4.7
2 parents 500d4ad + 031d5c6 commit e6dcf5b

File tree

12 files changed

+180
-16
lines changed

12 files changed

+180
-16
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"phpunit/phpcov": "^9.0.2 || ^10.0",
2929
"phpunit/phpunit": "^10.5.16 || ^11.2",
3030
"predis/predis": "^3.0",
31-
"rector/rector": "2.1.2",
31+
"rector/rector": "2.1.4",
3232
"shipmonk/phpstan-baseline-per-identifier": "^2.0"
3333
},
3434
"replace": {

system/Database/Config.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ public static function connect($group = null, bool $getShared = true)
8181

8282
$connection = static::$factory->load($config, $group);
8383

84-
static::$instances[$group] = $connection;
84+
if ($getShared) {
85+
static::$instances[$group] = $connection;
86+
}
8587

8688
return $connection;
8789
}

system/Database/OCI8/Connection.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,25 @@ protected function _fieldData(string $table): array
349349
$retval[$i]->max_length = $length;
350350

351351
$retval[$i]->nullable = $query[$i]->NULLABLE === 'Y';
352-
$retval[$i]->default = $query[$i]->DATA_DEFAULT;
352+
$retval[$i]->default = $this->normalizeDefault($query[$i]->DATA_DEFAULT);
353353
}
354354

355355
return $retval;
356356
}
357357

358+
/**
359+
* Removes trailing whitespace from default values
360+
* returned in database column metadata queries.
361+
*/
362+
private function normalizeDefault(?string $default): ?string
363+
{
364+
if ($default === null) {
365+
return $default;
366+
}
367+
368+
return rtrim($default);
369+
}
370+
358371
/**
359372
* Returns an array of objects with index data
360373
*

system/Database/Postgre/Forge.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ protected function _alterTable(string $alterType, string $table, $processedField
109109

110110
if (! empty($field['default'])) {
111111
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
112-
. " SET DEFAULT {$field['default']}";
112+
. " SET {$field['default']}";
113113
}
114114

115115
$nullable = true; // Nullable by default.

system/Database/SQLSRV/Connection.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,42 @@ protected function _fieldData(string $table): array
384384
);
385385

386386
$retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
387-
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;
387+
$retVal[$i]->default = $this->normalizeDefault($query[$i]->COLUMN_DEFAULT);
388388
}
389389

390390
return $retVal;
391391
}
392392

393+
/**
394+
* Normalizes SQL Server COLUMN_DEFAULT values.
395+
* Removes wrapping parentheses and handles basic conversions.
396+
*/
397+
private function normalizeDefault(?string $default): ?string
398+
{
399+
if ($default === null) {
400+
return null;
401+
}
402+
403+
$default = trim($default);
404+
405+
// Remove outer parentheses (handles both single and double wrapping)
406+
while (preg_match('/^\((.*)\)$/', $default, $matches)) {
407+
$default = trim($matches[1]);
408+
}
409+
410+
// Handle NULL literal
411+
if (strcasecmp($default, 'NULL') === 0) {
412+
return null;
413+
}
414+
415+
// Handle string literals - remove quotes and unescape
416+
if (preg_match("/^'(.*)'$/s", $default, $matches)) {
417+
return str_replace("''", "'", $matches[1]);
418+
}
419+
420+
return $default;
421+
}
422+
393423
/**
394424
* Begin Transaction
395425
*/

system/Database/SQLSRV/Forge.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,27 @@ protected function _alterTable(string $alterType, string $table, $processedField
253253
}
254254

255255
if (! empty($field['default'])) {
256-
$sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($field['name']) . '_def'
257-
. " DEFAULT {$field['default']} FOR " . $this->db->escapeIdentifiers($field['name']);
256+
$fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
257+
$colName = $field['name']; // bare, for sys.columns lookup
258+
259+
// find the existing default constraint name for this column
260+
$findSql = <<<SQL
261+
SELECT dc.name AS constraint_name
262+
FROM sys.default_constraints dc
263+
JOIN sys.columns c
264+
ON dc.parent_object_id = c.object_id
265+
AND dc.parent_column_id = c.column_id
266+
WHERE dc.parent_object_id = OBJECT_ID(N'{$fullTable}')
267+
AND c.name = N'{$colName}';
268+
SQL;
269+
270+
$toDrop = $this->db->query($findSql)->getRowArray();
271+
if (isset($toDrop['constraint_name']) && $toDrop['constraint_name'] !== '') {
272+
$sqls[] = $sql . ' DROP CONSTRAINT ' . $this->db->escapeIdentifiers($toDrop['constraint_name']);
273+
}
274+
275+
$sqls[] = $sql . ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($field['name'] . '_def')
276+
. "{$field['default']} FOR " . $this->db->escapeIdentifiers($field['name']);
258277
}
259278

260279
$nullable = true; // Nullable by default.

tests/system/Database/Live/ConnectTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,22 @@ public function testConnectWithFailover(): void
115115

116116
$this->assertGreaterThanOrEqual(0, count($db1->listTables()));
117117
}
118+
119+
public function testNonSharedInstanceDoesNotAffectSharedInstances(): void
120+
{
121+
$firstSharedDb = Database::connect('tests');
122+
$originalDebugValue = (bool) self::getPrivateProperty($firstSharedDb, 'DBDebug');
123+
124+
$nonSharedDb = Database::connect('tests', false);
125+
self::setPrivateProperty($nonSharedDb, 'DBDebug', ! $originalDebugValue);
126+
127+
$secondSharedDb = Database::connect('tests');
128+
129+
$this->assertSame($firstSharedDb, $secondSharedDb);
130+
$this->assertNotSame($firstSharedDb, $nonSharedDb);
131+
132+
$this->assertSame($originalDebugValue, self::getPrivateProperty($firstSharedDb, 'DBDebug'));
133+
$this->assertSame($originalDebugValue, self::getPrivateProperty($secondSharedDb, 'DBDebug'));
134+
$this->assertSame(! $originalDebugValue, self::getPrivateProperty($nonSharedDb, 'DBDebug'));
135+
}
118136
}

tests/system/Database/Live/ForgeTest.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,7 @@ public function testAddFields(): void
10931093
'type' => 'int',
10941094
'max_length' => 10,
10951095
'nullable' => false,
1096-
'default' => '((0))', // Why?
1096+
'default' => '0',
10971097
],
10981098
];
10991099
} elseif ($this->db->DBDriver === 'OCI8') {
@@ -1124,7 +1124,7 @@ public function testAddFields(): void
11241124
'type' => 'NUMBER',
11251125
'max_length' => '11',
11261126
'nullable' => false,
1127-
'default' => '0 ', // Why?
1127+
'default' => '0',
11281128
],
11291129
];
11301130

@@ -1818,6 +1818,28 @@ public function testProcessIndexesWithForeignKeyOnly(): void
18181818
$this->forge->dropTable('user2', true);
18191819
}
18201820

1821+
public function testChangeDefaultFieldValueWithModifyColumn(): void
1822+
{
1823+
$this->forge->addField([
1824+
'delivery_sum' => ['type' => 'int', 'constraint' => 6, 'default' => 10],
1825+
'delivery_free_sum' => ['type' => 'int', 'constraint' => 6, 'default' => 10],
1826+
])->addKey('id', true)->createTable('test_stores', true);
1827+
1828+
$this->forge->modifyColumn('test_stores', [
1829+
'delivery_sum' => ['type' => 'int', 'constraint' => 6, 'default' => 100],
1830+
'delivery_free_sum' => ['type' => 'int', 'constraint' => 6, 'default' => 1000],
1831+
]);
1832+
1833+
$expected = ['delivery_sum' => '100', 'delivery_free_sum' => '1000'];
1834+
1835+
$fields = $this->db->getFieldData('test_stores');
1836+
$results = array_column($fields, 'default', 'name');
1837+
1838+
$this->assertSame($expected, $results);
1839+
1840+
$this->forge->dropTable('test_stores', true);
1841+
}
1842+
18211843
private function createUser2TableWithKeys(): void
18221844
{
18231845
$fields = [

tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace CodeIgniter\Database\Live\SQLSRV;
1515

1616
use CodeIgniter\Database\Live\AbstractGetFieldDataTestCase;
17+
use CodeIgniter\Database\SQLSRV\Connection;
1718
use Config\Database;
19+
use PHPUnit\Framework\Attributes\DataProvider;
1820
use PHPUnit\Framework\Attributes\Group;
1921

2022
/**
@@ -68,31 +70,31 @@ public function testGetFieldDataDefault(): void
6870
'type' => 'int',
6971
'max_length' => 10,
7072
'nullable' => false,
71-
'default' => '((0))', // int 0
73+
'default' => '0',
7274
// 'primary_key' => 0,
7375
],
7476
(object) [
7577
'name' => 'text_default_null',
7678
'type' => 'varchar',
7779
'max_length' => 64,
7880
'nullable' => true,
79-
'default' => '(NULL)', // NULL value
81+
'default' => null,
8082
// 'primary_key' => 0,
8183
],
8284
(object) [
8385
'name' => 'text_default_text_null',
8486
'type' => 'varchar',
8587
'max_length' => 64,
8688
'nullable' => false,
87-
'default' => "('null')", // string "null"
89+
'default' => 'null', // string "null"
8890
// 'primary_key' => 0,
8991
],
9092
(object) [
9193
'name' => 'text_default_abc',
9294
'type' => 'varchar',
9395
'max_length' => 64,
9496
'nullable' => false,
95-
'default' => "('abc')", // string "abc"
97+
'default' => 'abc',
9698
// 'primary_key' => 0,
9799
],
98100
];
@@ -235,4 +237,58 @@ public function testGetFieldDataType(): void
235237
];
236238
$this->assertSameFieldData($expected, $fields);
237239
}
240+
241+
#[DataProvider('provideNormalizeDefault')]
242+
public function testNormalizeDefault(?string $input, ?string $expected): void
243+
{
244+
$this->assertInstanceOf(Connection::class, $this->db);
245+
246+
$normalizeDefault = self::getPrivateMethodInvoker($this->db, 'normalizeDefault');
247+
$this->assertSame($expected, $normalizeDefault($input));
248+
}
249+
250+
/**
251+
* @return iterable<string, array{string|null, string|null}>
252+
*/
253+
public static function provideNormalizeDefault(): iterable
254+
{
255+
return [
256+
// Null cases
257+
'null input' => [null, null],
258+
'NULL literal wrapped in parentheses' => ['(NULL)', null],
259+
'null literal lowercase' => ['(null)', null],
260+
'null literal mixed case' => ['(Null)', null],
261+
'null literal random case' => ['(nULL)', null],
262+
'null string' => ["('null')", 'null'],
263+
264+
// String literal cases
265+
'simple string' => ["('hello')", 'hello'],
266+
'empty string' => ['(())', ''],
267+
'string with space' => ["('hello world')", 'hello world'],
268+
'empty string literal' => ["('')", ''],
269+
'string with escaped quote' => ["('can''t')", "can't"],
270+
'string with multiple escaped quotes' => ["('it''s a ''test''')", "it's a 'test'"],
271+
'concatenated multiline expression' => ["('line1'+char(10)+'line2')", "line1'+char(10)+'line2"],
272+
273+
// Numeric cases
274+
'zero with double parentheses' => ['((0))', '0'],
275+
'positive integer with double parentheses' => ['((123))', '123'],
276+
'negative integer with double parentheses' => ['((-456))', '-456'],
277+
'float with double parentheses' => ['((3.14))', '3.14'],
278+
279+
// Function/expression cases
280+
'function call' => ['(getdate())', 'getdate()'],
281+
'newid function' => ['(newid())', 'newid()'],
282+
'user_name function' => ['(user_name())', 'user_name()'],
283+
'current_timestamp' => ['(current_timestamp)', 'current_timestamp'],
284+
'mathematical expression' => ['((1+1))', '1+1'],
285+
'multiplication expression' => ['((100*2))', '100*2'],
286+
287+
// Edge cases
288+
'multiple nested parentheses' => ["((('nested')))", 'nested'],
289+
'value without parentheses' => ['plain_value', 'plain_value'],
290+
'value with parentheses' => ['( plain_value )', 'plain_value'],
291+
'function with parameters' => ['(complex_func(1, 2))', 'complex_func(1, 2)'],
292+
];
293+
}
238294
}

user_guide_src/source/changelogs/v4.6.4.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Deprecations
3030
Bugs Fixed
3131
**********
3232

33+
- **Database:** Fixed a bug in ``Database::connect()`` which was causing to store non-shared connection instances in shared cache.
34+
- **Database:** Fixed a bug in ``Connection::getFieldData()`` for ``SQLSRV`` and ``OCI8`` where extra characters were returned in column default values (specific to those handlers), instead of following the convention used by other drivers.
35+
- **Forge:** Fixed a bug in ``Postgre`` and ``SQLSRV`` where changing a column's default value using ``Forge::modifyColumn()`` method produced incorrect SQL syntax.
36+
3337
See the repo's
3438
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
3539
for a complete list of bugs fixed.

0 commit comments

Comments
 (0)