• 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

88.95
/system/Database/SQLSRV/Connection.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\Exceptions\UniqueConstraintViolationException;
19
use CodeIgniter\Database\TableName;
20
use stdClass;
21

22
/**
23
 * Connection for SQLSRV
24
 *
25
 * @extends BaseConnection<resource, resource>
26
 */
27
class Connection extends BaseConnection
28
{
29
    /**
30
     * Database driver
31
     *
32
     * @var string
33
     */
34
    public $DBDriver = 'SQLSRV';
35

36
    /**
37
     * Database name
38
     *
39
     * @var string
40
     */
41
    public $database;
42

43
    /**
44
     * Scrollable flag
45
     *
46
     * Determines what cursor type to use when executing queries.
47
     *
48
     * FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
49
     * but would disable num_rows() (and possibly insert_id())
50
     *
51
     * @var false|string
52
     */
53
    public $scrollable;
54

55
    /**
56
     * Identifier escape character
57
     *
58
     * @var string
59
     */
60
    public $escapeChar = '"';
61

62
    /**
63
     * Database schema
64
     *
65
     * @var string
66
     */
67
    public $schema = 'dbo';
68

69
    /**
70
     * Trust server certificate.
71
     */
72
    public bool $trustServerCertificate = false;
73

74
    /**
75
     * Quoted identifier flag
76
     *
77
     * Whether to use SQL-92 standard quoted identifier
78
     * (double quotes) or brackets for identifier escaping.
79
     *
80
     * @var bool
81
     */
82
    protected $_quoted_identifier = true;
83

84
    /**
85
     * List of reserved identifiers
86
     *
87
     * Identifiers that must NOT be escaped.
88
     *
89
     * @var list<string>
90
     */
91
    protected $_reserved_identifiers = ['*'];
92

93
    /**
94
     * Class constructor
95
     */
96
    public function __construct(array $params)
97
    {
98
        parent::__construct($params);
30✔
99

100
        // This is only supported as of SQLSRV 3.0
101
        if ($this->scrollable === null) {
30✔
102
            $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
30✔
103
        }
104
    }
105

106
    /**
107
     * Connect to the database.
108
     *
109
     * @return false|resource
110
     *
111
     * @throws DatabaseException
112
     */
113
    public function connect(bool $persistent = false)
114
    {
115
        $charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
28✔
116

117
        $connection = [
28✔
118
            'UID'                    => empty($this->username) ? '' : $this->username,
28✔
119
            'PWD'                    => empty($this->password) ? '' : $this->password,
28✔
120
            'Database'               => $this->database,
28✔
121
            'ConnectionPooling'      => $persistent ? 1 : 0,
28✔
122
            'CharacterSet'           => $charset,
28✔
123
            'Encrypt'                => $this->encrypt === true ? 1 : 0,
28✔
124
            'TrustServerCertificate' => $this->trustServerCertificate ? 1 : 0,
28✔
125
            'ReturnDatesAsStrings'   => 1,
28✔
126
        ];
28✔
127

128
        // If the username and password are both empty, assume this is a
129
        // 'Windows Authentication Mode' connection.
130
        if (empty($connection['UID']) && empty($connection['PWD'])) {
28✔
UNCOV
131
            unset($connection['UID'], $connection['PWD']);
×
132
        }
133

134
        if (! str_contains($this->hostname, ',') && $this->port !== '') {
28✔
135
            $this->hostname .= ', ' . $this->port;
26✔
136
        }
137

138
        sqlsrv_configure('WarningsReturnAsErrors', 0);
28✔
139
        $this->connID = sqlsrv_connect($this->hostname, $connection);
28✔
140

141
        if ($this->connID !== false) {
28✔
142
            // Determine how identifiers are escaped
143
            $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
28✔
144
            $query = $query->getResultObject();
27✔
145

146
            $this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
27✔
147
            $this->escapeChar         = ($this->_quoted_identifier) ? '"' : ['[', ']'];
27✔
148

149
            return $this->connID;
27✔
150
        }
151

152
        throw new DatabaseException($this->getAllErrorMessages());
1✔
153
    }
154

155
    /**
156
     * For exception message
157
     *
158
     * @internal
159
     */
160
    public function getAllErrorMessages(): string
161
    {
162
        $errors = [];
37✔
163

164
        foreach (sqlsrv_errors() as $error) {
37✔
165
            $errors[] = sprintf(
37✔
166
                '%s SQLSTATE: %s, code: %s',
37✔
167
                $error['message'],
37✔
168
                $error['SQLSTATE'],
37✔
169
                $error['code'],
37✔
170
            );
37✔
171
        }
172

173
        return implode("\n", $errors);
37✔
174
    }
175

176
    /**
177
     * Close the database connection.
178
     *
179
     * @return void
180
     */
181
    protected function _close()
182
    {
183
        sqlsrv_close($this->connID);
3✔
184
    }
185

186
    /**
187
     * Platform-dependant string escape
188
     */
189
    protected function _escapeString(string $str): string
190
    {
191
        return str_replace("'", "''", remove_invisible_characters($str, false));
749✔
192
    }
193

194
    /**
195
     * Insert ID
196
     */
197
    public function insertID(): int
198
    {
199
        return (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0);
83✔
200
    }
201

202
    /**
203
     * Generates the SQL for listing tables in a platform-dependent manner.
204
     *
205
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
206
     */
207
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
208
    {
209
        $sql = 'SELECT [TABLE_NAME] AS "name"'
692✔
210
            . ' FROM [INFORMATION_SCHEMA].[TABLES] '
692✔
211
            . ' WHERE '
692✔
212
            . " [TABLE_SCHEMA] = '" . $this->schema . "'    ";
692✔
213

214
        if ($tableName !== null) {
692✔
215
            return $sql .= ' AND [TABLE_NAME] LIKE ' . $this->escape($tableName);
691✔
216
        }
217

218
        if ($prefixLimit && $this->DBPrefix !== '') {
34✔
UNCOV
219
            $sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' "
×
UNCOV
220
                . sprintf($this->likeEscapeStr, $this->likeEscapeChar);
×
221
        }
222

223
        return $sql;
34✔
224
    }
225

226
    /**
227
     * Generates a platform-specific query string so that the column names can be fetched.
228
     *
229
     * @param string|TableName $table
230
     */
231
    protected function _listColumns($table = ''): string
232
    {
233
        if ($table instanceof TableName) {
8✔
234
            $tableName = $this->escape(strtolower($table->getActualTableName()));
2✔
235
        } else {
236
            $tableName = $this->escape($this->DBPrefix . strtolower($table));
6✔
237
        }
238

239
        return 'SELECT [COLUMN_NAME] '
8✔
240
            . ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
8✔
241
            . ' WHERE  [TABLE_NAME] = ' . $tableName
8✔
242
            . ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
8✔
243
    }
244

245
    /**
246
     * Returns an array of objects with index data
247
     *
248
     * @return array<string, stdClass>
249
     *
250
     * @throws DatabaseException
251
     */
252
    protected function _indexData(string $table): array
253
    {
254
        $sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
41✔
255

256
        if (($query = $this->query($sql)) === false) {
41✔
UNCOV
257
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
258
        }
259
        $query = $query->getResultObject();
41✔
260

261
        $retVal = [];
41✔
262

263
        foreach ($query as $row) {
41✔
264
            $obj       = new stdClass();
28✔
265
            $obj->name = $row->index_name;
28✔
266

267
            $_fields     = explode(',', trim($row->index_keys));
28✔
268
            $obj->fields = array_map(trim(...), $_fields);
28✔
269

270
            if (str_contains($row->index_description, 'primary key located on')) {
28✔
271
                $obj->type = 'PRIMARY';
25✔
272
            } else {
273
                $obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX';
18✔
274
            }
275

276
            $retVal[$obj->name] = $obj;
28✔
277
        }
278

279
        return $retVal;
41✔
280
    }
281

282
    /**
283
     * Returns an array of objects with Foreign key data
284
     * referenced_object_id  parent_object_id
285
     *
286
     * @return array<string, stdClass>
287
     *
288
     * @throws DatabaseException
289
     */
290
    protected function _foreignKeyData(string $table): array
291
    {
292
        $sql = 'SELECT
5✔
293
                f.name as constraint_name,
294
                OBJECT_NAME (f.parent_object_id) as table_name,
295
                COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name,
296
                OBJECT_NAME(f.referenced_object_id) foreign_table_name,
297
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name,
298
                rc.delete_rule,
299
                rc.update_rule,
300
                rc.match_option
301
                FROM
302
                sys.foreign_keys AS f
303
                INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id
304
                INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id
305
                INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_NAME = f.name
306
                WHERE OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table);
5✔
307

308
        if (($query = $this->query($sql)) === false) {
5✔
UNCOV
309
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
310
        }
311

312
        $query   = $query->getResultObject();
5✔
313
        $indexes = [];
5✔
314

315
        foreach ($query as $row) {
5✔
316
            $indexes[$row->constraint_name]['constraint_name']       = $row->constraint_name;
4✔
317
            $indexes[$row->constraint_name]['table_name']            = $row->table_name;
4✔
318
            $indexes[$row->constraint_name]['column_name'][]         = $row->column_name;
4✔
319
            $indexes[$row->constraint_name]['foreign_table_name']    = $row->foreign_table_name;
4✔
320
            $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
4✔
321
            $indexes[$row->constraint_name]['on_delete']             = $row->delete_rule;
4✔
322
            $indexes[$row->constraint_name]['on_update']             = $row->update_rule;
4✔
323
            $indexes[$row->constraint_name]['match']                 = $row->match_option;
4✔
324
        }
325

326
        return $this->foreignKeyDataToObjects($indexes);
5✔
327
    }
