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

codeigniter4 / CodeIgniter4 / 28995226889

09 Jul 2026 04:59AM UTC coverage: 89.607% (+0.02%) from 89.588%
28995226889

Pull #10395

github

web-flow
Merge d47c78aa8 into 0d4f36d76
Pull Request #10395: feat: add StreamResponse and SSE response factories

31 of 41 new or added lines in 3 files covered. (75.61%)

223 existing lines in 6 files now uncovered.

25330 of 28268 relevant lines covered (89.61%)

230.92 hits per line

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

89.86
/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
     * Trust server certificate.
70
     */
71
    public bool $trustServerCertificate = false;
72

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

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

92
    /**
93
     * Checks whether the native database error represents a unique constraint violation.
94
     */
95
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
96
    {
97
        $vendorCode = $this->getVendorErrorCode($code);
73✔
98

99
        if ($vendorCode !== null && in_array($vendorCode, [2627, 2601], true)) {
73✔
100
            return $this->hasSQLState($code, '23000');
14✔
101
        }
102

103
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
59✔
104
        if (! is_array($errors)) {
59✔
105
            return false;
10✔
106
        }
107

108
        foreach ($errors as $error) {
49✔
109
            // SQLSTATE 23000 (integrity constraint violation) with SQL Server error
110
            // 2627 (UNIQUE CONSTRAINT or PRIMARY KEY violation) or 2601 (UNIQUE INDEX violation).
111
            if (($error['SQLSTATE'] ?? '') === '23000'
49✔
112
                && in_array($error['code'] ?? 0, [2627, 2601], true)) {
49✔
UNCOV
113
                return true;
×
114
            }
115
        }
116

117
        return false;
49✔
118
    }
119

120
    /**
121
     * Checks whether the native database error represents a NOT NULL constraint violation.
122
     */
123
    protected function isNotNullConstraintViolation(int|string $code, string $message): bool
124
    {
125
        return $this->getVendorErrorCode($code) === 515
61✔
126
            && $this->hasSQLState($code, '23000');
61✔
127
    }
128

129
    /**
130
     * Checks whether the native database error represents a constraint violation.
131
     */
132
    protected function isConstraintViolation(int|string $code, string $message): bool
133
    {
134
        return $this->getSQLState($code) === '23000'
54✔
135
            || ($this->getVendorErrorCode($code) === 547 && $this->hasSQLState($code, '23000'));
54✔
136
    }
137

138
    /**
139
     * Checks whether the native database code represents a retryable transaction failure.
140
     */
141
    protected function isRetryableTransactionErrorCode(int|string $code): bool
142
    {
143
        $vendorCode = $this->getVendorErrorCode($code);
26✔
144

145
        return $vendorCode !== null && in_array($vendorCode, [1205, 3960], true);
26✔
146
    }
147

148
    private function getVendorErrorCode(int|string $code): ?int
149
    {
150
        $vendorCode = (string) (is_string($code) && str_contains($code, '/')
73✔
151
            ? substr($code, strrpos($code, '/') + 1)
70✔
152
            : $code);
3✔
153

154
        return preg_match('/^\d+$/', $vendorCode) === 1 ? (int) $vendorCode : null;
73✔
155
    }
156

157
    private function getSQLState(int|string $code): string
158
    {
159
        return is_string($code) && str_contains($code, '/')
73✔
160
            ? substr($code, 0, strpos($code, '/'))
70✔
161
            : (string) $code;
73✔
162
    }
163

164
    private function hasSQLState(int|string $code, string $sqlstate): bool
165
    {
166
        return ! is_string($code)
23✔
167
            || ! str_contains($code, '/')
23✔
168
            || $this->getSQLState($code) === $sqlstate;
23✔
169
    }
170

171
    /**
172
     * Class constructor
173
     */
174
    public function __construct(array $params)
175
    {
176
        parent::__construct($params);
77✔
177

178
        // This is only supported as of SQLSRV 3.0
179
        if ($this->scrollable === null) {
77✔
180
            $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
77✔
181
        }
182
    }
