Passed
Push — master ( b65938...b07d24 )
by Wilmer
11:20 queued 09:43
created

QueryBuilder::dropForeignKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql\Query;
6
7
use Yiisoft\Db\Exception\Exception;
8
use Yiisoft\Db\Exception\InvalidArgumentException;
9
use Yiisoft\Db\Exception\InvalidConfigException;
10
use Yiisoft\Db\Exception\NotSupportedException;
11
use Yiisoft\Db\Expression\Expression;
12
use Yiisoft\Db\Expression\JsonExpression;
13
use Yiisoft\Db\Mysql\Expression\JsonExpressionBuilder;
14
use Yiisoft\Db\Mysql\Schema\Schema;
15
use Yiisoft\Db\Query\Query;
16
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
17
18
class QueryBuilder extends AbstractQueryBuilder
19
{
20
    /**
21
     * @var array mapping from abstract column types (keys) to physical column types (values).
22
     */
23
    protected array $typeMap = [
24
        Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
25
        Schema::TYPE_UPK => 'int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
26
        Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY',
27
        Schema::TYPE_UBIGPK => 'bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
28
        Schema::TYPE_CHAR => 'char(1)',
29
        Schema::TYPE_STRING => 'varchar(255)',
30
        Schema::TYPE_TEXT => 'text',
31
        Schema::TYPE_TINYINT => 'tinyint(3)',
32
        Schema::TYPE_SMALLINT => 'smallint(6)',
33
        Schema::TYPE_INTEGER => 'int(11)',
34
        Schema::TYPE_BIGINT => 'bigint(20)',
35
        Schema::TYPE_FLOAT => 'float',
36
        Schema::TYPE_DOUBLE => 'double',
37
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
38
        Schema::TYPE_DATETIME => 'datetime',
39
        Schema::TYPE_TIMESTAMP => 'timestamp',
40
        Schema::TYPE_TIME => 'time',
41
        Schema::TYPE_DATE => 'date',
42
        Schema::TYPE_BINARY => 'blob',
43
        Schema::TYPE_BOOLEAN => 'tinyint(1)',
44
        Schema::TYPE_MONEY => 'decimal(19,4)',
45
        Schema::TYPE_JSON => 'json',
46
    ];
47
48
    /**
49
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
50
     * expression builders for this query builder.
51
     *
52
     * @return array
53
     *
54
     * See {@see \Yiisoft\Db\Expression\ExpressionBuilder} docs for details.
55
     */
56 228
    protected function defaultExpressionBuilders(): array
57
    {
58 228
        return \array_merge(parent::defaultExpressionBuilders(), [
59 228
            JsonExpression::class => JsonExpressionBuilder::class,
60
        ]);
61
    }
62
63
    /**
64
     * Builds a SQL statement for renaming a column.
65
     *
66
     * @param string $table   the table whose column is to be renamed. The name will be properly quoted by the method.
67
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
68
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
69
     *
70
     * @throws Exception
71
     *
72
     * @return string the SQL statement for renaming a DB column.
73
     */
74
    public function renameColumn(string $table, string $oldName, string $newName): string
75
    {
76
        $quotedTable = $this->db->quoteTableName($table);
0 ignored issues
show
Bug introduced by
The method quoteTableName() 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

76
        /** @scrutinizer ignore-call */ 
77
        $quotedTable = $this->db->quoteTableName($table);

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...
77
78
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
79
80
        if ($row === false) {
81
            throw new Exception("Unable to find column '$oldName' in table '$table'.");
82
        }
83
84
        if (isset($row['Create Table'])) {
85
            $sql = $row['Create Table'];
86
        } else {
87
            $row = \array_values($row);
88
            $sql = $row[1];
89
        }
90
91
        if (\preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
92
            foreach ($matches[1] as $i => $c) {
93
                if ($c === $oldName) {
94
                    return "ALTER TABLE $quotedTable CHANGE "
95
                        . $this->db->quoteColumnName($oldName) . ' '
96
                        . $this->db->quoteColumnName($newName) . ' '
97
                        . $matches[2][$i];
98
                }
99
            }
100
        }
101
102
        // try to give back a SQL anyway
103
        return "ALTER TABLE $quotedTable CHANGE "
104
            . $this->db->quoteColumnName($oldName) . ' '
105
            . $this->db->quoteColumnName($newName);
106
    }
107
108
    /**
109
     * Builds a SQL statement for creating a new index.
110
     *
111
     * @param string $name the name of the index. The name will be properly quoted by the method.
112
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
113
     * the method.
114
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
115
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
116
     * method, unless a parenthesis is found in the name.
117
     * @param bool $unique whether to add UNIQUE constraint on the created index.
118
     *
119
     * @throws Exception
120
     * @throws InvalidArgumentException
121
     * @throws InvalidConfigException
122
     * @throws NotSupportedException
123
     *
124
     * @return string the SQL statement for creating a new index.
125
     *
126
     * {@see https://bugs.mysql.com/bug.php?id=48875}
127
     */
128 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
129
    {
130
        return 'ALTER TABLE '
131 6
            . $this->db->quoteTableName($table)
132 6
            . ($unique ? ' ADD UNIQUE INDEX ' : ' ADD INDEX ')
133 6
            . $this->db->quoteTableName($name)
134 6
            . ' (' . $this->buildColumns($columns) . ')';
135
    }
136
137
    /**
138
     * Builds a SQL statement for dropping a foreign key constraint.
139
     *
140
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the
141
     * method.
142
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
143
     *
144
     * @throws Exception
145
     * @throws InvalidConfigException
146
     * @throws NotSupportedException
147
     *
148
     * @return string the SQL statement for dropping a foreign key constraint.
149
     */
150 3
    public function dropForeignKey(string $name, string $table): string
151
    {
152
        return 'ALTER TABLE '
153 3
            . $this->db->quoteTableName($table)
154 3
            . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name);
155
    }