328

329
    /**
330
     * Disables foreign key checks temporarily.
331
     *
332
     * @return string
333
     */
334
    protected function _disableForeignKeyChecks()
335
    {
336
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
697✔
337
    }
338

339
    /**
340
     * Enables foreign key checks temporarily.
341
     *
342
     * @return string
343
     */
344
    protected function _enableForeignKeyChecks()
345
    {
346
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
697✔
347
    }
348

349
    /**
350
     * Returns an array of objects with field data
351
     *
352
     * @return list<stdClass>
353
     *
354
     * @throws DatabaseException
355
     */
356
    protected function _fieldData(string $table): array
357
    {
358
        $sql = 'SELECT
13✔
359
                COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
360
                COLUMN_DEFAULT, IS_NULLABLE
361
            FROM INFORMATION_SCHEMA.COLUMNS
362
            WHERE TABLE_NAME= ' . $this->escape(($table));
13✔
363

364
        if (($query = $this->query($sql)) === false) {
13✔
UNCOV
365
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
366
        }
367

368
        $query  = $query->getResultObject();
13✔
369
        $retVal = [];
13✔
370

371
        for ($i = 0, $c = count($query); $i < $c; $i++) {
13✔
372
            $retVal[$i] = new stdClass();
13✔
373

374
            $retVal[$i]->name = $query[$i]->COLUMN_NAME;
13✔
375
            $retVal[$i]->type = $query[$i]->DATA_TYPE;
13✔
376

377
            $retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
13✔
378
                ? $query[$i]->CHARACTER_MAXIMUM_LENGTH
11✔
379
                : (
13✔
380
                    $query[$i]->CHARACTER_MAXIMUM_LENGTH === -1
10✔
381
                    ? 'max'
1✔
382
                    : $query[$i]->NUMERIC_PRECISION
10✔
383
                );
13✔
384

385
            $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
13✔
386
            $retVal[$i]->default  = $this->normalizeDefault($query[$i]->COLUMN_DEFAULT);
13✔
387
        }
388

389
        return $retVal;
13✔
390
    }
