Skip to content

Commit 33d7d2a

Browse files
authored
Merge pull request #8664 from kenjis/feat-database-name-with-dot
feat: support database name with dots
2 parents 0e6bd76 + 6a552e0 commit 33d7d2a

File tree

13 files changed

+206
-31
lines changed

13 files changed

+206
-31
lines changed

phpstan-baseline.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8981,11 +8981,6 @@
89818981
'count' => 1,
89828982
'path' => __DIR__ . '/system/Test/Mock/MockConnection.php',
89838983
];
8984-
$ignoreErrors[] = [
8985-
'message' => '#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:\\$returnValues has no type specified\\.$#',
8986-
'count' => 1,
8987-
'path' => __DIR__ . '/system/Test/Mock/MockConnection.php',
8988-
];
89898984
$ignoreErrors[] = [
89908985
'message' => '#^Return type \\(array\\{code\\: int\\|string\\|null, message\\: string\\|null\\}\\) of method CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:error\\(\\) should be covariant with return type \\(array\\<string, int\\|string\\>\\) of method CodeIgniter\\\\Database\\\\ConnectionInterface\\<object\\|resource,object\\|resource\\>\\:\\:error\\(\\)$#',
89918986
'count' => 1,
@@ -12676,11 +12671,6 @@
1267612671
'count' => 1,
1267712672
'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php',
1267812673
];
12679-
$ignoreErrors[] = [
12680-
'message' => '#^Property class@anonymous/tests/system/Database/BaseConnectionTest\\.php\\:121\\:\\:\\$returnValues has no type specified\\.$#',
12681-
'count' => 1,
12682-
'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php',
12683-
];
1268412674
$ignoreErrors[] = [
1268512675
'message' => '#^Method CodeIgniter\\\\Database\\\\BaseQueryTest\\:\\:provideHighlightQueryKeywords\\(\\) return type has no value type specified in iterable type iterable\\.$#',
1268612676
'count' => 1,

system/Database/BaseConnection.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,24 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti
11881188
return $item . $alias;
11891189
}
11901190

1191+
/**
1192+
* Escape the SQL Identifier
1193+
*
1194+
* This function escapes single identifier.
1195+
*
1196+
* @param non-empty-string $item
1197+
*/
1198+
public function escapeIdentifier(string $item): string
1199+
{
1200+
return $this->escapeChar
1201+
. str_replace(
1202+
$this->escapeChar,
1203+
$this->escapeChar . $this->escapeChar,
1204+
$item
1205+
)
1206+
. $this->escapeChar;
1207+
}
1208+
11911209
/**
11921210
* Escape the SQL Identifiers
11931211
*

system/Database/Forge.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,14 @@ public function createDatabase(string $dbName, bool $ifNotExists = false): bool
229229
}
230230

231231
try {
232-
if (! $this->db->query(sprintf($ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr, $dbName, $this->db->charset, $this->db->DBCollat))) {
232+
if (! $this->db->query(
233+
sprintf(
234+
$ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr,
235+
$this->db->escapeIdentifier($dbName),
236+
$this->db->charset,
237+
$this->db->DBCollat
238+
)
239+
)) {
233240
// @codeCoverageIgnoreStart
234241
if ($this->db->DBDebug) {
235242
throw new DatabaseException('Unable to create the specified database.');
@@ -286,7 +293,9 @@ public function dropDatabase(string $dbName): bool
286293
return false;
287294
}
288295

289-
if (! $this->db->query(sprintf($this->dropDatabaseStr, $dbName))) {
296+
if (! $this->db->query(
297+
sprintf($this->dropDatabaseStr, $this->db->escapeIdentifier($dbName))
298+
)) {
290299
if ($this->db->DBDebug) {
291300
throw new DatabaseException('Unable to drop the specified database.');
292301
}
@@ -295,7 +304,11 @@ public function dropDatabase(string $dbName): bool
295304
}
296305

297306
if (! empty($this->db->dataCache['db_names'])) {
298-
$key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
307+
$key = array_search(
308+
strtolower($dbName),
309+
array_map('strtolower', $this->db->dataCache['db_names']),
310+
true
311+
);
299312
if ($key !== false) {
300313
unset($this->db->dataCache['db_names'][$key]);
301314
}

system/Database/MySQLi/Connection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ public function escapeLikeStringDirect($str)
389389
*/
390390
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
391391
{
392-
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database);
392+
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
393393