183

184
    /**
185
     * Connect to the database.
186
     *
187
     * @return false|resource
188
     *
189
     * @throws DatabaseException
190
     */
191
    public function connect(bool $persistent = false)
192
    {
193
        $charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
72✔
194

195
        $connection = [
72✔
196
            'UID'                    => empty($this->username) ? '' : $this->username,
72✔
197
            'PWD'                    => empty($this->password) ? '' : $this->password,
72✔
198
            'Database'               => $this->database,
72✔
199
            'ConnectionPooling'      => $persistent ? 1 : 0,
72✔
200
            'CharacterSet'           => $charset,
72✔
201
            'Encrypt'                => $this->encrypt === true ? 1 : 0,
72✔
202
            'TrustServerCertificate' => $this->trustServerCertificate ? 1 : 0,
72✔
203
            'ReturnDatesAsStrings'   => 1,
72✔
204
        ];
72✔
205

206
        // If the username and password are both empty, assume this is a
207
        // 'Windows Authentication Mode' connection.
208
        if (empty($connection['UID']) && empty($connection['PWD'])) {
72✔
UNCOV
209
            unset($connection['UID'], $connection['PWD']);
×
210
        }
211

212
        if (! str_contains($this->hostname, ',') && $this->port !== '') {
72✔
213
            $this->hostname .= ', ' . $this->port;
70✔
214
        }
215

216
        sqlsrv_configure('WarningsReturnAsErrors', 0);
72✔
217
        $this->connID = sqlsrv_connect($this->hostname, $connection);
72✔
218

219
        if ($this->connID !== false) {
72✔
220
            // Determine how identifiers are escaped
221
            $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
72✔
222
            $query = $query->getResultObject();
71✔
223

224
            $this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
71✔
225
            $this->escapeChar         = ($this->_quoted_identifier) ? '"' : ['[', ']'];
71✔
226

227
            return $this->connID;
71✔
228
        }
229

230
        throw new DatabaseException($this->getAllErrorMessages());
1✔
231
    }
232

233
    /**
234
     * For exception message
235
     *
236
     * @internal
237
     */
238
    public function getAllErrorMessages(): string
239
    {
240
        $errors = [];
57✔
241

242
        foreach (sqlsrv_errors() as $error) {
57✔
243
            $errors[] = sprintf(
57✔
244
                '%s SQLSTATE: %s, code: %s',
57✔
245
                $error['message'],
57✔
246
                $error['SQLSTATE'],
57✔
247
                $error['code'],
57✔
248
            );
57✔
249
        }
250

251
        return implode("\n", $errors);
57✔
252
    }
253

254
    /**
255
     * Close the database connection.
256
     *
257
     * @return void
258
     */
259
    protected function _close()
260
    {
261
        sqlsrv_close($this->connID);
6✔
262
    }
263

264
    /**
265
     * Platform-dependant string escape
266
     */
267
    protected function _escapeString(string $str): string
268
    {
269
        return str_replace("'", "''", remove_invisible_characters($str, false));
894✔
270
    }
271

272
    /**
273
     * Insert ID
274
     */
275
    public function insertID(): int
276
    {
277
        return (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0);
90✔
278
    }
279

280
    /**
281
     * Generates the SQL for listing tables in a platform-dependent manner.
282
     *
283
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
284
     */
285
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
286
    {
287
        $sql = 'SELECT [TABLE_NAME] AS "name"'
837✔
288
            . ' FROM [INFORMATION_SCHEMA].[TABLES] '
837✔
289
            . ' WHERE '
837✔
290
            . " [TABLE_SCHEMA] = '" . $this->schema . "'    ";
837✔
291

292
        if ($tableName !== null) {
837✔
293
            return $sql .= ' AND [TABLE_NAME] LIKE ' . $this->escape($tableName);
836✔
294
        }
295

296
        if ($prefixLimit && $this->DBPrefix !== '') {
87✔
UNCOV
297
            $sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' "
×
UNCOV
298
                . sprintf($this->likeEscapeStr, $this->likeEscapeChar);
×
299
        }
300

301
        return $sql;
87✔
302
    }