156
157
    /**
158
     * Builds a SQL statement for removing a primary key constraint to an existing table.
159
     *
160
     * @param string $name the name of the primary key constraint to be removed.
161
     * @param string $table the table that the primary key constraint will be removed from.
162
     *
163
     * @throws Exception
164
     * @throws InvalidConfigException
165
     * @throws NotSupportedException
166
     *
167
     * @return string the SQL statement for removing a primary key constraint from an existing table.
168
     */
169 2
    public function dropPrimaryKey(string $name, string $table): string
170
    {
171
        return 'ALTER TABLE '
172 2
            . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY';
173
    }
174
175
    /**
176
     * Creates a SQL command for dropping an unique constraint.
177
     *
178
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
179
     * method.
180
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
181
     * method.
182
     *
183
     * @throws Exception
184
     * @throws InvalidConfigException
185
     * @throws NotSupportedException
186
     *
187
     * @return string the SQL statement for dropping an unique constraint.
188
     */
189 2
    public function dropUnique(string $name, string $table): string
190
    {
191 2
        return $this->dropIndex($name, $table);
192
    }
193
194
    /**
195
     * @param string $name
196
     * @param string $table
197
     * @param string $expression
198
     *
199
     * @throws NotSupportedException Method not supported by Mysql.
200
     *
201
     * @return string the SQL statement for adding a check constraint to an existing table.
202
     */
203
    public function addCheck(string $name, string $table, string $expression): string
204
    {
205
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
206
    }
207
208
    /**
209
     * @param string $name
210
     * @param string $table
211
     *
212
     * @throws NotSupportedException Method not supported by Mysql.
213
     *
214
     * @return string the SQL statement for adding a check constraint to an existing table.
215
     */
216
    public function dropCheck(string $name, string $table): string
217
    {
218
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
219
    }
220
221
    /**
222
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
223
     *
224
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
225
     * or 1.
226
     *
227
     * @param string $tableName the name of the table whose primary key sequence will be reset.
228
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
229
     * row's primary key will have a value 1.
230
     *
231
     * @throws Exception
232
     * @throws InvalidArgumentException
233
     * @throws InvalidConfigException
234
     * @throws NotSupportedException
235
     *
236
     * @return string the SQL statement for resetting sequence.
237
     */
238 1
    public function resetSequence(string $tableName, $value = null): string
239
    {
240 1
        $table = $this->db->getTableSchema($tableName);
241
242 1
        if ($table !== null && $table->getSequenceName() !== null) {
243 1
            $tableName = $this->db->quoteTableName($tableName);
244
245 1
            if ($value === null) {
246 1
                $pk = $table->getPrimaryKey();
247 1
                $key = \reset($pk);
248 1
                $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1;
249
            } else {
250 1
                $value = (int) $value;
251
            }
252
253 1
            return "ALTER TABLE $tableName AUTO_INCREMENT=$value";
254
        }
255
256
        if ($table === null) {
257
            throw new \InvalidArgumentException("Table not found: $tableName");
258
        }
259
260
        throw new \InvalidArgumentException("There is no sequence associated with table '$tableName'.");
261
    }
