• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 22495985083

27 Feb 2026 05:08PM UTC coverage: 86.622% (+0.03%) from 86.594%
22495985083

push

github

web-flow
refactor: remove deprecations in `Database` (#9986)

7 of 9 new or added lines in 2 files covered. (77.78%)

283 existing lines in 13 files now uncovered.

22513 of 25990 relevant lines covered (86.62%)

218.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/system/Database/SQLSRV/Forge.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database\SQLSRV;
15

16
use CodeIgniter\Database\BaseConnection;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\Forge as BaseForge;
19
use Throwable;
20

21
/**
22
 * Forge for SQLSRV
23
 */
24
class Forge extends BaseForge
25
{
26
    /**
27
     * DROP CONSTRAINT statement
28
     *
29
     * @var string
30
     */
31
    protected $dropConstraintStr;
32

33
    /**
34
     * DROP INDEX statement
35
     *
36
     * @var string
37
     */
38
    protected $dropIndexStr;
39

40
    /**
41
     * CREATE DATABASE IF statement
42
     *
43
     * @todo missing charset, collat & check for existent
44
     *
45
     * @var string
46
     */
47
    protected $createDatabaseIfStr = "DECLARE @DBName VARCHAR(255) = '%s'\nDECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE %s'\nEXEC( @SQL )";
48

49
    /**
50
     * CREATE DATABASE IF statement
51
     *
52
     * @todo missing charset & collat
53
     *
54
     * @var string
55
     */
56
    protected $createDatabaseStr = 'CREATE DATABASE %s ';
57

58
    /**
59
     * CHECK DATABASE EXIST statement
60
     *
61
     * @var string
62
     */
63
    protected $checkDatabaseExistStr = 'IF DB_ID( %s ) IS NOT NULL SELECT 1';
64

65
    /**
66
     * RENAME TABLE statement
67
     *
68
     * While the below statement would work, it returns an error.
69
     * Also MS recommends dropping and dropping and re-creating the table.
70
     *
71
     * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql?view=sql-server-2017
72
     * 'EXEC sp_rename %s , %s ;'
73
     *
74
     * @var string
75
     */
76
    protected $renameTableStr;
77

78
    /**
79
     * UNSIGNED support
80
     *
81
     * @var array
82
     */
83
    protected $unsigned = [
84
        'TINYINT'  => 'SMALLINT',
85
        'SMALLINT' => 'INT',
86
        'INT'      => 'BIGINT',
87
        'REAL'     => 'FLOAT',
88
    ];
89

90
    /**
91
     * Foreign Key Allowed Actions
92
     *
93
     * @var array
94
     */
95
    protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
96

97
    /**
98
     * CREATE TABLE statement
99
     *
100
     * @var string
101
     */
102
    protected $createTableStr;
103

104
    public function __construct(BaseConnection $db)
105
    {
106
        parent::__construct($db);
785✔
107

108
        $this->createTableStr = '%s ' . $this->db->escapeIdentifiers($this->db->schema) . ".%s (%s\n) ";
785✔
109
        $this->renameTableStr = 'EXEC sp_rename [' . $this->db->escapeIdentifiers($this->db->schema) . '.%s] , %s ;';
785✔
110

111
        $this->dropConstraintStr = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s DROP CONSTRAINT %s';
785✔
112
        $this->dropIndexStr      = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s';
785✔
113
    }
114

115
    /**
116
     * Create database
117
     *
118
     * @param bool $ifNotExists Whether to add IF NOT EXISTS condition
119
     *
120
     * @throws DatabaseException
121
     */
122
    public function createDatabase(string $dbName, bool $ifNotExists = false): bool
123
    {
124
        if ($ifNotExists) {
7✔
125
            $sql = sprintf(
3✔
126
                $this->createDatabaseIfStr,
3✔
127
                $dbName,
3✔
128
                $this->db->escapeIdentifier($dbName),
3✔
129
            );
3✔
130
        } else {
131
            $sql = sprintf(
6✔
132
                $this->createDatabaseStr,
6✔
133
                $this->db->escapeIdentifier($dbName),
6✔
134
            );
6✔
135
        }
136

137
        try {
138
            if (! $this->db->query($sql)) {
7✔
139
                // @codeCoverageIgnoreStart
140
                if ($this->db->DBDebug) {
×
141
                    throw new DatabaseException('Unable to create the specified database.');
×
142
                }
143

144
                return false;
×
145
                // @codeCoverageIgnoreEnd
146
            }
147

148
            if (isset($this->db->dataCache['db_names'])) {
7✔
149
                $this->db->dataCache['db_names'][] = $dbName;
7✔
150
            }
151

152
            return true;
7✔
153
        } catch (Throwable $e) {
1✔
154
            if ($this->db->DBDebug) {
1✔
155
                throw new DatabaseException('Unable to create the specified database.', 0, $e);
1✔
156
            }
157

158
            return false; // @codeCoverageIgnore
×
159
        }
160
    }
161

162
    /**
163
     * {@inheritDoc}
164
     *
165
     * @see https://stackoverflow.com/questions/7469130/cannot-drop-database-because-it-is-currently-in-use
166
     */
167
    public function dropDatabase(string $dbName): bool
168
    {
169
        try {
170
            $this->db->query(sprintf(
8✔
171
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
8✔
172
                $this->db->escapeIdentifier($dbName),
8✔
173
            ));
8✔
174
        } catch (DatabaseException) {
1✔
175
            // no-op
176
        }
177

178
        return parent::dropDatabase($dbName);
8✔
179
    }
180

181
    /**
182
     * CREATE TABLE attributes
183
     */
184
    protected function _createTableAttributes(array $attributes): string
185
    {
186
        return '';
698✔
187
    }
188

189
    /**
190
     * @param array|string $processedFields Processed column definitions
191
     *                                      or column names to DROP
192
     *
193
     * @return ($alterType is 'DROP' ? string : false|list<string>)
194
     */
195
    protected function _alterTable(string $alterType, string $table, $processedFields)
196
    {
197
        // Handle DROP here
198
        if ($alterType === 'DROP') {
23✔
199
            $columnNamesToDrop = $processedFields;
16✔
200

201
            // check if fields are part of any indexes
202
            $indexData = $this->db->getIndexData($table);
16✔
203

204
            foreach ($indexData as $index) {
16✔
205
                if (is_string($columnNamesToDrop)) {
4✔
206
                    $columnNamesToDrop = explode(',', $columnNamesToDrop);
3✔
207
                }
208

209
                $fld = array_intersect($columnNamesToDrop, $index->fields);
4✔
210

211
                // Drop index if field is part of an index
212
                if ($fld !== []) {
4✔
213
                    $this->_dropIndex($table, $index);
2✔
214
                }
215
            }
216

217
            $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
16✔
218

219
            // Drop default constraints
220
            $fields = implode(',', $this->db->escape((array) $columnNamesToDrop));
16✔
221

222
            $sql = <<<SQL
16✔
223
                SELECT name
16✔
224
                FROM sys.default_constraints
225
                WHERE parent_object_id = OBJECT_ID('{$fullTable}')
16✔
226
                AND parent_column_id IN (
227
                SELECT column_id FROM sys.columns WHERE name IN ({$fields}) AND object_id = OBJECT_ID(N'{$fullTable}')
16✔
228
                )
229
                SQL;
16✔
230

231
            foreach ($this->db->query($sql)->getResultArray() as $index) {
16✔
232
                $this->db->query('ALTER TABLE ' . $fullTable . ' DROP CONSTRAINT ' . $index['name'] . '');
1✔
233
            }
234

235
            $sql = 'ALTER TABLE ' . $fullTable . ' DROP ';
16✔
236

237
            $fields = array_map(static fn ($item): string => 'COLUMN [' . trim($item) . ']', (array) $columnNamesToDrop);
16✔
238

239
            return $sql . implode(',', $fields);
16✔
240
        }
241

242
        $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
20✔
243
        $sql .= ($alterType === 'ADD') ? 'ADD ' : ' ';
20✔
244

245
        $sqls = [];
20✔
246

247
        if ($alterType === 'ADD') {
20✔
248
            foreach ($processedFields as $field) {
16✔
249
                $sqls[] = $sql . ($field['_literal'] !== false ? $field['_literal'] : $this->_processColumn($field));
16✔
250
            }
251

252
            return $sqls;
16✔
253
        }
254

255
        foreach ($processedFields as $field) {
4✔
256
            if ($field['_literal'] !== false) {
4✔
UNCOV
257
                return false;
×
258
            }
259

260
            if (isset($field['type'])) {
4✔
261
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
4✔
262
                    . " {$field['type']}{$field['length']}";
4✔
263
            }
264

265
            if (! empty($field['default'])) {
4✔
266
                $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
1✔
267
                $colName   = $field['name']; // bare, for sys.columns lookup
1✔
268

269
                // find the existing default constraint name for this column
270
                $findSql = <<<SQL
1✔
271
                    SELECT dc.name AS constraint_name
1✔
272
                    FROM sys.default_constraints dc
273
                    JOIN sys.columns c
274
                        ON dc.parent_object_id = c.object_id
275
                        AND dc.parent_column_id = c.column_id
276
                    WHERE dc.parent_object_id = OBJECT_ID(N'{$fullTable}')
1✔
277
                        AND c.name = N'{$colName}';
1✔
278
                    SQL;
1✔
279

280
                $toDrop = $this->db->query($findSql)->getRowArray();
1✔
281
                if (isset($toDrop['constraint_name']) && $toDrop['constraint_name'] !== '') {
1✔
282
                    $sqls[] = $sql . ' DROP CONSTRAINT ' . $this->db->escapeIdentifiers($toDrop['constraint_name']);
1✔
283
                }
284

285
                $sqls[] = $sql . ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($field['name'] . '_def')
1✔
286
                    . "{$field['default']} FOR " . $this->db->escapeIdentifiers($field['name']);
1✔
287
            }
288

289
            $nullable = true; // Nullable by default.
4✔
290
            if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) {
4✔
291
                $nullable = false;
2✔
292
            }
293
            $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
4✔
294
                . " {$field['type']}{$field['length']} " . ($nullable ? '' : 'NOT') . ' NULL';
4✔
295

296
            if (! empty($field['comment'])) {
4✔
UNCOV
297
                $sqls[] = 'EXEC sys.sp_addextendedproperty '
×
UNCOV
298
                    . "@name=N'Caption', @value=N'" . $field['comment'] . "' , "
×
UNCOV
299
                    . "@level0type=N'SCHEMA',@level0name=N'" . $this->db->schema . "', "
×
UNCOV
300
                    . "@level1type=N'TABLE',@level1name=N'" . $this->db->escapeIdentifiers($table) . "', "
×
UNCOV
301
                    . "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($field['name']) . "'";
×
302
            }
303

304
            if (! empty($field['new_name'])) {
4✔
305
                $sqls[] = "EXEC sp_rename  '[" . $this->db->schema . '].[' . $table . '].[' . $field['name'] . "]' , '" . $field['new_name'] . "', 'COLUMN';";
1✔
306
            }
307
        }
308

309
        return $sqls;
4✔
310
    }
311

312
    /**
313
     * Drop index for table
314
     *
315
     * @return mixed
316
     */
317
    protected function _dropIndex(string $table, object $indexData)
318
    {
319
        if ($indexData->type === 'PRIMARY') {
2✔
320
            $sql = 'ALTER TABLE [' . $this->db->schema . '].[' . $table . '] DROP [' . $indexData->name . ']';
2✔
321
        } else {
UNCOV
322
            $sql = 'DROP INDEX [' . $indexData->name . '] ON [' . $this->db->schema . '].[' . $table . ']';
×
323
        }
324

325
        return $this->db->simpleQuery($sql);
2✔
326
    }
327

328
    /**
329
     * Generates SQL to add indexes
330
     *
331
     * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
332
     */
333
    protected function _processIndexes(string $table, bool $asQuery = false): array
334
    {
335
        $sqls = [];
686✔
336

337
        for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
686✔
338
            for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
686✔
339
                if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
686✔
UNCOV
340
                    unset($this->keys[$i]['fields'][$i2]);
×
341
                }
342
            }