303

304
    /**
305
     * Generates a platform-specific query string so that the column names can be fetched.
306
     *
307
     * @param string|TableName $table
308
     */
309
    protected function _listColumns($table = ''): string
310
    {
311
        if ($table instanceof TableName) {
13✔
312
            $tableName = $this->escape(strtolower($table->getActualTableName()));
7✔
313
        } else {
314
            $tableName = $this->escape($this->DBPrefix . strtolower($table));
6✔
315
        }
316

317
        return 'SELECT [COLUMN_NAME] '
13✔
318
            . ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
13✔
319
            . ' WHERE  [TABLE_NAME] = ' . $tableName
13✔
320
            . ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
13✔
321
    }
322

323
    /**
324
     * Returns an array of objects with index data
325
     *
326
     * @return array<string, stdClass>
327
     *
328
     * @throws DatabaseException
329
     */
330
    protected function _indexData(string $table): array
331
    {
332
        $sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
41✔
333

334
        if (($query = $this->query($sql)) === false) {
41✔
UNCOV
335
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
336
        }
337
        $query = $query->getResultObject();
41✔
338

339
        $retVal = [];
41✔
340

341
        foreach ($query as $row) {
41✔
342
            $obj       = new stdClass();
28✔
343
            $obj->name = $row->index_name;
28✔
344

345
            $_fields     = explode(',', trim($row->index_keys));
28✔
346
            $obj->fields = array_map(trim(...), $_fields);
28✔
347

348
            if (str_contains($row->index_description, 'primary key located on')) {
28✔
349
                $obj->type = 'PRIMARY';
25✔
350
            } else {
351
                $obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX';
18✔
352
            }
353

354
            $retVal[$obj->name] = $obj;
28✔
355
        }
356

357
        return $retVal;
41✔
358
    }
359

360
    /**
361
     * Returns an array of objects with Foreign key data
362
     * referenced_object_id  parent_object_id
363
     *
364
     * @return array<string, stdClass>
365
     *
366
     * @throws DatabaseException
367
     */
368
    protected function _foreignKeyData(string $table): array
369
    {
370
        $sql = 'SELECT
5✔
371
                f.name as constraint_name,
372
                OBJECT_NAME (f.parent_object_id) as table_name,
373
                COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name,
374
                OBJECT_NAME(f.referenced_object_id) foreign_table_name,
375
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name,
376
                rc.delete_rule,
377
                rc.update_rule,
378
                rc.match_option
379
                FROM
380
                sys.foreign_keys AS f
381
                INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id
382
                INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id
383
                INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_NAME = f.name
384
                WHERE OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table);
5✔
385

386
        if (($query = $this->query($sql)) === false) {
5✔
UNCOV
387
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
388
        }
389

390
        $query   = $query->getResultObject();
5✔
391
        $indexes = [];
5✔
392

393
        foreach ($query as $row) {
5✔
394
            $indexes[$row->constraint_name]['constraint_name']       = $row->constraint_name;
4✔
395
            $indexes[$row->constraint_name]['table_name']            = $row->table_name;
4✔
396
            $indexes[$row->constraint_name]['column_name'][]         = $row->column_name;
4✔
397
            $indexes[$row->constraint_name]['foreign_table_name']    = $row->foreign_table_name;
4✔
398
            $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
4✔
399
            $indexes[$row->constraint_name]['on_delete']             = $row->delete_rule;
4✔
400
            $indexes[$row->constraint_name]['on_update']             = $row->update_rule;
4✔
401
            $indexes[$row->constraint_name]['match']                 = $row->match_option;
4✔
402
        }
403

404
        return $this->foreignKeyDataToObjects($indexes);
5✔
405
    }
406

407
    /**
408
     * Disables foreign key checks temporarily.
409
     *
410
     * @return string
411
     */
412
    protected function _disableForeignKeyChecks()
413
    {
414
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
842✔
415
    }
