Passed
Push — master ( edc9c1...558a8f )
by Alexander
08:28
created

QueryBuilder::dropCommentFromTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Query;
6
7
use Yiisoft\Db\Connection\Connection;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\InvalidParamException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Expression\ExpressionInterface;
16
use Yiisoft\Db\Query\Conditions\InCondition;
17
use Yiisoft\Db\Query\Conditions\LikeCondition;
18
use Yiisoft\Db\Query\Query;
19
use Yiisoft\Db\Query\QueryBuilder as BaseQueryBuilder;
20
use Yiisoft\Db\Sqlite\Condition\InConditionBuilder;
21
use Yiisoft\Db\Sqlite\Condition\LikeConditionBuilder;
22
use Yiisoft\Db\Sqlite\Schema\Schema;
23
use Yiisoft\Db\Sqlite\Token\SqlToken;
24
use Yiisoft\Db\Sqlite\Token\SqlTokenizer;
25
use Yiisoft\Strings\StringHelper;
26
27
use function array_column;
28
use function array_filter;
29
use function array_merge;
30
use function implode;
31
use function is_float;
32
use function is_string;
33
use function ltrim;
34
use function strpos;
35
use function substr;
36
use function trim;
37
use function version_compare;
38
39
class QueryBuilder extends BaseQueryBuilder
40
{
41
    /**
42
     * @var array mapping from abstract column types (keys) to physical column types (values).
43
     */
44
    protected array $typeMap = [
45
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
46
        Schema::TYPE_UPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
47
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
48
        Schema::TYPE_UBIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
49
        Schema::TYPE_CHAR => 'char(1)',
50
        Schema::TYPE_STRING => 'varchar(255)',
51
        Schema::TYPE_TEXT => 'text',
52
        Schema::TYPE_TINYINT => 'tinyint',
53
        Schema::TYPE_SMALLINT => 'smallint',
54
        Schema::TYPE_INTEGER => 'integer',
55
        Schema::TYPE_BIGINT => 'bigint',
56
        Schema::TYPE_FLOAT => 'float',
57
        Schema::TYPE_DOUBLE => 'double',
58
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
59
        Schema::TYPE_DATETIME => 'datetime',
60
        Schema::TYPE_TIMESTAMP => 'timestamp',
61
        Schema::TYPE_TIME => 'time',
62
        Schema::TYPE_DATE => 'date',
63
        Schema::TYPE_BINARY => 'blob',
64
        Schema::TYPE_BOOLEAN => 'boolean',
65
        Schema::TYPE_MONEY => 'decimal(19,4)',
66
    ];
67
68
    /**
69
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
70
     * expression builders for this query builder.
71
     *
72
     * @return array
73
     *
74
     * See {@see \Yiisoft\Db\Expression\ExpressionBuilder} docs for details.
75
     */
76 196
    protected function defaultExpressionBuilders(): array
77
    {
78 196
        return array_merge(parent::defaultExpressionBuilders(), [
79
            LikeCondition::class => LikeConditionBuilder::class,
80
            InCondition::class => InConditionBuilder::class,
81
        ]);
82
    }
83
84
    /**
85
     * Generates a batch INSERT SQL statement.
86
     *
87
     * For example,
88
     *
89
     * ```php
90
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
91
     *     ['Tom', 30],
92
     *     ['Jane', 20],
93
     *     ['Linda', 25],
94
     * ])->execute();
95
     * ```
96
     *
97
     * Note that the values in each row must match the corresponding column names.
98
     *
99
     * @param string $table the table that new rows will be inserted into.
100
     * @param array $columns the column names
101
     * @param array|\Generator $rows the rows to be batch inserted into the table
102
     * @param array $params
103
     *
104
     * @throws Exception
105
     * @throws InvalidConfigException
106
     * @throws InvalidArgumentException
107
     * @throws NotSupportedException
108
     *
109
     * @return string the batch INSERT SQL statement.
110
     */
111 13
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
112
    {
113 13
        if (empty($rows)) {
114 2
            return '';
115
        }
116
117
        /**
118
         * SQLite supports batch insert natively since 3.7.11.
119
         *
120
         * {@see http://www.sqlite.org/releaselog/3_7_11.html}
121
         */
122 12
        $this->db->open();
0 ignored issues
show
Bug introduced by
The method open() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

122
        $this->db->/** @scrutinizer ignore-call */ 
123
                   open();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
123
124 12
        if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
125 12
            return parent::batchInsert($table, $columns, $rows, $params);
126
        }
127
128
        $schema = $this->db->getSchema();
129
130
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
131
            $columnSchemas = $tableSchema->getColumns();
132
        } else {
133
            $columnSchemas = [];
134
        }
135
136
        $values = [];
