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

codeigniter4 / CodeIgniter4 / 12518821104

27 Dec 2024 05:21PM UTC coverage: 84.426% (+0.02%) from 84.404%
12518821104

Pull #9339

github

web-flow
Merge 5caee6ae0 into 6cbbf601b
Pull Request #9339: refactor: enable instanceof and strictBooleans rector set

55 of 60 new or added lines in 34 files covered. (91.67%)

19 existing lines in 3 files now uncovered.

20437 of 24207 relevant lines covered (84.43%)

189.66 hits per line

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

78.79
/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 stdClass;
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

141
            return $this->connID;
18✔
142
        }
143

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

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

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

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

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

176
    /**
177
     * Close the database connection.
178
     *
179
     * @return void
180
     */
181
    protected function _close()
182
    {
183
        sqlsrv_close($this->connID);
1✔
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));
633✔
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);
79✔
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"'
579✔
210
            . ' FROM [INFORMATION_SCHEMA].[TABLES] '
579✔
211
            . ' WHERE '
579✔
212
            . " [TABLE_SCHEMA] = '" . $this->schema . "'    ";
579✔
213

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

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

223
        return $sql;
28✔
224
    }
225

226
    /**
227
     * Generates a platform-specific query string so that the column names can be fetched.
228
     */
229
    protected function _listColumns(string $table = ''): string
230
    {
231
        return 'SELECT [COLUMN_NAME] '
8✔
232
            . ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
8✔
233
            . ' WHERE  [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table)
8✔
234
            . ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
8✔
235
    }
236

237
    /**
238
     * Returns an array of objects with index data
239
     *
240
     * @return array<string, stdClass>
241
     *
242
     * @throws DatabaseException
243
     */
244
    protected function _indexData(string $table): array
245
    {
246
        $sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
38✔
247

248
        if (($query = $this->query($sql)) === false) {
38✔
249
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
250
        }
251
        $query = $query->getResultObject();
38✔
252

253
        $retVal = [];
38✔
254

255
        foreach ($query as $row) {
38✔
256
            $obj       = new stdClass();
27✔
257
            $obj->name = $row->index_name;
27✔
258

259
            $_fields     = explode(',', trim($row->index_keys));
27✔
260
            $obj->fields = array_map(static fn ($v) => trim($v), $_fields);
27✔
261

262
            if (str_contains($row->index_description, 'primary key located on')) {
27✔
263
                $obj->type = 'PRIMARY';
25✔
264
            } else {
265
                $obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX';
17✔
266
            }
267

268
            $retVal[$obj->name] = $obj;
27✔
269
        }
270

271
        return $retVal;
38✔
272
    }
273

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

300
        if (($query = $this->query($sql)) === false) {
5✔
301
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
302
        }
303

304
        $query   = $query->getResultObject();
5✔
305
        $indexes = [];
5✔
306

307
        foreach ($query as $row) {
5✔
308
            $indexes[$row->constraint_name]['constraint_name']       = $row->constraint_name;
4✔
309
            $indexes[$row->constraint_name]['table_name']            = $row->table_name;
4✔
310
            $indexes[$row->constraint_name]['column_name'][]         = $row->column_name;
4✔
311
            $indexes[$row->constraint_name]['foreign_table_name']    = $row->foreign_table_name;
4✔
312
            $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
4✔
313
            $indexes[$row->constraint_name]['on_delete']             = $row->delete_rule;
4✔
314
            $indexes[$row->constraint_name]['on_update']             = $row->update_rule;
4✔
315
            $indexes[$row->constraint_name]['match']                 = $row->match_option;
4✔
316
        }
317

318
        return $this->foreignKeyDataToObjects($indexes);
5✔
319
    }
320

321
    /**
322
     * Disables foreign key checks temporarily.
323
     *
324
     * @return string
325
     */
326
    protected function _disableForeignKeyChecks()
327
    {
328
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
584✔
329
    }
330

331
    /**
332
     * Enables foreign key checks temporarily.
333
     *
334
     * @return string
335
     */
336
    protected function _enableForeignKeyChecks()
337
    {
338
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
584✔
339
    }
340

341
    /**
342
     * Returns an array of objects with field data
343
     *
344
     * @return list<stdClass>
345
     *
346
     * @throws DatabaseException
347
     */
348
    protected function _fieldData(string $table): array
349
    {
350
        $sql = 'SELECT
12✔
351
                COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
352
                COLUMN_DEFAULT, IS_NULLABLE
353
            FROM INFORMATION_SCHEMA.COLUMNS
354
            WHERE TABLE_NAME= ' . $this->escape(($table));
12✔
355

356
        if (($query = $this->query($sql)) === false) {
12✔
357
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
358
        }
359

360
        $query  = $query->getResultObject();
12✔
361
        $retVal = [];
12✔
362

363
        for ($i = 0, $c = count($query); $i < $c; $i++) {
12✔
364
            $retVal[$i] = new stdClass();
12✔
365

366
            $retVal[$i]->name = $query[$i]->COLUMN_NAME;
12✔
367
            $retVal[$i]->type = $query[$i]->DATA_TYPE;
12✔
368

369
            $retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
12✔
370
                ? $query[$i]->CHARACTER_MAXIMUM_LENGTH
11✔
371
                : (
12✔
372
                    $query[$i]->CHARACTER_MAXIMUM_LENGTH === -1
9✔
373
                    ? 'max'
1✔
374
                    : $query[$i]->NUMERIC_PRECISION
9✔
375
                );
12✔
376

377
            $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
12✔
378
            $retVal[$i]->default  = $query[$i]->COLUMN_DEFAULT;
12✔
379
        }
380

381
        return $retVal;
12✔
382
    }