391

392
    /**
393
     * Normalizes SQL Server COLUMN_DEFAULT values.
394
     * Removes wrapping parentheses and handles basic conversions.
395
     */
396
    private function normalizeDefault(?string $default): ?string
397
    {
398
        if ($default === null) {
13✔
399
            return null;
12✔
400
        }
401

402
        $default = trim($default);
2✔
403

404
        // Remove outer parentheses (handles both single and double wrapping)
405
        while (preg_match('/^\((.*)\)$/', $default, $matches)) {
2✔
406
            $default = trim($matches[1]);
2✔
407
        }
408

409
        // Handle NULL literal
410
        if (strcasecmp($default, 'NULL') === 0) {
2✔
UNCOV
411
            return null;
×
412
        }
413

414
        // Handle string literals - remove quotes and unescape
415
        if (preg_match("/^'(.*)'$/s", $default, $matches)) {
2✔
UNCOV
416
            return str_replace("''", "'", $matches[1]);
×
417
        }
418

419
        return $default;
2✔
420
    }
421

422
    /**
423
     * Begin Transaction
424
     */
425
    protected function _transBegin(): bool
426
    {
427
        return sqlsrv_begin_transaction($this->connID);
19✔
428
    }
429

430
    /**
431
     * Commit Transaction
432
     */
433
    protected function _transCommit(): bool
434
    {
435
        return sqlsrv_commit($this->connID);
5✔
436
    }
437

438
    /**
439
     * Rollback Transaction
440
     */
441
    protected function _transRollback(): bool
442
    {
443
        return sqlsrv_rollback($this->connID);
19✔
444
    }
445

446
    /**
447
     * Returns the last error code and message.
448
     * Must return this format: ['code' => string|int, 'message' => string]
449
     * intval(code) === 0 means "no error".
450
     *
451
     * @return array<string, int|string>
452
     */
453
    public function error(): array