137
138
        foreach ($rows as $row) {
139
            $vs = [];
140
            foreach ($row as $i => $value) {
141
                if (isset($columnSchemas[$columns[$i]])) {
142
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
143
                }
144
                if (is_string($value)) {
145
                    $value = $schema->quoteValue($value);
146
                } elseif (is_float($value)) {
147
                    // ensure type cast always has . as decimal separator in all locales
148
                    $value = StringHelper::floatToString($value);
149
                } elseif ($value === false) {
150
                    $value = 0;
151
                } elseif ($value === null) {
152
                    $value = 'NULL';
153
                } elseif ($value instanceof ExpressionInterface) {
154
                    $value = $this->buildExpression($value, $params);
155
                }
156
                $vs[] = $value;
157
            }
158
            $values[] = implode(', ', $vs);
159
        }
160
161
        if (empty($values)) {
162
            return '';
163
        }
164
165
        foreach ($columns as $i => $name) {
166
            $columns[$i] = $schema->quoteColumnName($name);
167
        }
168
169
        return 'INSERT INTO ' . $schema->quoteTableName($table)
170
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
171
    }
172
173
    /**
174
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
175
     *
176
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
177
     * or 1.
178
     *
179
     * @param string $tableName the name of the table whose primary key sequence will be reset.
180
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
181
     * row's primary key will have a value 1.
182
     *
183
     * @throws Exception
184
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
185
     * @throws InvalidConfigException
186
     * @throws NotSupportedException
187
     * @throws \Throwable
188
     *
189
     * @return string the SQL statement for resetting sequence.
190
     */
191 1
    public function resetSequence(string $tableName, $value = null): string