383

384
    /**
385
     * Begin Transaction
386
     */
387
    protected function _transBegin(): bool
388
    {
389
        return sqlsrv_begin_transaction($this->connID);
14✔
390
    }
391

392
    /**
393
     * Commit Transaction
394
     */
395
    protected function _transCommit(): bool
396
    {
397
        return sqlsrv_commit($this->connID);
3✔
398
    }
399

400
    /**
401
     * Rollback Transaction
402
     */
403
    protected function _transRollback(): bool
404
    {
405
        return sqlsrv_rollback($this->connID);
14✔
406
    }
407

408
    /**
409
     * Returns the last error code and message.
410
     * Must return this format: ['code' => string|int, 'message' => string]
411
     * intval(code) === 0 means "no error".
412
     *
413
     * @return array<string, int|string>
414
     */
415
    public function error(): array
416
    {
417
        $error = [
29✔
418
            'code'    => '00000',
29✔
419
            'message' => '',
29✔
420
        ];
29✔
421

422
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
29✔
423

424
        if (! is_array($sqlsrvErrors)) {
29✔
425
            return $error;
2✔
426
        }
427

428
        $sqlsrvError = array_shift($sqlsrvErrors);
27✔
429
        if (isset($sqlsrvError['SQLSTATE'])) {
27✔
430
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
27✔
UNCOV
431
        } elseif (isset($sqlsrvError['code'])) {
×
UNCOV
432
            $error['code'] = $sqlsrvError['code'];
×
433
        }
434

435
        if (isset($sqlsrvError['message'])) {
27✔
436
            $error['message'] = $sqlsrvError['message'];
27✔
437
        }
438

439
        return $error;
27✔
440
    }
441

442
    /**
443
     * Returns the total number of rows affected by this query.
444
     */
445
    public function affectedRows(): int
446
    {
447
        return sqlsrv_rows_affected($this->resultID);
46✔
448
    }
449

450
    /**
451
     * Select a specific database table to use.
452
     *
453
     * @return bool
454
     */
455
    public function setDatabase(?string $databaseName = null)
456
    {
457
        if ($databaseName === null || $databaseName === '') {
×
458
            $databaseName = $this->database;
×
459
        }
460

461
        if (empty($this->connID)) {
×
462
            $this->initialize();
×
463
        }
464

465
        if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
×
UNCOV
466
            $this->database  = $databaseName;
×
UNCOV
467
            $this->dataCache = [];
×
468

UNCOV
469
            return true;
×
470
        }
471

UNCOV
472
        return false;
×
473
    }
474

475
    /**
476
     * Executes the query against the database.
477
     *
478
     * @return false|resource
479
     */
480
    protected function execute(string $sql)
481
    {
482
        $stmt = ($this->scrollable === false || $this->isWriteType($sql)) ?
658✔
483
            sqlsrv_query($this->connID, $sql) :
627✔
484
            sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
658✔
485

486
        if ($stmt === false) {
658✔
487
            $error = $this->error();
27✔
488

489
            log_message('error', $error['message']);
27✔
490

491
            if ($this->DBDebug) {
27✔
492
                throw new DatabaseException($error['message']);
11✔
493
            }
494
        }
495

496
        return $stmt;
658✔
497
    }
498

499
    /**
500
     * Returns the last error encountered by this connection.
501
     *
502
     * @return array<string, int|string>
503
     *
504
     * @deprecated Use `error()` instead.
505
     */
506
    public function getError()
507
    {
UNCOV
508
        $error = [
×
509
            'code'    => '00000',
×
UNCOV
510
            'message' => '',
×
511
        ];
×
512

UNCOV
513
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
×
514

515
        if (! is_array($sqlsrvErrors)) {
×
516
            return $error;
×
517
        }
518

519
        $sqlsrvError = array_shift($sqlsrvErrors);
×
UNCOV
520
        if (isset($sqlsrvError['SQLSTATE'])) {
×
UNCOV
521
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
×
522
        } elseif (isset($sqlsrvError['code'])) {
×
523
            $error['code'] = $sqlsrvError['code'];
×
524
        }
525

526
        if (isset($sqlsrvError['message'])) {
×
UNCOV
527
            $error['message'] = $sqlsrvError['message'];
×
528
        }
529

UNCOV
530
        return $error;
×
531
    }
532

533
    /**
534
     * The name of the platform in use (MySQLi, mssql, etc)
535
     */
536
    public function getPlatform(): string
537
    {
538
        return $this->DBDriver;
10✔
539
    }
540

541
    /**
542
     * Returns a string containing the version of the database being used.
543
     */
544
    public function getVersion(): string
545
    {
546
        $info = [];
3✔
547
        if (isset($this->dataCache['version'])) {
3✔
548
            return $this->dataCache['version'];
1✔
549
        }
550

551
        if (! $this->connID || ($info = sqlsrv_server_info($this->connID)) === []) {
2✔
UNCOV
552
            $this->initialize();
×
553
        }
554

555
        return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false;
2✔
556
    }
557

558
    /**
559
     * Determines if a query is a "write" type.
560
     *
561
     * Overrides BaseConnection::isWriteType, adding additional read query types.
562
     *
563
     * @param string $sql
564
     */
565
    public function isWriteType($sql): bool
566
    {
567
        if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
658✔
568
            return true;
3✔
569
        }
570

571
        return parent::isWriteType($sql);
658✔
572
    }
573
}
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