262
263
    /**
264
     * Builds a SQL statement for enabling or disabling integrity check.
265
     *
266
     * @param bool $check  whether to turn on or off the integrity check.
267
     * @param string $schema the schema of the tables. Meaningless for MySQL.
268
     * @param string $table  the table name. Meaningless for MySQL.
269
     *
270
     * @return string the SQL statement for checking integrity.
271
     */
272
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
273
    {
274
        return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0);
275
    }
276
277
    /**
278
     * @param int|object|null $limit
279
     * @param int|object|null $offset
280
     *
281
     * @return string the LIMIT and OFFSET clauses.
282
     */
283 153
    public function buildLimit($limit, $offset): string
284
    {
285 153
        $sql = '';
286
287 153
        if ($this->hasLimit($limit)) {
288 9
            $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

288
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
289
290 9
            if ($this->hasOffset($offset)) {
291 9
                $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

291
                $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
292
            }
293 147
        } elseif ($this->hasOffset($offset)) {
294
            /**
295
             * limit is not optional in MySQL.
296
             *
297
             * http://stackoverflow.com/a/271650/1106908
298
             * http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240
299
             */
300
            $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1
301
        }
302
303 153
        return $sql;
304
    }
305
306
    /**
307
     * Checks to see if the given limit is effective.
308
     *
309
     * @param mixed $limit the given limit.
310
     *
311
     * @return bool whether the limit is effective.
312
     */
313 153
    protected function hasLimit($limit): bool
314
    {
315
        /** In MySQL limit argument must be non negative integer constant */
316 153
        return \ctype_digit((string) $limit);
317
    }
318
319
    /**
320
     * Checks to see if the given offset is effective.
321
     *
322
     * @param mixed $offset the given offset.
323
     *
324
     * @return bool whether the offset is effective.
325
     */
326 153
    protected function hasOffset($offset): bool
327
    {
328
        /** In MySQL offset argument must be non negative integer constant */
329 153
        $offset = (string) $offset;
330
331 153
        return \ctype_digit($offset) && $offset !== '0';
332
    }
333
334
    /**
335
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
336
     *
337
     * @param string $table the table that new rows will be inserted into.
338
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
339
     * {@see \Yiisoft\Db\Query\Query|Query} to perform INSERT INTO ... SELECT SQL statement.
340
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
341
     * command later.
342
     *
343
     * @throws Exception
344
     * @throws InvalidArgumentException
345
     * @throws InvalidConfigException
346
     * @throws NotSupportedException
347
     *
348
     * @return array array of column names, placeholders, values and params.
349
     */
350 45
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
351
    {
352 45
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
353 42
        if (!$columns instanceof Query && empty($names)) {
354
            $tableSchema = $this->db->getSchema()->getTableSchema($table);
355
            $columns = $tableSchema->getColumns();
356
            if ($tableSchema !== null) {
357
                $columns = !empty($tableSchema->getPrimaryKey())
358
                    ? $tableSchema->getPrimaryKey() : [\reset($columns)->getName()];
359
                foreach ($columns as $name) {
360
                    $names[] = $this->db->quoteColumnName($name);
361
                    $placeholders[] = 'DEFAULT';
362
                }
363
            }
364
        }
365
366 42
        return [$names, $placeholders, $values, $params];
367
    }
368
369
    /**
370
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
371
     * constraints), or update them if they do.
372
     *
373
     * For example,
374
     *
375
     * ```php
376
     * $sql = $queryBuilder->upsert('pages', [
377
     *     'name' => 'Front page',
378
     *     'url' => 'http://example.com/', // url is unique
379
     *     'visits' => 0,
380
     * ], [
381
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
382
     * ], $params);
383
     * ```
384
     *
385
     * The method will properly escape the table and column names.
386
     *
387
     * @param string $table the table that new rows will be inserted into/updated in.
388
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
389
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
390
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist. If `true`
391
     * is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
392
     * be performed if the column data already exists.
393
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
394
     * command later.
395
     *
396
     * @throws Exception
397
     * @throws InvalidConfigException
398
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
399
     *
400
     * @return string the resulting SQL.
401
     */
