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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

79.41
/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\TableName;
19
use stdClass;
20

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

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

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

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

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

68
    /**
69
     * Quoted identifier flag
70
     *
71
     * Whether to use SQL-92 standard quoted identifier
72
     * (double quotes) or brackets for identifier escaping.
73
     *
74
     * @var bool
75
     */
76
    protected $_quoted_identifier = true;
77

78
    /**
79
     * List of reserved identifiers
80
     *
81
     * Identifiers that must NOT be escaped.
82
     *
83
     * @var list<string>
84
     */
85
    protected $_reserved_identifiers = ['*'];
86

87
    /**
88
     * Class constructor
89
     */
90
    public function __construct(array $params)
91
    {
92
        parent::__construct($params);
23✔
93

94
        // This is only supported as of SQLSRV 3.0
95
        if ($this->scrollable === null) {
23✔
96
            $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
23✔
97
        }
98
    }
99

100
    /**
101
     * Connect to the database.
102
     *
103
     * @return false|resource
104
     *
105
     * @throws DatabaseException
106
     */
107
    public function connect(bool $persistent = false)
108
    {
109
        $charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
22✔
110

111
        $connection = [
22✔
112
            'UID'                  => empty($this->username) ? '' : $this->username,
22✔
113
            'PWD'                  => empty($this->password) ? '' : $this->password,
22✔
114
            'Database'             => $this->database,
22✔
115
            'ConnectionPooling'    => $persistent ? 1 : 0,
22✔
116
            'CharacterSet'         => $charset,
22✔
117
            'Encrypt'              => $this->encrypt === true ? 1 : 0,
22✔
118
            'ReturnDatesAsStrings' => 1,
22✔
119
        ];
22✔
120

121
        // If the username and password are both empty, assume this is a
122
        // 'Windows Authentication Mode' connection.
123
        if (empty($connection['UID']) && empty($connection['PWD'])) {
22✔
124
            unset($connection['UID'], $connection['PWD']);
×
125
        }
126

127
        if (! str_contains($this->hostname, ',') && $this->port !== '') {
22✔
128
            $this->hostname .= ', ' . $this->port;
22✔
129
        }
130

131
        sqlsrv_configure('WarningsReturnAsErrors', 0);
22✔
132
        $this->connID = sqlsrv_connect($this->hostname, $connection);
22✔
133

134
        if ($this->connID !== false) {
22✔
135
            // Determine how identifiers are escaped
136
            $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
22✔
137
            $query = $query->getResultObject();
21✔
138

139
            $this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
21✔
140
            $this->escapeChar         = ($this->_quoted_identifier) ? '"' : ['[', ']'];
21✔
141

142
            return $this->connID;
21✔
143
        }
144

145
        throw new DatabaseException($this->getAllErrorMessages());
1✔
146
    }
147

148
    /**
149
     * For exception message
150
     *
151
     * @internal
152
     */
153
    public function getAllErrorMessages(): string
154
    {
155
        $errors = [];
2✔
156

157
        foreach (sqlsrv_errors() as $error) {
2✔
158
            $errors[] = $error['message']
2✔
159
                . ' SQLSTATE: ' . $error['SQLSTATE'] . ', code: ' . $error['code'];
2✔
160
        }
161

162
        return implode("\n", $errors);
2✔
163
    }
164

165
    /**
166
     * Keep or establish the connection if no queries have been sent for
167
     * a length of time exceeding the server's idle timeout.
168
     *
169
     * @return void
170
     */
171
    public function reconnect()
172
    {
173
        $this->close();
×
174
        $this->initialize();
×
175
    }
176

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

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

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

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

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

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

224
        return $sql;
31✔
225
    }
226

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

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

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

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

262
        $retVal = [];
38✔
263

264
        foreach ($query as $row) {
38✔
265
            $obj       = new stdClass();
27✔
266
            $obj->name = $row->index_name;
27✔
267

268
            $_fields     = explode(',', trim($row->index_keys));
27✔
269
            $obj->fields = array_map(static fn ($v): string => trim($v), $_fields);
27✔
270

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

277
            $retVal[$obj->name] = $obj;
27✔
278
        }
279

280
        return $retVal;
38✔
281
    }
282

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

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

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

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

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

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

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

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

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

369
        $query  = $query->getResultObject();
12✔
370
        $retVal = [];
12✔
371

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

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

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

386
            $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
12✔
387
            $retVal[$i]->default  = $query[$i]->COLUMN_DEFAULT;
12✔
388
        }
389

390
        return $retVal;
12✔
391
    }
392

393
    /**
394
     * Begin Transaction
395
     */
396
    protected function _transBegin(): bool
397
    {
398
        return sqlsrv_begin_transaction($this->connID);
17✔
399
    }
400

401
    /**
402
     * Commit Transaction
403
     */
404
    protected function _transCommit(): bool
405
    {
406
        return sqlsrv_commit($this->connID);
5✔
407
    }
408

