Passed
Pull Request — master (#33)
by Wilmer
15:02
created

QueryBuilder::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Generator;
8
use JsonException;
9
use Throwable;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\InvalidParamException;
16
use Yiisoft\Db\Exception\NotSupportedException;
17
use Yiisoft\Db\Expression\Expression;
18
use Yiisoft\Db\Expression\ExpressionBuilder;
19
use Yiisoft\Db\Expression\ExpressionInterface;
20
use Yiisoft\Db\Query\Conditions\InCondition;
21
use Yiisoft\Db\Query\Conditions\LikeCondition;
22
use Yiisoft\Db\Query\Query;
23
use Yiisoft\Db\Query\QueryBuilder as BaseQueryBuilder;
24
use Yiisoft\Db\Sqlite\Condition\InConditionBuilder;
25
use Yiisoft\Db\Sqlite\Condition\LikeConditionBuilder;
26
use Yiisoft\Strings\NumericHelper;
27
28
use function array_column;
29
use function array_filter;
30
use function array_merge;
31
use function implode;
32
use function is_float;
33
use function is_string;
34
use function ltrim;
35
use function reset;
36
use function strpos;
37
use function strrpos;
38
use function substr;
39
use function trim;
40
use function version_compare;
41
42
final class QueryBuilder extends BaseQueryBuilder
43
{
44
    /**
45
     * @var array mapping from abstract column types (keys) to physical column types (values).
46
     */
47
    protected array $typeMap = [
48
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
49
        Schema::TYPE_UPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
50
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
51
        Schema::TYPE_UBIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
52
        Schema::TYPE_CHAR => 'char(1)',
53
        Schema::TYPE_STRING => 'varchar(255)',
54
        Schema::TYPE_TEXT => 'text',
55
        Schema::TYPE_TINYINT => 'tinyint',
56
        Schema::TYPE_SMALLINT => 'smallint',
57
        Schema::TYPE_INTEGER => 'integer',
58
        Schema::TYPE_BIGINT => 'bigint',
59
        Schema::TYPE_FLOAT => 'float',
60
        Schema::TYPE_DOUBLE => 'double',
61
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
62
        Schema::TYPE_DATETIME => 'datetime',
63
        Schema::TYPE_TIMESTAMP => 'timestamp',
64
        Schema::TYPE_TIME => 'time',
65
        Schema::TYPE_DATE => 'date',
66
        Schema::TYPE_BINARY => 'blob',
67
        Schema::TYPE_BOOLEAN => 'boolean',
68
        Schema::TYPE_MONEY => 'decimal(19,4)',
69
    ];
70
71
    /** @psalm-var Connection $db */
72
    private ConnectionInterface $db;
73
74
    public function __construct(ConnectionInterface $db)
75
    {
76 235
        $this->db = $db;
77
78 235
        parent::__construct($db);
79 235
    }
80
81
    /**
82
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
83
     * expression builders for this query builder.
84
     *
85
     * @return array
86
     *
87
     * See {@see ExpressionBuilder} docs for details.
88
     */
89
    protected function defaultExpressionBuilders(): array
90
    {
91
        return array_merge(parent::defaultExpressionBuilders(), [
92
            LikeCondition::class => LikeConditionBuilder::class,
93
            InCondition::class => InConditionBuilder::class,
94
        ]);
95
    }
96
97
    /**
98
     * Generates a batch INSERT SQL statement.
99
     *
100
     * For example,
101
     *
102
     * ```php
103
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
104
     *     ['Tom', 30],
105
     *     ['Jane', 20],
106
     *     ['Linda', 25],
107
     * ])->execute();
108
     * ```
109
     *
110
     * Note that the values in each row must match the corresponding column names.
111 13
     *
112
     * @param string $table the table that new rows will be inserted into.
113 13
     * @param array $columns the column names
114 2
     * @param array|Generator $rows the rows to be batch inserted into the table
115
     * @param array $params
116
     *
117
     * @throws Exception|InvalidConfigException|InvalidArgumentException|NotSupportedException
118
     *
119
     * @return string the batch INSERT SQL statement.
120
     *
121
     */
122 12
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
123
    {
124 12
        if (empty($rows)) {
125 12
            return '';
126
        }
127
128
        /**
129
         * SQLite supports batch insert natively since 3.7.11.
130
         *
131
         * {@see http://www.sqlite.org/releaselog/3_7_11.html}
132
         */
133
        $this->db->open();
134
135
        if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
136
            return parent::batchInsert($table, $columns, $rows, $params);
137
        }
138
139
        $schema = $this->db->getSchema();
140
141
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
142
            $columnSchemas = $tableSchema->getColumns();
143
        } else {
144
            $columnSchemas = [];
145
        }
146
147
        $values = [];
148
149
        foreach ($rows as $row) {
150
            $vs = [];
151
            foreach ($row as $i => $value) {
152
                if (isset($columnSchemas[$columns[$i]])) {
153
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
154
                }
155
                if (is_string($value)) {
156
                    $value = $schema->quoteValue($value);
157
                } elseif (is_float($value)) {
158
                    /** ensure type cast always has . as decimal separator in all locales */
159
                    $value = NumericHelper::normalize($value);
160
                } elseif ($value === false) {
161
                    $value = 0;
162
                } elseif ($value === null) {
163
                    $value = 'NULL';
164
                } elseif ($value instanceof ExpressionInterface) {
165
                    $value = $this->buildExpression($value, $params);
166
                }
167
                $vs[] = $value;
168
            }
169
            $values[] = implode(', ', $vs);
170
        }
171
172
        if (empty($values)) {
173
            return '';
174
        }
175
176
        foreach ($columns as $i => $name) {
177
            $columns[$i] = $schema->quoteColumnName($name);
178
        }
179
180
        return 'INSERT INTO ' . $schema->quoteTableName($table)
181
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
182
    }