394394
if ($tableName !== null) {
395395
return $sql . ' LIKE ' . $this->escape($tableName);

system/Database/SQLSRV/Forge.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace CodeIgniter\Database\SQLSRV;
1515

1616
use CodeIgniter\Database\BaseConnection;
17+
use CodeIgniter\Database\Exceptions\DatabaseException;
1718
use CodeIgniter\Database\Forge as BaseForge;
19+
use Throwable;
1820

1921
/**
2022
* Forge for SQLSRV
@@ -42,7 +44,7 @@ class Forge extends BaseForge
4244
*
4345
* @var string
4446
*/
45-
protected $createDatabaseIfStr = "DECLARE @DBName VARCHAR(255) = '%s'\nDECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE ' + @DBName\nEXEC( @SQL )";
47+
protected $createDatabaseIfStr = "DECLARE @DBName VARCHAR(255) = '%s'\nDECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE %s'\nEXEC( @SQL )";
4648

4749
/**
4850
* CREATE DATABASE IF statement
@@ -119,6 +121,53 @@ public function __construct(BaseConnection $db)
119121
$this->dropIndexStr = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s';
120122
}
121123

124+
/**
125+
* Create database
126+
*
127+
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
128+
*
129+
* @throws DatabaseException
130+
*/
131+
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
132+
{
133+
if ($ifNotExists) {
134+
$sql = sprintf(
135+
$this->createDatabaseIfStr,
136+
$dbName,
137+
$this->db->escapeIdentifier($dbName)
138+
);
139+
} else {
140+
$sql = sprintf(
141+
$this->createDatabaseStr,
142+
$this->db->escapeIdentifier($dbName)
143+
);
144+
}
145+
146+
try {
147+
if (! $this->db->query($sql)) {
148+
// @codeCoverageIgnoreStart
149+
if ($this->db->DBDebug) {
150+
throw new DatabaseException('Unable to create the specified database.');
151+
}
152+
153+
return false;
154+
// @codeCoverageIgnoreEnd
155+
}
156+
157+
if (isset($this->db->dataCache['db_names'])) {
158+
$this->db->dataCache['db_names'][] = $dbName;
159+
}
160+
161+
return true;
162+
} catch (Throwable $e) {
163+
if ($this->db->DBDebug) {
164+
throw new DatabaseException('Unable to create the specified database.', 0, $e);
165+
}
166+
167+
return false; // @codeCoverageIgnore
168+
}
169+
}
170+
122171
/**
123172
* CREATE TABLE attributes
124173
*/

system/Test/Mock/MockConnection.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
*/
2424
class MockConnection extends BaseConnection
2525
{
26+
/**
27+
* @var array{connect?: mixed, execute?: bool|object}
28+
*/
2629
protected $returnValues = [];
2730

2831
/**
@@ -84,7 +87,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s
8487
$query->setDuration($startTime);
8588

8689
// resultID is not false, so it must be successful
87-
if ($query->isWriteType()) {
90+
if ($query->isWriteType($sql)) {
8891
return true;
8992
}
9093

tests/system/Database/BaseConnectionTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,54 @@ public static function provideProtectIdentifiers(): iterable
283283
],
284284
];
285285
}
286+
287+
/**
288+
* These tests are intended to confirm the current behavior.
289+
*
290+
* @dataProvider provideEscapeIdentifiers
291+
*/
292+
public function testEscapeIdentifiers(string $item, string $expected): void
293+
{
294+
$db = new MockConnection($this->options);
295+
296+
$return = $db->escapeIdentifiers($item);
297+
298+
$this->assertSame($expected, $return);
299+
}
300+
301+
/**
302+
* @return array<string, list<string>>
303+
*/
304+
public static function provideEscapeIdentifiers(): iterable
305+
{
306+
yield from [
307+
// $item, $expected
308+
'simple' => ['test', '"test"'],
309+
'with dots' => ['com.sitedb.web', '"com"."sitedb"."web"'],
310+
];
311+
}
312+
313+
/**
314+
* @dataProvider provideEscapeIdentifier
315+
*/
316+
public function testEscapeIdentifier(string $item, string $expected): void
317+
{
318+
$db = new MockConnection($this->options);
319+
320+
$return = $db->escapeIdentifier($item);
321+
322+
$this->assertSame($expected, $return);
323+
}
324+
325+
/**
326+
* @return array<string, list<string>>
327+
*/
328+
public static function provideEscapeIdentifier(): iterable
329+
{
330+
yield from [
331+
// $item, $expected
332+
'simple' => ['test', '"test"'],
333+
'with dots' => ['com.sitedb.web', '"com.sitedb.web"'],
334+
];
335+
}
286336
}

tests/system/Database/Live/ForgeTest.php

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,40 +54,89 @@ public function testCreateDatabase(): void
5454
if ($this->db->DBDriver === 'OCI8') {
5555
$this->markTestSkipped('OCI8 does not support create database.');
5656
}
57+
5758
$databaseCreated = $this->forge->createDatabase('test_forge_database');
5859

5960
$this->assertTrue($databaseCreated);
6061
}
6162