192
    {
193 1
        $db = $this->db;
194
195 1
        $table = $db->getTableSchema($tableName);
0 ignored issues
show
Bug introduced by
The method getTableSchema() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

195
        /** @scrutinizer ignore-call */ 
196
        $table = $db->getTableSchema($tableName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
196
197 1
        if ($table !== null && $table->getSequenceName() !== null) {
198 1
            $tableName = $db->quoteTableName($tableName);
199 1
            if ($value === null) {
200 1
                $pk = $table->getPrimaryKey();
201 1
                $key = $this->db->quoteColumnName(\reset($pk));
202 1
                $value = $this->db->useMaster(static function (Connection $db) use ($key, $tableName) {
203 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
204 1
                });
205
            } else {
206 1
                $value = (int) $value - 1;
207
            }
208
209 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
210
        } elseif ($table === null) {
211
            throw new InvalidArgumentException("Table not found: $tableName");
212
        }
213
214
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
215
    }
216
217
    /**
218
     * Enables or disables integrity check.
219
     *
220
     * @param bool $check whether to turn on or off the integrity check.
221
     * @param string $schema the schema of the tables. Meaningless for SQLite.
222
     * @param string $table the table name. Meaningless for SQLite.
223
     *
224
     * @return string the SQL statement for checking integrity.
225
     */
226
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
227
    {
228
        return 'PRAGMA foreign_keys=' . (int) $check;
229
    }
230
231
    /**
232
     * Builds a SQL statement for truncating a DB table.
233
     *
234
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
235
     *
236
     * @throws Exception
237
     * @throws InvalidConfigException
238
     * @throws NotSupportedException
239
     *
240
     * @return string the SQL statement for truncating a DB table.
241
     */
242 1
    public function truncateTable(string $table): string
243
    {
244 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
245
    }
246
247
    /**
248
     * Builds a SQL statement for dropping an index.
249
     *
250
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
251
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
252
     *
253
     * @throws Exception
254
     * @throws InvalidConfigException
255
     * @throws NotSupportedException
256
     *
257
     * @return string the SQL statement for dropping an index.
258
     */
259 2
    public function dropIndex(string $name, string $table): string
260
    {
261 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
262
    }
263
264
    /**
265
     * Builds a SQL statement for dropping a DB column.
266
     *
267
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
268
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
269
     *
270
     * @throws NotSupportedException this is not supported by SQLite.
271
     *
272
     * @return string the SQL statement for dropping a DB column.
273
     */
274
    public function dropColumn(string $table, string $column): string
275
    {
276
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
277
    }
278
279
    /**
280
     * Builds a SQL statement for renaming a column.
281
     *
282
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
283
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
284
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
285
     *
286
     * @throws NotSupportedException this is not supported by SQLite.
287
     *
288
     * @return string the SQL statement for renaming a DB column.
289
     */
290
    public function renameColumn(string $table, string $oldName, string $newName): string
291
    {
292
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
293
    }
294
295
    /**
296
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
297
     *
298
     * The method will properly quote the table and column names.
299
     *
300
     * @param string $name the name of the foreign key constraint.
301
     * @param string $table the table that the foreign key constraint will be added to.
302
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
303
     * multiple columns, separate them with commas or use an array to represent them.
304
     * @param string $refTable the table that the foreign key references to.
305
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
306
     * columns, separate them with commas or use an array to represent them.
307
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
308
     * SET DEFAULT, SET NULL.
309
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
310
     * SET DEFAULT, SET NULL.
311
     *
312
     * @throws Exception
313
     * @throws InvalidConfigException
314
     * @throws InvalidParamException
315
     * @throws NotSupportedException this is not supported by SQLite.
316
     *
317
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
318
     */
319 2
    public function addForeignKey(
320
        string $name,
321
        string $table,
322
        $columns,
323
        string $refTable,
324
        $refColumns,
325
        ?string $delete = null,
326
        ?string $update = null
327
    ): string {
328 2
        $schema = $refschema = '';
329
330 2
        if (($pos = strpos($table, '.')) !== false) {
331
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
332
            $table = substr($table, $pos + 1);
333
        }
334
335 2
        if (($pos_ref = strpos($refTable, '.')) !== false) {
336
            $refschema = substr($refTable, 0, $pos_ref);
337
            $refTable = substr($refTable, $pos_ref + 1);
338
        }
339
340 2
        if (($schema !== '' || ($refschema !== '' && $schema !== $refschema))) {
341
            return '' ;
342
        }
343
344 2
        if ($schema !== '') {
345
            $tmp_table_name =  "temp_{$schema}_" . $this->unquoteTableName($table);
346
            $schema .= '.';
347
            $unquoted_tablename = $schema . $this->unquoteTableName($table);
348
            $quoted_tablename = $schema . $this->db->quoteTableName($table);
349
        } else {
350 2
            $unquoted_tablename = $this->unquoteTableName($table);
351 2
            $quoted_tablename = $this->db->quoteTableName($table);
352 2
            $tmp_table_name =  "temp_" . $this->unquoteTableName($table);
353
        }
354
355 2
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
356 2
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
357 2
        $ddl_fields_defs .= ",\nCONSTRAINT " . $this->db->quoteColumnName($name) . " FOREIGN KEY (" .
358 2
            implode(",", (array)$columns) . ") REFERENCES $refTable(" . implode(",", (array)$refColumns) . ")";
359
360 2
        if ($update !== null) {
361
            $ddl_fields_defs .= " ON UPDATE $update";
362
        }
363
364 2
        if ($delete !== null) {
365
            $ddl_fields_defs .= " ON DELETE $delete";
366
        }
367
368 2
        $foreign_keys_state = $this->foreignKeysState();
369 2
        $return_queries = [];
370 2
        $return_queries[] = "PRAGMA foreign_keys = off";
371 2
        $return_queries[] = "SAVEPOINT add_foreign_key_to_$tmp_table_name";
372 2
        $return_queries[] = "CREATE TEMP TABLE " . $this->db->quoteTableName($tmp_table_name)
373 2
            . " AS SELECT * FROM $quoted_tablename";
374 2
        $return_queries[] = "DROP TABLE $quoted_tablename";
375 2
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ")";
376 2
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->db->quoteTableName($tmp_table_name);
377 2
        $return_queries[] = "DROP TABLE " . $this->db->quoteTableName($tmp_table_name);
378 2
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
379
380 2
        $return_queries[] = "RELEASE add_foreign_key_to_$tmp_table_name";
381 2
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
382
383 2
        return implode(";", $return_queries);
384
    }
385
386
    /**
387
     * Builds a SQL statement for dropping a foreign key constraint.
388
     *
389
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
390
     * by the method.
391
     * @param string $tableName
392
     *
393
     * @throws Exception
394
     * @throws InvalidConfigException
395
     * @throws InvalidParamException
396
     * @throws NotSupportedException
397
     *
398
     * @return string the SQL statement for dropping a foreign key constraint.
399
     */
400 1
    public function dropForeignKey(string $name, string $tableName): string
401
    {
402 1
        $return_queries = [];
403 1
        $ddl_fields_def = '';
404 1
        $sql_fields_to_insert = [];
405 1
        $skipping = false;
406 1
        $foreign_found = false;
407 1
        $quoted_foreign_name = $this->db->quoteColumnName($name);
408 1
        $quoted_tablename = $this->db->quoteTableName($tableName);
409 1
        $unquoted_tablename = $this->unquoteTableName($tableName);
410 1
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
411 1
        $offset = 0;
412 1
        $constraint_pos = 0;
413
414
        /** Traverse the tokens looking for either an identifier (field name) or a foreign key */
415 1
        while ($fields_definitions_tokens->offsetExists($offset)) {
416 1
            $token = $fields_definitions_tokens[$offset++];
417
            /**
418
             * These searchs could be done with another SqlTokenizer, but I don't konw how to do them, the documentation
419
             * for sqltokenizer si really scarse.
420
             */
421 1
            $tokenType = $token->getType();
0 ignored issues
show
Bug introduced by
The method getType() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

421
            /** @scrutinizer ignore-call */ 
422
            $tokenType = $token->getType();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
422 1
            if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
423 1
                $identifier = (string) $token;
424 1
                $sql_fields_to_insert[] = $identifier;
425 1
            } elseif ($tokenType === SqlToken::TYPE_KEYWORD) {
426 1
                $keyword = (string) $token;
427 1
                if ($keyword === 'CONSTRAINT' || $keyword === 'FOREIGN') {
428
                    /** Constraint key found */
429 1
                    $other_offset = $offset;
430 1
                    if ($keyword === 'CONSTRAINT') {
431 1
                        $constraint_name = (string) $fields_definitions_tokens[$other_offset];
432
                    } else {
433
                        $constraint_name = $this->db->quoteColumnName((string) $constraint_pos);
434
                    }
435 1
                    if (($constraint_name === $quoted_foreign_name) || (is_int($name) && $constraint_pos === $name)) {
436
                        /** Found foreign key $name, skip it */
437 1
                        $foreign_found = true;
438 1
                        $skipping = true;
439 1
                        $offset = $other_offset;
440
                    }
441 1
                    $constraint_pos++;
442
                }
443
            } else {
444
                throw new NotSupportedException("Unexpected: $token");
445
            }
446
447 1
            if (!$skipping) {
448 1
                $ddl_fields_def .= $token . " ";
449
            }
450
451
            /** Skip or keep until the next */
452 1
            while ($fields_definitions_tokens->offsetExists($offset)) {
453 1
                $skip_token = $fields_definitions_tokens[$offset];
454 1
                if (!$skipping) {
455 1
                    $ddl_fields_def .= (string)$skip_token . " ";
456
                }
457 1
                $skipTokenType = $skip_token->getType();
0 ignored issues
show
Bug introduced by
The method getType() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

457
                /** @scrutinizer ignore-call */ 
458
                $skipTokenType = $skip_token->getType();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
458 1
                if ($skipTokenType === SqlToken::TYPE_OPERATOR && (string)$skip_token == ',') {
459 1
                    $ddl_fields_def .= "\n";
460 1
                    ++$offset;
461 1
                    $skipping = false;
462 1
                    break;
463
                }
464 1
                ++$offset;
465
            }
466
        }
467
468 1
        if (!$foreign_found) {
469
            throw new InvalidParamException("foreign key constraint '$name' not found in table '$tableName'");
470
        }
471
472 1
        $foreign_keys_state = $this->foreignKeysState();
473 1
        $return_queries[] = "PRAGMA foreign_keys = 0";
474 1
        $return_queries[] = "SAVEPOINT drop_column_$unquoted_tablename";
475 1
        $return_queries[] = "CREATE TABLE " . $this->db->quoteTableName("temp_$unquoted_tablename")
476 1
            . " AS SELECT * FROM $quoted_tablename";
477 1
        $return_queries[] = "DROP TABLE $quoted_tablename";
478 1
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_def, " \n\r\t,") . ")";
479 1
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT " . implode(",", $sql_fields_to_insert) . " FROM "
480 1
             . $this->db->quoteTableName("temp_$unquoted_tablename");