183
184
    /**
185
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
186
     *
187
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
188
     * or 1.
189
     *
190
     * @param string $tableName the name of the table whose primary key sequence will be reset.
191 1
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
192
     * row's primary key will have a value 1.
193 1
     *
194
     * @throws Exception|Throwable|InvalidArgumentException if the table does not exist or there is no sequence
195 1
     * associated with the table.
196
     *
197 1
     * @return string the SQL statement for resetting sequence.
198 1
     */
199 1
    public function resetSequence(string $tableName, $value = null): string
200 1
    {
201 1
        $db = $this->db;
202 1
203 1
        $table = $db->getTableSchema($tableName);
204 1
205
        if ($table !== null && $table->getSequenceName() !== null) {
206 1
            $tableName = $db->quoteTableName($tableName);
207
            if ($value === null) {
208
                $pk = $table->getPrimaryKey();
209 1
                $key = $this->db->quoteColumnName(reset($pk));
210
                $value = $this->db->useMaster(static function (Connection $db) use ($key, $tableName) {
211
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
212
                });
213
            } else {
214
                $value = (int) $value - 1;
215
            }
216
217
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
218
        }
219
220
        if ($table === null) {
221
            throw new InvalidArgumentException("Table not found: $tableName");
222
        }
223
224
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
225
    }
226
227
    /**
228
     * Enables or disables integrity check.
229
     *
230
     * @param bool $check whether to turn on or off the integrity check.
231
     * @param string $schema the schema of the tables. Meaningless for SQLite.
232
     * @param string $table the table name. Meaningless for SQLite.
233
     *
234
     * @return string the SQL statement for checking integrity.
235
     */
236
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
237
    {
238
        return 'PRAGMA foreign_keys=' . (int) $check;
239
    }
240
241
    /**
242 1
     * Builds a SQL statement for truncating a DB table.
243
     *
244 1
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
245
     *
246
     * @return string the SQL statement for truncating a DB table.
247
     */
248
    public function truncateTable(string $table): string