416

417
    /**
418
     * Enables foreign key checks temporarily.
419
     *
420
     * @return string
421
     */
422
    protected function _enableForeignKeyChecks()
423
    {
424
        return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
842✔
425
    }
426

427
    /**
428
     * Returns an array of objects with field data
429
     *
430
     * @return list<stdClass>
431
     *
432
     * @throws DatabaseException
433
     */
434
    protected function _fieldData(string $table): array
435
    {
436
        $sql = 'SELECT
15✔
437
                COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
438
                COLUMN_DEFAULT, IS_NULLABLE
439
            FROM INFORMATION_SCHEMA.COLUMNS
440
            WHERE TABLE_NAME= ' . $this->escape(($table));
15✔
441

442
        if (($query = $this->query($sql)) === false) {
15✔
UNCOV
443
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
444
        }
445

446
        $query  = $query->getResultObject();
15✔
447
        $retVal = [];
15✔
448

449
        for ($i = 0, $c = count($query); $i < $c; $i++) {
15✔
450
            $retVal[$i] = new stdClass();
15✔
451

452
            $retVal[$i]->name = $query[$i]->COLUMN_NAME;
15✔
453
            $retVal[$i]->type = $query[$i]->DATA_TYPE;
15✔
454

455
            $retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
15✔
456
                ? $query[$i]->CHARACTER_MAXIMUM_LENGTH
13✔
457
                : (
15✔
458
                    $query[$i]->CHARACTER_MAXIMUM_LENGTH === -1
12✔
459
                    ? 'max'
3✔
460
                    : $query[$i]->NUMERIC_PRECISION
12✔
461
                );
15✔
462

463
            $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
15✔
464
            $retVal[$i]->default  = $this->normalizeDefault($query[$i]->COLUMN_DEFAULT);
15✔
465
        }
466

467
        return $retVal;
15✔
468
    }
469

470
    /**
471
     * Normalizes SQL Server COLUMN_DEFAULT values.
472
     * Removes wrapping parentheses and handles basic conversions.
473
     */
474
    private function normalizeDefault(?string $default): ?string
475
    {
476
        if ($default === null) {
15✔
477
            return null;
14✔
478
        }
479

480
        $default = trim($default);
2✔
481

482
        // Remove outer parentheses (handles both single and double wrapping)
483
        while (preg_match('/^\((.*)\)$/', $default, $matches)) {
2✔
484
            $default = trim($matches[1]);
2✔
485
        }
486

487
        // Handle NULL literal
488
        if (strcasecmp($default, 'NULL') === 0) {
2✔
UNCOV
489
            return null;
×
490
        }
491

492
        // Handle string literals - remove quotes and unescape
493
        if (preg_match("/^'(.*)'$/s", $default, $matches)) {
2✔
UNCOV
494
            return str_replace("''", "'", $matches[1]);
×
495
        }
496

497
        return $default;
2✔
498
    }
499

500
    /**
501
     * Begin Transaction
502
     */
503
    protected function _transBegin(): bool
504
    {
505
        return sqlsrv_begin_transaction($this->connID);
59✔
506
    }
507

508
    /**
509
     * Commit Transaction
510
     */
511
    protected function _transCommit(): bool
512
    {
513
        return sqlsrv_commit($this->connID);
21✔
514
    }
515

516
    /**
517
     * Rollback Transaction
518
     */
519
    protected function _transRollback(): bool
520
    {
521
        return sqlsrv_rollback($this->connID);
47✔
522
    }
523

524
    /**
525
     * Returns the last error code and message.
526
     * Must return this format: ['code' => string|int, 'message' => string]
527
     * intval(code) === 0 means "no error".
528
     *
529
     * @return array{code: int|string|null, message: string|null}
530
     */
531
    public function error(): array