481 1
        $return_queries[] = "DROP TABLE " . $this->db->quoteTableName("temp_$unquoted_tablename");
482
483 1
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
484
485 1
        $return_queries[] = "RELEASE drop_column_$unquoted_tablename";
486 1
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
487
488 1
        return implode(";", $return_queries);
489
    }
490
491
    /**
492
     * Builds a SQL statement for renaming a DB table.
493
     *
494
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
495
     * @param string $newName the new table name. The name will be properly quoted by the method.
496
     *
497
     * @throws Exception
498
     * @throws InvalidConfigException
499
     * @throws NotSupportedException
500
     *
501
     * @return string the SQL statement for renaming a DB table.
502
     */
503 3
    public function renameTable(string $table, string $newName): string
504
    {
505 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
506
    }
507
508
    /**
509
     * Builds a SQL statement for changing the definition of a column.
510
     *
511
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
512
     * method.
513
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
514
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
515
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
516
     * generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
517
     * 'varchar(255) not null'.
518
     *
519
     * @throws NotSupportedException this is not supported by SQLite.
520
     *
521
     * @return string the SQL statement for changing the definition of a column.
522
     */
523
    public function alterColumn(string $table, string $column, string $type): string
524
    {
525
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
526
    }
527
528
    /**
529
     * Builds a SQL statement for adding a primary key constraint to an existing table.
530
     *
531
     * @param string $name the name of the primary key constraint.
532
     * @param string $table the table that the primary key constraint will be added to.
533
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
534
     *
535
     * @throws Exception
536
     * @throws InvalidConfigException
537
     * @throws InvalidParamException
538
     * @throws NotSupportedException this is not supported by SQLite.
539
     *
540
     * @return string the SQL statement for adding a primary key constraint to an existing table.
541
     */