249
    {
250
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
251
    }
252
253
    /**
254
     * Builds a SQL statement for dropping an index.
255
     *
256
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
257
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
258
     *
259 2
     * @return string the SQL statement for dropping an index.
260
     */
261 2
    public function dropIndex(string $name, string $table): string
262
    {
263
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
264
    }
265
266
    /**
267
     * Builds a SQL statement for dropping a DB column.
268
     *
269
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
270
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
271
     *
272
     * @throws NotSupportedException this is not supported by SQLite.
273
     *
274
     * @return string the SQL statement for dropping a DB column.
275
     */
276
    public function dropColumn(string $table, string $column): string
277
    {
278
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
279
    }
280
281
    /**
282
     * Builds a SQL statement for renaming a column.
283
     *
284
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
285
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
286
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
287
     *
288
     * @throws NotSupportedException this is not supported by SQLite.
289
     *
290
     * @return string the SQL statement for renaming a DB column.
291
     */
292
    public function renameColumn(string $table, string $oldName, string $newName): string
293
    {
294
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
295
    }
296
297
    /**
298
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
299
     *
300
     * The method will properly quote the table and column names.
301
     *
302
     * @param string $name the name of the foreign key constraint.
303
     * @param string $table the table that the foreign key constraint will be added to.
304
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
305
     * multiple columns, separate them with commas or use an array to represent them.
306
     * @param string $refTable the table that the foreign key references to.
307
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
308
     * columns, separate them with commas or use an array to represent them.
309
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
310
     * SET DEFAULT, SET NULL.
311
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
312
     * SET DEFAULT, SET NULL.
313
     *
314
     * @throws Exception|InvalidParamException
315
     *
316
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
317
     */
318
    public function addForeignKey(
319 4
        string $name,
320
        string $table,
321
        $columns,
322
        string $refTable,
323
        $refColumns,
324
        ?string $delete = null,
325
        ?string $update = null
326
    ): string {
327
        $schema = $refschema = '';
328 4
329
        if (($pos = strpos($table, '.')) !== false) {
330 4
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
331
            $table = substr($table, $pos + 1);
332
        }
333
334
        if (($pos_ref = strpos($refTable, '.')) !== false) {
335 4
            $refschema = substr($refTable, 0, $pos_ref);
336
            $refTable = substr($refTable, $pos_ref + 1);
337
        }
338
339
        if (($schema !== '' || ($refschema !== '' && $schema !== $refschema))) {
340 4
            return '' ;
341
        }
342
343
        /** @psalm-suppress TypeDoesNotContainType */
344 4
        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 4
            $unquoted_tablename = $this->unquoteTableName($table);
351 4
            $quoted_tablename = $this->db->quoteTableName($table);
352 4
            $tmp_table_name =  "temp_" . $this->unquoteTableName($table);
353
        }
354
355 4
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
356 4
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
357 4
        $ddl_fields_defs .= ",\nCONSTRAINT " . $this->db->quoteColumnName($name) . " FOREIGN KEY (" .
358 4
            implode(",", (array)$columns) . ") REFERENCES $refTable(" . implode(",", (array)$refColumns) . ")";
359
360 4
        if ($update !== null) {
361 2
            $ddl_fields_defs .= " ON UPDATE $update";
362
        }
363
364 4
        if ($delete !== null) {
365 2
            $ddl_fields_defs .= " ON DELETE $delete";
366
        }
367
368 4
        $foreign_keys_state = $this->foreignKeysState();
369 4
        $return_queries = [];
370 4
        $return_queries[] = "PRAGMA foreign_keys = off";
371 4
        $return_queries[] = "SAVEPOINT add_foreign_key_to_$tmp_table_name";
372 4
        $return_queries[] = "CREATE TEMP TABLE " . $this->db->quoteTableName($tmp_table_name)
373 4
            . " AS SELECT * FROM $quoted_tablename";