532
    {
533
        $error = [
59✔
534
            'code'    => '00000',
59✔
535
            'message' => '',
59✔
536
        ];
59✔
537

538
        $sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
59✔
539

540
        if (! is_array($sqlsrvErrors)) {
59✔
541
            return $error;
2✔
542
        }
543

544
        $sqlsrvError = array_shift($sqlsrvErrors);
57✔
545
        if (isset($sqlsrvError['SQLSTATE'])) {
57✔
546
            $error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
57✔
547
        } elseif (isset($sqlsrvError['code'])) {
×
UNCOV
548
            $error['code'] = $sqlsrvError['code'];
×
549
        }
550

551
        if (isset($sqlsrvError['message'])) {
57✔
552
            $error['message'] = $sqlsrvError['message'];
57✔
553
        }
554

555
        return $error;
57✔
556
    }
557

558
    /**
559
     * Returns the total number of rows affected by this query.
560
     */
561
    public function affectedRows(): int
562
    {
563
        if ($this->resultID === false) {
56✔
564
            return 0;
1✔
565
        }
566

567
        return sqlsrv_rows_affected($this->resultID);
55✔
568
    }
569

570
    /**
571
     * Select a specific database table to use.
572
     *
573
     * @return bool
574
     */
575
    public function setDatabase(?string $databaseName = null)
576
    {
UNCOV
577
        if ($databaseName === null || $databaseName === '') {
×
UNCOV
578
            $databaseName = $this->database;
×
579
        }
580

UNCOV
581
        if (empty($this->connID)) {
×
UNCOV
582
            $this->initialize();
×
583
        }
584

UNCOV
585
        if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
×
UNCOV
586
            $this->database  = $databaseName;
×
UNCOV
587
            $this->dataCache = [];
×
588

UNCOV
589
            return true;
×
590
        }
591

UNCOV
592
        return false;
×
593
    }
594

595
    /**
596
     * Executes the query against the database.
597
     *
598
     * @return false|resource
599
     */
600
    protected function execute(string $sql)
601
    {
602
        $stmt = ($this->scrollable === false || $this->isWriteType($sql))
920✔
603
            ? sqlsrv_query($this->connID, $sql)
887✔
604
            : sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
920✔
605

606
        if ($stmt === false) {
920✔
607
            $trace   = debug_backtrace();
52✔
608
            $first   = array_shift($trace);
52✔
609
            $message = $this->getAllErrorMessages();
52✔
610

611
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
52✔
612
                'message' => $message,
52✔
613
                'exFile'  => clean_path($first['file']),
52✔
614
                'exLine'  => $first['line'],
52✔
615
                'trace'   => render_backtrace($trace),
52✔
616
            ]);
52✔
617

618
            $error     = $this->error();
52✔
619
            $exception = $this->createDatabaseException($message, $error['code']);
52✔
620

621
            if ($this->DBDebug) {
52✔
622
                throw $exception;
25✔
623
            }
624

625
            $this->lastException = $exception;
27✔
626
        }
627

628
        return $stmt;
920✔
629
    }
630

631
    /**
632
     * The name of the platform in use (MySQLi, mssql, etc)
633
     */
634
    public function getPlatform(): string
635
    {
636
        return $this->DBDriver;
13✔
637
    }
638

639
    /**
640
     * Returns a string containing the version of the database being used.
641
     */
642
    public function getVersion(): string
643
    {
644
        $info = [];
3✔
645
        if (isset($this->dataCache['version'])) {
3✔
646
            return $this->dataCache['version'];
2✔
647
        }
648

649
        if (! $this->connID) {
1✔
650
            $this->initialize();
1✔
651
        }
652

653
        if (($info = sqlsrv_server_info($this->connID)) === []) {
1✔
UNCOV
654
            return '';
×
655
        }
656

657
        return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : '';
1✔
658
    }
659

660
    /**
661
     * Determines if a query is a "write" type.
662
     *
663
     * Overrides BaseConnection::isWriteType, adding additional read query types.
664
     *
665
     * @param string $sql
666
     */
667
    public function isWriteType($sql): bool
668
    {
669
        if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
920✔
670
            return true;
3✔
671
        }
672

673
        return parent::isWriteType($sql);
920✔
674
    }
675
}
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