542
    public function addPrimaryKey(string $name, string $table, $columns): string
543
    {
544
        $return_queries = [];
545
        $schema = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $schema is dead and can be removed.
Loading history...
546
547
        if (($pos = strpos($table, '.')) !== false) {
548
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
549
            $table = substr($table, $pos + 1);
550
            $unquoted_tablename = $schema . '.' . $this->unquoteTableName($table);
551
            $quoted_tablename = $schema . '.' . $this->db->quoteTableName($table);
552
            $tmp_table_name =  "temp_{$schema}_" . $this->unquoteTableName($table);
553
        } else {
554
            $unquoted_tablename = $this->unquoteTableName($table);
555
            $quoted_tablename = $this->db->quoteTableName($table);
556
            $tmp_table_name =  "temp_" . $this->unquoteTableName($table);
557
        }
558
559
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
560
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
561
        $ddl_fields_defs .= ", CONSTRAINT " . $this->db->quoteColumnName($name) . " PRIMARY KEY (" .
562
            implode(",", (array)$columns) . ")";
563
        $foreign_keys_state = $this->foreignKeysState();
564
        $return_queries[] = "PRAGMA foreign_keys = 0";
565
        $return_queries[] = "SAVEPOINT add_primary_key_to_$tmp_table_name";
566
        $return_queries[] = "CREATE TABLE " . $this->db->quoteTableName($tmp_table_name) .
567
            " AS SELECT * FROM $quoted_tablename";
568
        $return_queries[] = "DROP TABLE $quoted_tablename";
569
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ")";
570
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->db->quoteTableName($tmp_table_name);
571
        $return_queries[] = "DROP TABLE " . $this->db->quoteTableName($tmp_table_name);
572
573
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
574
575
        $return_queries[] = "RELEASE add_primary_key_to_$tmp_table_name";
576
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
577
578
        return implode(";", $return_queries);
579
    }
580
581
    /**
582
     * Builds a SQL statement for removing a primary key constraint to an existing table.
583
     *
584
     * @param string $name the name of the primary key constraint to be removed.
585
     * @param string $table the table that the primary key constraint will be removed from.
586
     *
587
     * @throws NotSupportedException this is not supported by SQLite.
588
     *
589
     * @return string the SQL statement for removing a primary key constraint from an existing table.
590
     */
591
    public function dropPrimaryKey(string $name, string $table): string
592
    {
593
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
594
    }
595
596
    /**
597
     * Creates a SQL command for adding an unique constraint to an existing table.
598
     *
599
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
600
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
601
     * the method.
602
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
603
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
604
     *
605
     * @throws Exception
606
     * @throws InvalidArgumentException
607
     * @throws InvalidConfigException
608
     * @throws NotSupportedException
609
     *
610
     * @return string the SQL statement for adding an unique constraint to an existing table.
611
     */
612 1
    public function addUnique(string $name, string $table, $columns): string
613
    {
614 1
        return $this->createIndex($name, $table, $columns, true);
615
    }
616
617
    /**
618
     * Creates a SQL command for dropping an unique constraint.
619
     *
620
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
621
     * method.
622
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
623
     * method.
624
     *
625
     * @return string the SQL statement for dropping an unique constraint.
626
     */
627 1
    public function dropUnique(string $name, string $table): string
628
    {
629 1
        return "DROP INDEX $name";
630
    }
631
632
    /**
633
     * Creates a SQL command for adding a check constraint to an existing table.
634
     *
635
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
636
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
637
     * the method.
638
     * @param string $expression the SQL of the `CHECK` constraint.
639
     *
640
     * @throws Exception
641
     * @throws NotSupportedException
642
     *
643
     * @return string the SQL statement for adding a check constraint to an existing table.
644
     */
645
    public function addCheck(string $name, string $table, string $expression): string
646
    {
647
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
648
    }
649
650
    /**
651
     * Creates a SQL command for dropping a check constraint.
652
     *
653
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
654
     * method.
655
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
656
     * method.
657
     *
658
     * @throws Exception
659
     * @throws NotSupportedException
660
     *
661
     * @return string the SQL statement for dropping a check constraint.
662
     */
663
    public function dropCheck(string $name, string $table): string
664
    {
665
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
666
    }
667
668
    /**
669
     * Creates a SQL command for adding a default value constraint to an existing table.
670
     *
671
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
672
     * @param string $table the table that the default value constraint will be added to. The name will be properly
673
     * quoted by the method.
674
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
675
     * quoted by the method.
676
     * @param mixed $value default value.
677
     *
678
     * @throws Exception
679
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
680
     *
681
     * @return string the SQL statement for adding a default value constraint to an existing table.
682
     */
683
    public function addDefaultValue(string $name, string $table, string $column, $value): string
684
    {
685
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
686
    }