374 4
        $return_queries[] = "DROP TABLE $quoted_tablename";
375 4
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ")";
376 4
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->db->quoteTableName($tmp_table_name);
377 4
        $return_queries[] = "DROP TABLE " . $this->db->quoteTableName($tmp_table_name);
378 4
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
379
380 4
        $return_queries[] = "RELEASE add_foreign_key_to_$tmp_table_name";
381 4
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
382
383 4
        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 $table
392
     *
393
     * @throws Exception|InvalidParamException|NotSupportedException
394
     *
395
     * @return string the SQL statement for dropping a foreign key constraint.
396
     */
397
    public function dropForeignKey(string $name, string $table): string
398
    {
399
        $return_queries = [];
400 2
        $ddl_fields_def = '';
401
        $sql_fields_to_insert = [];
402 2
        $skipping = false;
403 2
        $foreign_found = false;
404 2
        $quoted_foreign_name = $this->db->quoteColumnName($name);
405 2
406 2
        $quoted_tablename = $this->db->quoteTableName($table);
407 2
        $unquoted_tablename = $this->unquoteTableName($table);
408
409 2
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
410 2
411
        $offset = 0;
412 2
        $constraint_pos = 0;
413
414 2
        /** Traverse the tokens looking for either an identifier (field name) or a foreign key */
415 2
        while ($fields_definitions_tokens->offsetExists($offset)) {
416
            $token = $fields_definitions_tokens[$offset++];
417
418 2
            /**
419 2
             * These searchs could be done with another SqlTokenizer, but I don't konw how to do them, the documentation
420
             * for sqltokenizer si really scarse.
421
             */
422
            $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

422
            /** @scrutinizer ignore-call */ 
423
            $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...
423
424
            if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
425 2
                $identifier = (string) $token;
426
                $sql_fields_to_insert[] = $identifier;
427 2
            } elseif ($tokenType === SqlToken::TYPE_KEYWORD) {
428 2
                $keyword = (string) $token;
429 2
430 2
                if ($keyword === 'CONSTRAINT' || $keyword === 'FOREIGN') {
431 2
                    /** Constraint key found */
432
                    $other_offset = $offset;
433 2
434
                    if ($keyword === 'CONSTRAINT') {
435 2
                        $constraint_name = $this->db->quoteColumnName(
436
                            $fields_definitions_tokens[$other_offset]->getContent()
0 ignored issues
show
Bug introduced by
The method getContent() 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

436
                            $fields_definitions_tokens[$other_offset]->/** @scrutinizer ignore-call */ 
437
                                                                       getContent()

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...
437 2
                        );
438 2
                    } else {
439 2
                        $constraint_name = $this->db->quoteColumnName((string) $constraint_pos);
440
                    }
441
442
                    /** @psalm-suppress TypeDoesNotContainType */
443
                    if (($constraint_name === $quoted_foreign_name) || (is_int($name) && $constraint_pos === $name)) {
444
                        /** Found foreign key $name, skip it */
445 2
                        $foreign_found = true;
446
                        $skipping = true;
447 2
                        $offset = $other_offset;
448 2
                    }
449 2
                    $constraint_pos++;
450
                }
451 2
            } else {
452
                throw new NotSupportedException("Unexpected: $token");
453
            }
454
455
            if (!$skipping) {
456
                $ddl_fields_def .= $token . " ";
457 2
            }
458 2
459
            /** Skip or keep until the next */
460
            while ($fields_definitions_tokens->offsetExists($offset)) {
461
                $skip_token = $fields_definitions_tokens[$offset];
462 2
                $skip_next = $fields_definitions_tokens[$offset + 1];
463 2
464 2
                if (!$skipping) {
465
                    $ddl_fields_def .= (string) $skip_token . ($skip_next == ',' ? '' : ' ');
466 2
                }
467 2
468
                $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

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

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

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

1017
        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...
1018 4
            throw new InvalidParamException("Table not found: $tableName");
1019 4
        }
1020
1021 4
        /** Get the fields definition and foreign keys tokens */
1022 4
        return $code[0][$lastMatchIndex - 1];
1023
    }