63+
public function testCreateDatabaseWithDots(): void
64+
{
65+
if ($this->db->DBDriver === 'OCI8') {
66+
$this->markTestSkipped('OCI8 does not support create database.');
67+
}
68+
69+
$dbName = 'test_com.sitedb.web';
70+
71+
$databaseCreated = $this->forge->createDatabase($dbName);
72+
73+
$this->assertTrue($databaseCreated);
74+
75+
// Checks if tableExists() works.
76+
$config = config(Database::class)->{$this->DBGroup};
77+
$config['database'] = $dbName;
78+
$db = db_connect($config);
79+
$result = $db->tableExists('not_exist');
80+
81+
$this->assertFalse($result);
82+
83+
$db->close();
84+
if ($this->db->DBDriver !== 'SQLite3') {
85+
$this->forge->dropDatabase($dbName);
86+
}
87+
}
88+
6289
public function testCreateDatabaseIfNotExists(): void
6390
{
6491
if ($this->db->DBDriver === 'OCI8') {
6592
$this->markTestSkipped('OCI8 does not support create database.');
6693
}
94+
6795
$dbName = 'test_forge_database_exist';
6896

6997
$databaseCreateIfNotExists = $this->forge->createDatabase($dbName, true);
98+
99+
$this->assertTrue($databaseCreateIfNotExists);
100+
70101
if ($this->db->DBDriver !== 'SQLite3') {
71102
$this->forge->dropDatabase($dbName);
72103
}
73-
74-
$this->assertTrue($databaseCreateIfNotExists);
75104
}
76105

77106
public function testCreateDatabaseIfNotExistsWithDb(): void
78107
{
79108
if ($this->db->DBDriver === 'OCI8') {
80109
$this->markTestSkipped('OCI8 does not support create database.');
81110
}
111+
82112
$dbName = 'test_forge_database_exist';
83113

84114
$this->forge->createDatabase($dbName);
85115
$databaseExists = $this->forge->createDatabase($dbName, true);
116+
117+
$this->assertTrue($databaseExists);
118+
86119
if ($this->db->DBDriver !== 'SQLite3') {
87120
$this->forge->dropDatabase($dbName);
88121
}
122+
}
123+
124+
public function testCreateDatabaseIfNotExistsWithDbWithDots(): void
125+
{
126+
if ($this->db->DBDriver === 'OCI8') {
127+
$this->markTestSkipped('OCI8 does not support create database.');
128+
}
129+
130+
$dbName = 'test_forge.database.exist';
131+
132+
$this->forge->createDatabase($dbName);
133+
$databaseExists = $this->forge->createDatabase($dbName, true);
89134

90135
$this->assertTrue($databaseExists);
136+
137+
if ($this->db->DBDriver !== 'SQLite3') {
138+
$this->forge->dropDatabase($dbName);
139+
}
91140
}
92141

93142
public function testDropDatabase(): void
@@ -106,17 +155,14 @@ public function testDropDatabase(): void
106155

107156
public function testCreateDatabaseExceptionNoCreateStatement(): void
108157
{
109-
$this->setPrivateProperty($this->forge, 'createDatabaseStr', false);
158+
if ($this->db->DBDriver !== 'OCI8') {
159+
$this->markTestSkipped($this->db->DBDriver . ' does support drop database.');
160+
}
110161

111-
if ($this->db->DBDriver === 'SQLite3') {
112-
$databaseCreated = $this->forge->createDatabase('test_forge_database');
113-
$this->assertTrue($databaseCreated);
114-
} else {
115-
$this->expectException(DatabaseException::class);
116-
$this->expectExceptionMessage('This feature is not available for the database you are using.');
162+
$this->expectException(DatabaseException::class);
163+
$this->expectExceptionMessage('This feature is not available for the database you are using.');
117164

118-
$this->forge->createDatabase('test_forge_database');
119-
}
165+
$this->forge->createDatabase('test_forge_database');
120166
}
121167

122168
public function testDropDatabaseExceptionNoDropStatement(): void

user_guide_src/source/changelogs/v4.5.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Forge
7272
Others
7373
------
7474

75+
- Added support for database names containing dots (``.``).
76+
7577
Model
7678
=====
7779

user_guide_src/source/database/configuration.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ Explanation of Values
149149
**password** The password used to connect to the database. (``SQLite3`` does not use this.)
150150
**database** The name of the database you want to connect to.
151151

152-
.. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names.
152+
.. note:: CodeIgniter doesn't support dots (``.``) in the table and column names.
153+
Since v4.5.0, database names with dots are supported.
153154
**DBDriver** The database driver name. The case must match the driver name.
154155
You can set a fully qualified classname to use your custom driver.
155156
Supported drivers: ``MySQLi``, ``Postgre``, ``SQLite3``, ``SQLSRV``, and ``OCI8``.

user_guide_src/source/database/examples.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ The following page contains example code showing how the database class
66
is used. For complete details please read the individual pages
77
describing each function.
88

9-
.. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names.
9+
.. note:: CodeIgniter doesn't support dots (``.``) in the table and column names.
10+
Since v4.5.0, database names with dots are supported.
1011

1112
.. contents::
1213
:local:

user_guide_src/source/database/queries.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Queries
1010
Query Basics
1111
************
1212

13-
.. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names.
13+
.. note:: CodeIgniter doesn't support dots (``.``) in the table and column names.
14+
Since v4.5.0, database names with dots are supported.
1415

1516
Regular Queries
1617
===============

0 commit comments

Comments
 (0)