402 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
403
    {
404 18
        $insertSql = $this->insert($table, $insertColumns, $params);
405
406 18
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
407
408 18
        if (empty($uniqueNames)) {
409 3
            return $insertSql;
410
        }
411
412 15
        if ($updateColumns === true) {
413 4
            $updateColumns = [];
414 4
            foreach ($updateNames as $name) {
415 4
                $updateColumns[$name] = new Expression('VALUES(' . $this->db->quoteColumnName($name) . ')');
416
            }
417 11
        } elseif ($updateColumns === false) {
418 5
            $name = $this->db->quoteColumnName(\reset($uniqueNames));
419 5
            $updateColumns = [$name => new Expression($this->db->quoteTableName($table) . '.' . $name)];
420
        }
421
422 15
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
423
424 15
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . \implode(', ', $updates);
425
    }
426
427
    /**
428
     * Builds a SQL command for adding comment to column.
429
     *
430
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
431
     * method.
432
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
433
     * method.
434
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
435
     *
436
     * @throws Exception
437
     * @throws InvalidConfigException
438
     * @throws NotSupportedException
439
     *
440
     * @return string the SQL statement for adding comment on column.
441
     */
442 2
    public function addCommentOnColumn(string $table, string $column, string $comment): string
443
    {
444
        /** Strip existing comment which may include escaped quotes */
445 2
        $definition = \trim(
446 2
            \preg_replace(
447 2
                "/COMMENT '(?:''|[^'])*'/i",
448 2
                '',
449 2
                $this->getColumnDefinition($table, $column)
450
            )
451
        );
452
453 2
        $checkRegex = '/CHECK *(\(([^()]|(?-2))*\))/';
454
455 2
        $check = \preg_match($checkRegex, $definition, $checkMatches);
456
457 2
        if ($check === 1) {
458
            $definition = \preg_replace($checkRegex, '', $definition);
459
        }
460
461 2
        $alterSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
462 2
            . ' CHANGE ' . $this->db->quoteColumnName($column)
463 2
            . ' ' . $this->db->quoteColumnName($column)
464 2
            . (empty($definition) ? '' : ' ' . $definition)
465 2
            . ' COMMENT ' . $this->db->quoteValue($comment);
466
467 2
        if ($check === 1) {
468
            $alterSql .= ' ' . $checkMatches[0];
469
        }
470
471 2
        return $alterSql;
472
    }
473
474
    /**
475
     * Builds a SQL command for adding comment to table.
476
     *
477
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
478
     * method.
479
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
480
     *
481
     * @throws Exception
482
     * @throws InvalidConfigException
483
     * @throws NotSupportedException
484
     *
485
     * @return string the SQL statement for adding comment on table.
486
     */
487 1
    public function addCommentOnTable(string $table, string $comment): string
488
    {
489 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' COMMENT ' . $this->db->quoteValue($comment);
490
    }
491
492
    /**
493
     * Builds a SQL command for adding comment to column.
494
     *
495
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
496
     * method.
497
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
498
     * method.
499
     *
500
     * @throws Exception
501
     * @throws InvalidConfigException
502
     * @throws NotSupportedException
503
     *
504
     * @return string the SQL statement for adding comment on column.
505
     */
506 2
    public function dropCommentFromColumn(string $table, string $column): string
507
    {
508 2
        return $this->addCommentOnColumn($table, $column, '');
509
    }
510
511
    /**
512
     * Builds a SQL command for adding comment to table.
513
     *
514
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
515
     * method.
516
     *
517
     * @throws Exception
518
     * @throws InvalidConfigException
519
     * @throws NotSupportedException
520
     *
521
     * @return string the SQL statement for adding comment on column.
522
     */
523 1
    public function dropCommentFromTable(string $table): string
524
    {
525 1
        return $this->addCommentOnTable($table, '');
526
    }
527
528
    /**
529
     * Gets column definition.
530
     *
531
     * @param string $table table name.
532
     * @param string $column column name.
533
     *
534
     * @throws Exception in case when table does not contain column.
535
     *
536
     * @return string|null the column definition.
537
     */
538 2
    private function getColumnDefinition(string $table, string $column): ?string
539
    {
540 2
        $quotedTable = $this->db->quoteTableName($table);
541
542 2
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
543
544 2
        if ($row === false) {
545
            throw new Exception("Unable to find column '$column' in table '$table'.");
546
        }
547
548 2
        if (!isset($row['Create Table'])) {
549
            $row = \array_values($row);
550
            $sql = $row[1];
551
        } else {
552 2
            $sql = $row['Create Table'];
553
        }
554
555 2
        if (\preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
556 2
            foreach ($matches[1] as $i => $c) {
557 2
                if ($c === $column) {
558 2
                    return $matches[2][$i];
559
                }
560
            }
561
        }
562
    }
563
}
564