1024 4
1025
    private function getCreateTable(string $tableName): string
1026
    {
1027
        if (($pos = strpos($tableName, '.')) !== false) {
1028 10
            $schema = substr($tableName, 0, $pos + 1);
1029 10
            $tableName = substr($tableName, $pos + 1);
1030 10
        } else {
1031
            $schema = '';
1032 10
        }
1033
1034
        $create_table = $this->db->createCommand(
1035 5
            "select SQL from {$schema}SQLite_Master where tbl_name = '$tableName' and type='table'"
1036
        )->queryScalar();
1037 5
1038
        if ($create_table === null) {
1039
            throw new InvalidParamException("Table not found: $tableName");
1040 5
        }
1041
1042 5
        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

1042
        return trim(/** @scrutinizer ignore-type */ $create_table);
Loading history...
1043
    }
1044
1045 5
    private function foreignKeysState()
1046 5
    {
1047 5
        return $this->db->createCommand("PRAGMA foreign_keys")->queryScalar();
1048
    }
1049
1050
    private function getIndexSqls(string $tableName, $skipColumn = null, $newColumn = null): array
1051
    {
1052 5
        /** Get all indexes on this table */
1053
        $indexes = $this->db->createCommand(
1054
            "select SQL from SQLite_Master where tbl_name = '$tableName' and type='index'"
1055 5
        )->queryAll();
1056
1057 5
        if ($skipColumn === null) {
1058
            return array_column($indexes, "sql");
1059
        }
1060
1061 5
        $quoted_skip_column = $this->db->quoteColumnName((string) $skipColumn);
1062
        if ($newColumn === null) {
1063
            /** Skip indexes which contain this column */
1064 5
            foreach ($indexes as $key => $index) {
1065 5
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1066 5
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any()'))->tokenize();
1067
1068 5
                /** Extract the list of fields of this index */
1069
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1070
                    throw new InvalidParamException("Index definition error: $index");
1071
                }
1072 5
1073
                $found = false;
1074
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1075 5
                $offset = 0;
1076
                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

1076
                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...
1077 5
                    $token = $indexFieldsDef[$offset];
1078
                    $tokenType = $token->getType();
1079
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1080 5
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1081
                            $found = true;
1082
                            unset($indexes[$key]);
1083 5
                            break;
1084 5
                        }
1085 5
                    }
1086
                    ++$offset;
1087 5
                }
1088 5
1089
                if (!$found) {
1090
                    /** If the index contains this column, do not add it */
1091
                    $indexes[$key] = $index["sql"];
1092
                }
1093
            }
1094
        } else {
1095
            foreach ($indexes as $key => $index) {
1096
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1097
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1098
1099
                /** Extract the list of fields of this index */
1100
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1101
                    throw new InvalidParamException("Index definition error: $index");
1102
                }
1103
1104
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1105
                $new_index_def = '';
1106
1107
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1108
                    $new_index_def .= $code[0][$i] . " ";
1109
                }
1110
1111
                $offset = 0;
1112
                while ($indexFieldsDef->offsetExists($offset)) {
1113
                    $token = $indexFieldsDef[$offset];
1114
                    $tokenType = $token->getType();
1115
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1116
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1117
                            $token = $this->db->quoteColumnName((string) $newColumn);
1118
                        }
1119
                    }
1120
                    $new_index_def .= $token;
1121
                    ++$offset;
1122
                }
1123
1124
                while ($code[0]->offsetExists($lastMatchIndex)) {
1125
                    $new_index_def .= $code[0][$lastMatchIndex++] . " ";
1126
                }
1127
1128
                $indexes[$key] = $this->dropIndex((string) $code[0][2], $tableName) . ";$new_index_def";
1129
            }
1130
        }
1131
1132
        return $indexes;
1133
    }
1134
}
1135