687
688
    /**
689
     * Creates a SQL command for dropping a default value constraint.
690
     *
691
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
692
     * the method.
693
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
694
     * by the method.
695
     *
696
     * @throws Exception
697
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
698
     *
699
     * @return string the SQL statement for dropping a default value constraint.
700
     */
701
    public function dropDefaultValue(string $name, string $table): string
702
    {
703
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
704
    }
705
706
    /**
707
     * Builds a SQL command for adding comment to column.
708
     *
709
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
710
     * method.
711
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
712
     * method.
713
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
714
     *
715
     * @throws Exception
716
     * @throws NotSupportedException
717
     *
718
     * @return string the SQL statement for adding comment on column.
719
     */
720
    public function addCommentOnColumn(string $table, string $column, string $comment): string
721
    {
722
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
723
    }
724
725
    /**
726
     * Builds a SQL command for adding comment to table.
727
     *
728
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
729
     * method.
730
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
731
     *
732
     * @throws Exception
733
     * @throws NotSupportedException
734
     *
735
     * @return string the SQL statement for adding comment on table.
736
     */
737
    public function addCommentOnTable(string $table, string $comment): string
738
    {
739
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
740
    }
741
742
    /**
743
     * Builds a SQL command for adding comment to column.
744
     *
745
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
746
     * method.
747
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
748
     * method.
749
     *
750
     * @throws Exception
751
     * @throws NotSupportedException
752
     *
753
     * @return string the SQL statement for adding comment on column.
754
     */
755
    public function dropCommentFromColumn(string $table, string $column): string
756
    {
757
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
758
    }
759
760
    /**
761
     * Builds a SQL command for adding comment to table.
762
     *
763
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
764
     * method.
765
     *
766
     * @throws Exception
767
     * @throws NotSupportedException
768
     *
769
     * @return string the SQL statement for adding comment on column.
770
     */
771
    public function dropCommentFromTable(string $table): string
772
    {
773
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
774
    }
775
776
    /**
777
     * @param int|object|null $limit
778
     * @param int|object|null $offset
779
     *
780
     * @return string the LIMIT and OFFSET clauses.
781
     */
782 136
    public function buildLimit($limit, $offset): string
783
    {
784 136
        $sql = '';
785
786 136
        if ($this->hasLimit($limit)) {
787 8
            $sql = 'LIMIT ' . $limit;
0 ignored issues
show
Bug introduced by
Are you sure $limit of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

787
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
788 8
            if ($this->hasOffset($offset)) {
789 8
                $sql .= ' OFFSET ' . $offset;
0 ignored issues
show
Bug introduced by
Are you sure $offset of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

789
                $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
790
            }
791 131
        } elseif ($this->hasOffset($offset)) {
792
            /**
793
             * limit is not optional in SQLite.
794
             *
795
             * {@see http://www.sqlite.org/syntaxdiagrams.html#select-stmt}
796
             */
797
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
798
        }
799
800 136
        return $sql;
801
    }
802
803
    /**
804
     * Generates a SELECT SQL statement from a {@see Query} object.
805
     *
806
     * @param Query $query the {@see Query} object from which the SQL statement will be generated.
807
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
808
     * in the result with the additional parameters generated during the query building process.
809
     *
810
     * @throws Exception
811
     * @throws InvalidArgumentException
812
     * @throws InvalidConfigException
813
     * @throws NotSupportedException
814
     *
815
     * @return array the generated SQL statement (the first array element) and the corresponding parameters to be bound
816
     * to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
817
     */
818 136
    public function build(Query $query, array $params = []): array
819
    {
820 136
        $query = $query->prepare($this);
821
822 136
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
823
824
        $clauses = [
825 136
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
826 136
            $this->buildFrom($query->getFrom(), $params),
827 136
            $this->buildJoin($query->getJoin(), $params),
828 136
            $this->buildWhere($query->getWhere(), $params),
829 136
            $this->buildGroupBy($query->getGroupBy()),
830 136
            $this->buildHaving($query->getHaving(), $params),
831
        ];
832
833 136
        $sql = implode($this->separator, array_filter($clauses));
834 136
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
835
836 136
        if (!empty($query->getOrderBy())) {
837 5
            foreach ($query->getOrderBy() as $expression) {
838 5
                if ($expression instanceof ExpressionInterface) {
839 1
                    $this->buildExpression($expression, $params);
840
                }
841
            }
842
        }
843
844 136
        if (!empty($query->getGroupBy())) {
845 2
            foreach ($query->getGroupBy() as $expression) {
846 2
                if ($expression instanceof ExpressionInterface) {
847 1
                    $this->buildExpression($expression, $params);
848
                }
849
            }
850
        }
851
852 136
        $union = $this->buildUnion($query->getUnion(), $params);
853
854 136
        if ($union !== '') {
855 3
            $sql = "$sql{$this->separator}$union";
856
        }
857
858 136
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
859
860 136
        if ($with !== '') {
861 2
            $sql = "$with{$this->separator}$sql";
862
        }
863
864 136
        return [$sql, $params];
865
    }
