Passed
Pull Request — master (#17)
by Wilmer
14:53
created

QueryBuilder::build()   B

Complexity

Conditions 10
Paths 32

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 26
nc 32
nop 2
dl 0
loc 47
ccs 17
cts 17
cp 1
crap 10
rs 7.6666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
class QueryBuilder extends BaseQueryBuilder
28
{
29
    /**
30
     * @var array mapping from abstract column types (keys) to physical column types (values).
31
     */
32
    protected array $typeMap = [
33
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
34
        Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
35
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
36
        Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
37
        Schema::TYPE_CHAR => 'char(1)',
38
        Schema::TYPE_STRING => 'varchar(255)',
39
        Schema::TYPE_TEXT => 'text',
40
        Schema::TYPE_TINYINT => 'tinyint',
41
        Schema::TYPE_SMALLINT => 'smallint',
42
        Schema::TYPE_INTEGER => 'integer',
43
        Schema::TYPE_BIGINT => 'bigint',
44
        Schema::TYPE_FLOAT => 'float',
45
        Schema::TYPE_DOUBLE => 'double',
46
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
47
        Schema::TYPE_DATETIME => 'datetime',
48
        Schema::TYPE_TIMESTAMP => 'timestamp',
49
        Schema::TYPE_TIME => 'time',
50
        Schema::TYPE_DATE => 'date',
51
        Schema::TYPE_BINARY => 'blob',
52
        Schema::TYPE_BOOLEAN => 'boolean',
53
        Schema::TYPE_MONEY => 'decimal(19,4)',
54
    ];
55
56
    /**
57
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
58
     * expression builders for this query builder.
59
     *
60
     * @return array
61 192
     *
62
     * See {@see \Yiisoft\Db\Expression\ExpressionBuilder} docs for details.
63 192
     */
64 192
    protected function defaultExpressionBuilders(): array
65
    {
66
        return array_merge(parent::defaultExpressionBuilders(), [
67
            LikeCondition::class => LikeConditionBuilder::class,
68
            InCondition::class => InConditionBuilder::class,
69
        ]);
70
    }
71
72
    /**
73
     * Generates a batch INSERT SQL statement.
74
     *
75
     * For example,
76
     *
77
     * ```php
78
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
79
     *     ['Tom', 30],
80
     *     ['Jane', 20],
81
     *     ['Linda', 25],
82
     * ])->execute();
83
     * ```
84
     *
85
     * Note that the values in each row must match the corresponding column names.
86
     *
87
     * @param string $table the table that new rows will be inserted into.
88
     * @param array $columns the column names
89
     * @param array|\Generator $rows the rows to be batch inserted into the table
90
     * @param array $params
91
     *
92
     * @throws Exception
93
     * @throws InvalidConfigException
94
     * @throws InvalidArgumentException
95
     * @throws NotSupportedException
96 13
     *
97
     * @return string the batch INSERT SQL statement.
98 13
     */
99 2
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
100
    {
101
        if (empty($rows)) {
102
            return '';
103
        }
104
105
        /**
106
         * SQLite supports batch insert natively since 3.7.11.
107 12
         *
108
         * {@see http://www.sqlite.org/releaselog/3_7_11.html}
109 12
         */
110 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

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

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

409
            /** @scrutinizer ignore-call */ 
410
            $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...
410
            if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
411
                $identifier = (string) $token;
412
                $sql_fields_to_insert[] = $identifier;
413
            } elseif ($tokenType === SqlToken::TYPE_KEYWORD) {
414
                $keyword = (string) $token;
415
                if ($keyword === 'CONSTRAINT' || $keyword === 'FOREIGN') {
416
                    /** Constraint key found */
417
                    $other_offset = $offset;
418
                    if ($keyword === 'CONSTRAINT') {
419
                        $constraint_name = (string) $fields_definitions_tokens[$other_offset];
420
                    } else {
421
                        $constraint_name = $this->db->quoteColumnName((string) $constraint_pos);
422
                    }
423
                    if (($constraint_name === $quoted_foreign_name) || (is_int($name) && $constraint_pos === $name)) {
424
                        /** Found foreign key $name, skip it */
425
                        $foreign_found = true;
426
                        $skipping = true;
427
                        $offset = $other_offset;
428
                    }
429
                    $constraint_pos++;
430
                }
431
            } else {
432
                throw new NotSupportedException("Unexpected: $token");
433
            }
434
435
            if (!$skipping) {
436
                $ddl_fields_def .= $token . " ";
437
            }
438
439
            /** Skip or keep until the next */
440
            while ($fields_definitions_tokens->offsetExists($offset)) {
441
                $skip_token = $fields_definitions_tokens[$offset];
442
                if (!$skipping) {
443
                    $ddl_fields_def .= (string)$skip_token . " ";
444
                }
445
                $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

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

775
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
776
            if ($this->hasOffset($offset)) {
777 18
                $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

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

1032
        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...
1033
            throw new InvalidParamException("Table not found: $tableName");
1034
        }
1035
1036
        /** Get the fields definition and foreign keys tokens */
1037
        return $code[0][$lastMatchIndex - 1];
1038
    }
1039
1040
    private function getCreateTable($tableName)
1041
    {
1042
        if (($pos = strpos($tableName, '.')) !== false) {
1043
            $schema = substr($tableName, 0, $pos + 1);
1044
            $tableName = substr($tableName, $pos + 1);
1045
        } else {
1046
            $schema = '';
1047
        }
1048
1049
        $create_table = $this->db->createCommand(
1050
            "select SQL from {$schema}SQLite_Master where tbl_name = '$tableName' and type='table'"
1051
        )->queryScalar();
1052
1053
        if ($create_table === null) {
1054
            throw new InvalidParamException("Table not found: $tableName");
1055
        }
1056
1057
        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

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

1091
                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...
1092
                    $token = $indexFieldsDef[$offset];
1093
                    $tokenType = $token->getType();
1094
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1095
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1096
                            $found = true;
1097
                            unset($indexes[$key]);
1098
                            break;
1099
                        }
1100
                    }
1101
                    ++$offset;
1102
                }
1103
1104
                if (!$found) {
1105
                    /** If the index contains this column, do not add it */
1106
                    $indexes[$key] = $index["sql"];
1107
                }
1108
            }
1109
        } else {
1110
            foreach ($indexes as $key => $index) {
1111
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1112
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1113
1114
                /** Extract the list of fields of this index */
1115
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1116
                    throw new InvalidParamException("Index definition error: $index");
1117
                }
1118
1119
                $found = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $found is dead and can be removed.
Loading history...
1120
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1121
                $new_index_def = '';
1122
1123
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1124
                    $new_index_def .= (string)$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 .= (string)$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