454
    {
455
        $error = [
38✔
456
            'code'    => '00000',
38✔
457
            'message' => '',
38✔
458
        ];
38✔
459

460
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
38✔
461

462
        if (! is_array($sqlsrvErrors)) {
38✔
463
            return $error;
2✔
464
        }
465

466
        $sqlsrvError = array_shift($sqlsrvErrors);
36✔
467
        if (isset($sqlsrvError['SQLSTATE'])) {
36✔
468
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
36✔
UNCOV
469
        } elseif (isset($sqlsrvError['code'])) {
×
470
            $error['code'] = $sqlsrvError['code'];
×
471
        }
472

473
        if (isset($sqlsrvError['message'])) {
36✔
474
            $error['message'] = $sqlsrvError['message'];
36✔
475
        }
476

477
        return $error;
36✔
478
    }
479

480
    /**
481
     * Returns the total number of rows affected by this query.
482
     */
483
    public function affectedRows(): int
484
    {
485
        if ($this->resultID === false) {
51✔
486
            return 0;
1✔
487
        }
488

489
        return sqlsrv_rows_affected($this->resultID);
50✔
490
    }
491

492
    /**
493
     * Select a specific database table to use.
494
     *
495
     * @return bool
496
     */
497
    public function setDatabase(?string $databaseName = null)
498
    {
UNCOV
499
        if ($databaseName === null || $databaseName === '') {
×
500
            $databaseName = $this->database;
×
501
        }
502

UNCOV
503
        if (empty($this->connID)) {
×
504
            $this->initialize();
×
505
        }
506

507
        if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
×
UNCOV
508
            $this->database  = $databaseName;
×
UNCOV
509
            $this->dataCache = [];
×
510

UNCOV
511
            return true;
×
512
        }
513

UNCOV
514
        return false;
×
515
    }
516

517
    /**
518
     * Executes the query against the database.
519
     *
520
     * @return false|resource
521
     */
522
    protected function execute(string $sql)
523
    {
524
        $stmt = ($this->scrollable === false || $this->isWriteType($sql))
779✔
525
            ? sqlsrv_query($this->connID, $sql)
742✔
526
            : sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
778✔
527

528
        if ($stmt === false) {
779✔
529
            $trace   = debug_backtrace();
36✔
530
            $first   = array_shift($trace);
36✔
531
            $message = $this->getAllErrorMessages();
36✔
532

533
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
36✔
534
                'message' => $message,
36✔
535
                'exFile'  => clean_path($first['file']),
36✔
536
                'exLine'  => $first['line'],
36✔
537
                'trace'   => render_backtrace($trace),
36✔
538
            ]);
36✔
539

540
            $error     = $this->error();
36✔
541
            $exception = $this->isUniqueConstraintViolation()
36✔
542
                ? new UniqueConstraintViolationException($message, $error['code'])
3✔
543
                : new DatabaseException($message, $error['code']);
33✔
544

545
            if ($this->DBDebug) {
36✔
546
                throw $exception;
17✔
547
            }
548

549
            $this->lastException = $exception;
19✔
550
        }
551

552
        return $stmt;
779✔
553
    }
554

555
    private function isUniqueConstraintViolation(): bool
556
    {
557
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
36✔
558
        if (! is_array($errors)) {
36✔
UNCOV
559
            return false;
×
560
        }
561

562
        foreach ($errors as $error) {
36✔
563
            // SQLSTATE 23000 (integrity constraint violation) with SQL Server error
564
            // 2627 (UNIQUE CONSTRAINT or PRIMARY KEY violation) or 2601 (UNIQUE INDEX violation).
565
            if (($error['SQLSTATE'] ?? '') === '23000'
36✔
566
                && in_array($error['code'] ?? 0, [2627, 2601], true)) {
36✔
567
                return true;
3✔
568
            }
569
        }
570

571
        return false;
33✔
572
    }
573

574
    /**
575
     * The name of the platform in use (MySQLi, mssql, etc)
576
     */
577
    public function getPlatform(): string
578
    {
579
        return $this->DBDriver;
10✔
580
    }
581

582
    /**
583
     * Returns a string containing the version of the database being used.
584
     */
585
    public function getVersion(): string
586
    {
587
        $info = [];
3✔
588
        if (isset($this->dataCache['version'])) {
3✔
589
            return $this->dataCache['version'];
2✔
590
        }
591

592
        if (! $this->connID) {
1✔
593
            $this->initialize();
1✔
594
        }
595

596
        if (($info = sqlsrv_server_info($this->connID)) === []) {
1✔
UNCOV
597
            return '';
×
598
        }
599

600
        return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : '';
1✔
601
    }
602

603
    /**
604
     * Determines if a query is a "write" type.
605
     *
606
     * Overrides BaseConnection::isWriteType, adding additional read query types.
607
     *
608
     * @param string $sql
609
     */
610
    public function isWriteType($sql): bool
611
    {
612
        if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
779✔
613
            return true;
3✔
614
        }
615

616
        return parent::isWriteType($sql);
779✔
617
    }
618
}
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