866
867
    /**
868
     * Builds a SQL statement for creating a new index.
869
     *
870
     * @param string $name the name of the index. The name will be properly quoted by the method.
871
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
872
     * the method.
873
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
874
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
875
     * method, unless a parenthesis is found in the name.
876
     * @param bool $unique whether to add UNIQUE constraint on the created index.
877
     *
878
     * @throws Exception
879
     * @throws InvalidArgumentException
880
     * @throws InvalidConfigException
881
     * @throws NotSupportedException
882
     *
883
     * @return string the SQL statement for creating a new index.
884
     */
885 8
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
886
    {
887 8
        $tableParts = explode('.', $table);
888
889 8
        $schema = null;
890 8
        if (count($tableParts) === 2) {
891 1
            [$schema, $table] = $tableParts;
892
        }
893
894 8
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
895 8
            . $this->db->quoteTableName(($schema ? $schema . '.' : '') . $name) . ' ON '
896 8
            . $this->db->quoteTableName($table)
897 8
            . ' (' . $this->buildColumns($columns) . ')';
898
    }
899
900
    /**
901
     * @param array $unions
902
     * @param array $params the binding parameters to be populated.
903
     *
904
     * @throws Exception
905
     * @throws InvalidArgumentException
906
     * @throws InvalidConfigException
907
     * @throws NotSupportedException
908
     *
909
     * @return string the UNION clause built from {@see Query::$union}.
910
     */
911 136
    public function buildUnion(array $unions, array &$params = []): string
912
    {
913 136
        if (empty($unions)) {
914 136
            return '';
915
        }
916
917 3
        $result = '';
918
919 3
        foreach ($unions as $i => $union) {
920 3
            $query = $union['query'];
921 3
            if ($query instanceof Query) {
922 3
                [$unions[$i]['query'], $params] = $this->build($query, $params);
923
            }
924
925 3
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
926
        }
927
928 3
        return trim($result);
929
    }
930
931
    /**
932
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
933
     * constraints), or update them if they do.
934
     *
935
     * For example,
936
     *
937
     * ```php
938
     * $sql = $queryBuilder->upsert('pages', [
939
     *     'name' => 'Front page',
940
     *     'url' => 'http://example.com/', // url is unique
941
     *     'visits' => 0,
942
     * ], [
943
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
944
     * ], $params);
945
     * ```
946
     *
947
     * The method will properly escape the table and column names.
948
     *
949
     * @param string $table the table that new rows will be inserted into/updated in.
950
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
951
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
952
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
953
     * If `true` is passed, the column data will be updated to match the insert column data.
954
     * If `false` is passed, no update will be performed if the column data already exists.
955
     * @param array $params the binding parameters that will be generated by this method.
956
     * They should be bound to the DB command later.
957
     *
958
     * @throws Exception
959
     * @throws InvalidConfigException
960
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
961
     *
962
     * @return string the resulting SQL.
963
     */
964 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
965
    {
966
        /** @var Constraint[] $constraints */
967 18
        $constraints = [];
968
969 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
970 18
            $table,
971
            $insertColumns,
972
            $updateColumns,
973
            $constraints
974
        );
975
976 18
        if (empty($uniqueNames)) {
977 3
            return $this->insert($table, $insertColumns, $params);
978
        }
979
980 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
981
982 15
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
983 15
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
984 15
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
985
986 15
        if ($updateColumns === false) {
987 5
            return $insertSql;
988
        }
989
990 10
        $updateCondition = ['or'];
991 10
        $quotedTableName = $this->db->quoteTableName($table);
992
993 10
        foreach ($constraints as $constraint) {
994 10
            $constraintCondition = ['and'];
995 10
            foreach ($constraint->getColumnNames() as $name) {
996 10
                $quotedName = $this->db->quoteColumnName($name);
997 10
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
998
            }
999 10
            $updateCondition[] = $constraintCondition;
1000
        }
1001
1002 10
        if ($updateColumns === true) {
1003 4
            $updateColumns = [];
1004 4
            foreach ($updateNames as $name) {
1005 4
                $quotedName = $this->db->quoteColumnName($name);
1006
1007 4
                if (\strrpos($quotedName, '.') === false) {
1008 4
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
1009
                }
1010 4
                $updateColumns[$name] = new Expression($quotedName);
1011
            }
1012
        }
1013
1014 10
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
1015 10
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')'
1016 10
            : ltrim($values, ' ')) . ') ' . $this->update($table, $updateColumns, $updateCondition, $params);
1017
1018 10
        return "$updateSql; $insertSql;";
1019
    }
1020
1021 2
    private function unquoteTableName(string $tableName): string