343

344
            if (count($this->keys[$i]['fields']) <= 0) {
686✔
UNCOV
345
                continue;
×
346
            }
347

348
            $keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
686✔
349
                $table . '_' . implode('_', $this->keys[$i]['fields']) :
686✔
350
                $this->keys[$i]['keyName']);
686✔
351

352
            if (in_array($i, $this->uniqueKeys, true)) {
686✔
353
                $sqls[] = 'ALTER TABLE '
686✔
354
                    . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
686✔
355
                    . ' ADD CONSTRAINT ' . $keyName
686✔
356
                    . ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ');';
686✔
357

358
                continue;
686✔
359
            }
360

361
            $sqls[] = 'CREATE INDEX '
686✔
362
                . $keyName
686✔
363
                . ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
686✔
364
                . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ');';
686✔
365
        }
366

367
        return $sqls;
686✔
368
    }
369

370
    /**
371
     * Process column
372
     */
373
    protected function _processColumn(array $processedField): string
374
    {
375
        return $this->db->escapeIdentifiers($processedField['name'])
698✔
376
            . (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name']))
698✔
377
            . ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length'])
698✔
378
            . $processedField['default']
698✔
379
            . $processedField['null']
698✔
380
            . $processedField['auto_increment']
698✔
381
            . ''
698✔
382
            . $processedField['unique'];
698✔
383
    }
384

385
    /**
386
     * Performs a data type mapping between different databases.
387
     */
388
    protected function _attributeType(array &$attributes)
389
    {
390
        // Reset field lengths for data types that don't support it
391
        if (isset($attributes['CONSTRAINT']) && str_contains(strtolower($attributes['TYPE']), 'int')) {
698✔
392
            $attributes['CONSTRAINT'] = null;
688✔
393
        }
394

395
        switch (strtoupper($attributes['TYPE'])) {
698✔
396
            case 'MEDIUMINT':
698✔
UNCOV
397
                $attributes['TYPE']     = 'INTEGER';
×
UNCOV
398
                $attributes['UNSIGNED'] = false;
×
UNCOV
399
                break;
×
400

401
            case 'INTEGER':
698✔
402
                $attributes['TYPE'] = 'INT';
687✔
403
                break;
687✔
404

405
            case 'ENUM':
698✔
406
                // in char(n) and varchar(n), the n defines the string length in
407
                // bytes (0 to 8,000).
408
                // https://learn.microsoft.com/en-us/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver16#remarks
409
                $maxLength = max(
686✔
410
                    array_map(
686✔
411
                        strlen(...),
686✔
412
                        $attributes['CONSTRAINT'],
686✔
413
                    ),
686✔
414
                );
686✔
415

416
                $attributes['TYPE']       = 'VARCHAR';
686✔
417
                $attributes['CONSTRAINT'] = $maxLength;
686✔
418
                break;
686✔
419

420
            case 'TIMESTAMP':
698✔
UNCOV
421
                $attributes['TYPE'] = 'DATETIME';
×
UNCOV
422
                break;
×
423

424
            case 'BOOLEAN':
698✔
425
                $attributes['TYPE'] = 'BIT';
686✔
426
                break;
686✔
427

428
            case 'BLOB':
698✔
429
                $attributes['TYPE'] = 'VARBINARY';
686✔
430
                $attributes['CONSTRAINT'] ??= 'MAX';
686✔
431
                break;
686✔
432

433
            default:
434
                break;
698✔
435
        }
436
    }
437

438
    /**
439
     * Field attribute AUTO_INCREMENT
440
     */
441
    protected function _attributeAutoIncrement(array &$attributes, array &$field)
442
    {
443
        if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && str_contains(strtolower($field['type']), strtolower('INT'))) {
698✔
444
            $field['auto_increment'] = ' IDENTITY(1,1)';
689✔
445
        }
446
    }
447

448
    /**
449
     * Generates a platform-specific DROP TABLE string
450
     *
451
     * @todo Support for cascade
452
     */
453
    protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
454
    {
455
        $sql = 'DROP TABLE';
697✔
456

457
        if ($ifExists) {
697✔
458
            $sql .= ' IF EXISTS ';
697✔
459
        }
460

461
        $table = ' [' . $this->db->database . '].[' . $this->db->schema . '].[' . $table . '] ';
697✔
462

463
        $sql .= $table;
697✔
464

465
        if ($cascade) {
697✔
466
            $sql .= '';
1✔
467
        }
468

469
        return $sql;
697✔
470
    }
471

472
    /**
473
     * Constructs sql to check if key is a constraint.
474
     */
475
    protected function _dropKeyAsConstraint(string $table, string $constraintName): string
476
    {
477
        return "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
2✔
478
                WHERE TABLE_NAME= '" . trim($table, '"') . "'
2✔
479
                AND CONSTRAINT_NAME = '" . trim($constraintName, '"') . "'";
2✔
480
    }
481
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc