diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 67921d2c13f8..796b412982a9 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -349,12 +349,25 @@ protected function _fieldData(string $table): array $retval[$i]->max_length = $length; $retval[$i]->nullable = $query[$i]->NULLABLE === 'Y'; - $retval[$i]->default = $query[$i]->DATA_DEFAULT; + $retval[$i]->default = $this->normalizeDefault($query[$i]->DATA_DEFAULT); } return $retval; } + /** + * Removes trailing whitespace from default values + * returned in database column metadata queries. + */ + private function normalizeDefault(?string $default): ?string + { + if ($default === null) { + return $default; + } + + return rtrim($default); + } + /** * Returns an array of objects with index data * diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 7ef4f31901d1..53927e036f59 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -384,12 +384,42 @@ protected function _fieldData(string $table): array ); $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO'; - $retVal[$i]->default = $query[$i]->COLUMN_DEFAULT; + $retVal[$i]->default = $this->normalizeDefault($query[$i]->COLUMN_DEFAULT); } return $retVal; } + /** + * Normalizes SQL Server COLUMN_DEFAULT values. + * Removes wrapping parentheses and handles basic conversions. + */ + private function normalizeDefault(?string $default): ?string + { + if ($default === null) { + return null; + } + + $default = trim($default); + + // Remove outer parentheses (handles both single and double wrapping) + while (preg_match('/^\((.*)\)$/', $default, $matches)) { + $default = trim($matches[1]); + } + + // Handle NULL literal + if (strcasecmp($default, 'NULL') === 0) { + return null; + } + + // Handle string literals - remove quotes and unescape + if (preg_match("/^'(.*)'$/s", $default, $matches)) { + return str_replace("''", "'", $matches[1]); + } + + return $default; + } + /** * Begin Transaction */ diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index d36a0e558685..c2e1e92b0357 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1093,7 +1093,7 @@ public function testAddFields(): void 'type' => 'int', 'max_length' => 10, 'nullable' => false, - 'default' => '((0))', // Why? + 'default' => '0', ], ]; } elseif ($this->db->DBDriver === 'OCI8') { @@ -1124,7 +1124,7 @@ public function testAddFields(): void 'type' => 'NUMBER', 'max_length' => '11', 'nullable' => false, - 'default' => '0 ', // Why? + 'default' => '0', ], ]; diff --git a/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php b/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php index 47aa8d108972..c07566e187df 100644 --- a/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php +++ b/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php @@ -14,7 +14,9 @@ namespace CodeIgniter\Database\Live\SQLSRV; use CodeIgniter\Database\Live\AbstractGetFieldDataTestCase; +use CodeIgniter\Database\SQLSRV\Connection; use Config\Database; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -68,7 +70,7 @@ public function testGetFieldDataDefault(): void 'type' => 'int', 'max_length' => 10, 'nullable' => false, - 'default' => '((0))', // int 0 + 'default' => '0', // 'primary_key' => 0, ], (object) [ @@ -76,7 +78,7 @@ public function testGetFieldDataDefault(): void 'type' => 'varchar', 'max_length' => 64, 'nullable' => true, - 'default' => '(NULL)', // NULL value + 'default' => null, // 'primary_key' => 0, ], (object) [ @@ -84,7 +86,7 @@ public function testGetFieldDataDefault(): void 'type' => 'varchar', 'max_length' => 64, 'nullable' => false, - 'default' => "('null')", // string "null" + 'default' => 'null', // string "null" // 'primary_key' => 0, ], (object) [ @@ -92,7 +94,7 @@ public function testGetFieldDataDefault(): void 'type' => 'varchar', 'max_length' => 64, 'nullable' => false, - 'default' => "('abc')", // string "abc" + 'default' => 'abc', // 'primary_key' => 0, ], ]; @@ -235,4 +237,58 @@ public function testGetFieldDataType(): void ]; $this->assertSameFieldData($expected, $fields); } + + #[DataProvider('provideNormalizeDefault')] + public function testNormalizeDefault(?string $input, ?string $expected): void + { + $this->assertInstanceOf(Connection::class, $this->db); + + $normalizeDefault = self::getPrivateMethodInvoker($this->db, 'normalizeDefault'); + $this->assertSame($expected, $normalizeDefault($input)); + } + + /** + * @return iterable + */ + public static function provideNormalizeDefault(): iterable + { + return [ + // Null cases + 'null input' => [null, null], + 'NULL literal wrapped in parentheses' => ['(NULL)', null], + 'null literal lowercase' => ['(null)', null], + 'null literal mixed case' => ['(Null)', null], + 'null literal random case' => ['(nULL)', null], + 'null string' => ["('null')", 'null'], + + // String literal cases + 'simple string' => ["('hello')", 'hello'], + 'empty string' => ['(())', ''], + 'string with space' => ["('hello world')", 'hello world'], + 'empty string literal' => ["('')", ''], + 'string with escaped quote' => ["('can''t')", "can't"], + 'string with multiple escaped quotes' => ["('it''s a ''test''')", "it's a 'test'"], + 'concatenated multiline expression' => ["('line1'+char(10)+'line2')", "line1'+char(10)+'line2"], + + // Numeric cases + 'zero with double parentheses' => ['((0))', '0'], + 'positive integer with double parentheses' => ['((123))', '123'], + 'negative integer with double parentheses' => ['((-456))', '-456'], + 'float with double parentheses' => ['((3.14))', '3.14'], + + // Function/expression cases + 'function call' => ['(getdate())', 'getdate()'], + 'newid function' => ['(newid())', 'newid()'], + 'user_name function' => ['(user_name())', 'user_name()'], + 'current_timestamp' => ['(current_timestamp)', 'current_timestamp'], + 'mathematical expression' => ['((1+1))', '1+1'], + 'multiplication expression' => ['((100*2))', '100*2'], + + // Edge cases + 'multiple nested parentheses' => ["((('nested')))", 'nested'], + 'value without parentheses' => ['plain_value', 'plain_value'], + 'value with parentheses' => ['( plain_value )', 'plain_value'], + 'function with parameters' => ['(complex_func(1, 2))', 'complex_func(1, 2)'], + ]; + } } diff --git a/user_guide_src/source/changelogs/v4.6.4.rst b/user_guide_src/source/changelogs/v4.6.4.rst index 8ef80eabbfcf..705901e231af 100644 --- a/user_guide_src/source/changelogs/v4.6.4.rst +++ b/user_guide_src/source/changelogs/v4.6.4.rst @@ -31,6 +31,7 @@ Bugs Fixed ********** - **Database:** Fixed a bug in ``Database::connect()`` which was causing to store non-shared connection instances in shared cache. +- **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. See the repo's `CHANGELOG.md `_