1022
    {
1023 2
        return $this->db->getSchema()->unquoteSimpleTableName($this->db->quoteSql($tableName));
1024
    }
1025
1026 2
    private function getFieldDefinitionsTokens($tableName)
1027
    {
1028 2
        $create_table = $this->getCreateTable($tableName);
1029
1030
        /** Parse de CREATE TABLE statement to skip any use of this column, namely field definitions and FOREIGN KEYS */
1031 2
        $code = (new SqlTokenizer($create_table))->tokenize();
1032 2
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
1033 2
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Bug introduced by
The method matches() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1033
        if (!$code[0]->/** @scrutinizer ignore-call */ matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1034
            throw new InvalidParamException("Table not found: $tableName");
1035
        }
1036
1037
        /** Get the fields definition and foreign keys tokens */
1038 2
        return $code[0][$lastMatchIndex - 1];
1039
    }
1040
1041 2
    private function getCreateTable($tableName)
1042
    {
1043 2
        if (($pos = strpos($tableName, '.')) !== false) {
1044
            $schema = substr($tableName, 0, $pos + 1);
1045
            $tableName = substr($tableName, $pos + 1);
1046
        } else {
1047 2
            $schema = '';
1048
        }
1049
1050 2
        $create_table = $this->db->createCommand(
1051 2
            "select SQL from {$schema}SQLite_Master where tbl_name = '$tableName' and type='table'"
1052 2
        )->queryScalar();
1053
1054 2
        if ($create_table === null) {
1055
            throw new InvalidParamException("Table not found: $tableName");
1056
        }
1057
1058 2
        return trim($create_table);
0 ignored issues
show
Bug introduced by
It seems like $create_table can also be of type false; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1058
        return trim(/** @scrutinizer ignore-type */ $create_table);
Loading history...
1059
    }
1060
1061 2
    private function foreignKeysState()
1062
    {
1063 2
        return $this->db->createCommand("PRAGMA foreign_keys")->queryScalar();
1064
    }
1065
1066 2
    private function getIndexSqls(string $tableName, $skipColumn = null, $newColumn = null): array
1067
    {
1068
        /** Get all indexes on this table */
1069 2
        $indexes = $this->db->createCommand(
1070 2
            "select SQL from SQLite_Master where tbl_name = '$tableName' and type='index'"
1071 2
        )->queryAll();
1072
1073 2
        if ($skipColumn === null) {
1074 2
            return array_column($indexes, "sql");
1075
        }
1076
1077
        $quoted_skip_column = $this->db->quoteColumnName((string) $skipColumn);
1078
        if ($newColumn === null) {
1079
            /** Skip indexes which contain this column */
1080
            foreach ($indexes as $key => $index) {
1081
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1082
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any()'))->tokenize();
1083
1084
                /** Extract the list of fields of this index */
1085
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1086
                    throw new InvalidParamException("Index definition error: $index");
1087
                }
1088
1089
                $found = false;
1090
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1091
                $offset = 0;
1092
                while ($indexFieldsDef->offsetExists($offset)) {
0 ignored issues
show
Bug introduced by
The method offsetExists() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1092
                while ($indexFieldsDef->/** @scrutinizer ignore-call */ offsetExists($offset)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1093
                    $token = $indexFieldsDef[$offset];
1094
                    $tokenType = $token->getType();
1095
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1096
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1097
                            $found = true;
1098
                            unset($indexes[$key]);
1099
                            break;
1100
                        }
1101
                    }
1102
                    ++$offset;
1103
                }
1104
1105
                if (!$found) {
1106
                    /** If the index contains this column, do not add it */
1107
                    $indexes[$key] = $index["sql"];
1108
                }
1109
            }
1110
        } else {
1111
            foreach ($indexes as $key => $index) {
1112
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1113
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1114
1115
                /** Extract the list of fields of this index */
1116
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1117
                    throw new InvalidParamException("Index definition error: $index");
1118
                }
1119
1120
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1121
                $new_index_def = '';
1122
1123
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1124
                    $new_index_def .= $code[0][$i] . " ";
1125
                }
1126
1127
                $offset = 0;
1128
                while ($indexFieldsDef->offsetExists($offset)) {
1129
                    $token = $indexFieldsDef[$offset];
1130
                    $tokenType = $token->getType();
1131
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1132
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1133
                            $token = $this->db->quoteColumnName((string) $newColumn);
1134
                        }
1135
                    }
1136
                    $new_index_def .= $token;
1137
                    ++$offset;
1138
                }
1139
1140
                while ($code[0]->offsetExists($lastMatchIndex)) {
1141
                    $new_index_def .= $code[0][$lastMatchIndex++] . " ";
1142
                }
1143
1144
                $indexes[$key] = $this->dropIndex((string) $code[0][2], $tableName) . ";$new_index_def";
1145
            }
1146
        }
1147
1148
        return $indexes;
1149
    }
1150
}
1151