409
    /**
410
     * Rollback Transaction
411
     */
412
    protected function _transRollback(): bool
413
    {
414
        return sqlsrv_rollback($this->connID);
17✔
415
    }
416

417
    /**
418
     * Returns the last error code and message.
419
     * Must return this format: ['code' => string|int, 'message' => string]
420
     * intval(code) === 0 means "no error".
421
     *
422
     * @return array<string, int|string>
423
     */
424
    public function error(): array
425
    {
426
        $error = [
33✔
427
            'code'    => '00000',
33✔
428
            'message' => '',
33✔
429
        ];
33✔
430

431
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
33✔
432

433
        if (! is_array($sqlsrvErrors)) {
33✔
434
            return $error;
2✔
435
        }
436

437
        $sqlsrvError = array_shift($sqlsrvErrors);
31✔
438
        if (isset($sqlsrvError['SQLSTATE'])) {
31✔
439
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
31✔
440
        } elseif (isset($sqlsrvError['code'])) {
×
441
            $error['code'] = $sqlsrvError['code'];
×
442
        }
443

444
        if (isset($sqlsrvError['message'])) {
31✔
445
            $error['message'] = $sqlsrvError['message'];
31✔
446
        }
447

448
        return $error;
31✔
449
    }
450

451
    /**
452
     * Returns the total number of rows affected by this query.
453
     */
454
    public function affectedRows(): int
455
    {
456
        if ($this->resultID === false) {
47✔
457
            return 0;
1✔
458
        }
459

460
        return sqlsrv_rows_affected($this->resultID);
46✔
461
    }
462

463
    /**
464
     * Select a specific database table to use.
465
     *
466
     * @return bool
467
     */
468
    public function setDatabase(?string $databaseName = null)
469
    {
470
        if ($databaseName === null || $databaseName === '') {
×
471
            $databaseName = $this->database;
×
472
        }
473

474
        if (empty($this->connID)) {
×
475
            $this->initialize();
×
476
        }
477

478
        if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
×
479
            $this->database  = $databaseName;
×
480
            $this->dataCache = [];
×
481

482
            return true;
×
483
        }
484

485
        return false;
×
486
    }
487

488
    /**
489
     * Executes the query against the database.
490
     *
491
     * @return false|resource
492
     */
493
    protected function execute(string $sql)
494
    {
495
        $stmt = ($this->scrollable === false || $this->isWriteType($sql)) ?
670✔
496
            sqlsrv_query($this->connID, $sql) :
639✔
497
            sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
670✔
498

499
        if ($stmt === false) {
670✔
500
            $error = $this->error();
31✔
501

502
            log_message('error', $error['message']);
31✔
503

504
            if ($this->DBDebug) {
31✔
505
                throw new DatabaseException($error['message']);
14✔
506
            }
507
        }
508

509
        return $stmt;
670✔
510
    }
511

512
    /**
513
     * Returns the last error encountered by this connection.
514
     *
515
     * @return array<string, int|string>
516
     *
517
     * @deprecated Use `error()` instead.
518
     */
519
    public function getError()
520
    {
521
        $error = [
×
522
            'code'    => '00000',
×
523
            'message' => '',
×
524
        ];
×
525

526
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
×
527

528
        if (! is_array($sqlsrvErrors)) {
×
529
            return $error;
×
530
        }
531

532
        $sqlsrvError = array_shift($sqlsrvErrors);
×
533
        if (isset($sqlsrvError['SQLSTATE'])) {
×
534
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
×
535
        } elseif (isset($sqlsrvError['code'])) {
×
536
            $error['code'] = $sqlsrvError['code'];
×
537
        }
538

539
        if (isset($sqlsrvError['message'])) {
×
540
            $error['message'] = $sqlsrvError['message'];
×
541
        }
542

543
        return $error;
×
544
    }
545

546
    /**
547
     * The name of the platform in use (MySQLi, mssql, etc)
548
     */
549
    public function getPlatform(): string
550
    {
551
        return $this->DBDriver;
10✔
552
    }
553

554
    /**
555
     * Returns a string containing the version of the database being used.
556
     */
557
    public function getVersion(): string
558
    {
559
        $info = [];
3✔
560
        if (isset($this->dataCache['version'])) {
3✔
561
            return $this->dataCache['version'];
1✔
562
        }
563

564
        if (! $this->connID || ($info = sqlsrv_server_info($this->connID)) === []) {
2✔
565
            $this->initialize();
×
566
        }
567

568
        return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false;
2✔
569
    }
570

571
    /**
572
     * Determines if a query is a "write" type.
573
     *
574
     * Overrides BaseConnection::isWriteType, adding additional read query types.
575
     *
576
     * @param string $sql
577
     */
578
    public function isWriteType($sql): bool
579
    {
580
        if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
670✔
581
            return true;
3✔
582
        }
583

584
        return parent::isWriteType($sql);
670✔
585
    }
586
}
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

© 2025 Coveralls, Inc