Passed
Pull Request — master (#22)
by Wilmer
15:13
created

QueryBuilder::createIndex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

72
        /** @scrutinizer ignore-call */ 
73
        $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...
73
74
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
75
76
        if ($row === false) {
77
            throw new Exception("Unable to find column '$oldName' in table '$table'.");
78
        }
79
80
        if (isset($row['Create Table'])) {
81
            $sql = $row['Create Table'];
82
        } else {
83
            $row = array_values($row);
84
            $sql = $row[1];
85
        }
86
87
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
88
            foreach ($matches[1] as $i => $c) {
89
                if ($c === $oldName) {
90
                    return "ALTER TABLE $quotedTable CHANGE "
91
                        . $this->db->quoteColumnName($oldName) . ' '
92
                        . $this->db->quoteColumnName($newName) . ' '
93
                        . $matches[2][$i];
94
                }
95
            }
96
        }
97
98
        // try to give back a SQL anyway
99
        return "ALTER TABLE $quotedTable CHANGE "
100
            . $this->db->quoteColumnName($oldName) . ' '
101
            . $this->db->quoteColumnName($newName);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     *
107
     * @see https://bugs.mysql.com/bug.php?id=48875
108
     */
109
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
110
    {
111
        return 'ALTER TABLE '
112
            . $this->db->quoteTableName($table)
113
            . ($unique ? ' ADD UNIQUE INDEX ' : ' ADD INDEX ')
114
            . $this->db->quoteTableName($name)
115
            . ' (' . $this->buildColumns($columns) . ')';
116
    }
117
118
    /**
119
     * Builds a SQL statement for dropping a foreign key constraint.
120
     *
121
     * @param string $name  the name of the foreign key constraint to be dropped. The name will be properly quoted by
122
     * the method.
123
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
124
     *
125
     * @return string the SQL statement for dropping a foreign key constraint.
126
     */
127
    public function dropForeignKey(string $name, string $table): string
128
    {
129
        return 'ALTER TABLE '
130
            . $this->db->quoteTableName($table)
131
            . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name);
132
    }
133
134
    /**
135
     * Builds a SQL statement for removing a primary key constraint to an existing table.
136
     *
137
     * @param string $name  the name of the primary key constraint to be removed.
138
     * @param string $table the table that the primary key constraint will be removed from.
139
     *
140
     * @return string the SQL statement for removing a primary key constraint from an existing table.
141
     */
142
    public function dropPrimaryKey(string $name, string $table): string
143
    {
144
        return 'ALTER TABLE '
145
            . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY';
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function dropUnique(string $name, string $table): string
152
    {
153
        return $this->dropIndex($name, $table);
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     *
159
     * @throws NotSupportedException this is not supported by MySQL.
160
     */
161
    public function addCheck(string $name, string $table, string $expression): string
162
    {
163
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     *
169
     * @throws NotSupportedException this is not supported by MySQL.
170
     */
171
    public function dropCheck(string $name, string $table): string
172
    {
173
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
174
    }
175
176
    /**
177
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
178
     * The sequence will be reset such that the primary key of the next new row inserted
179
     * will have the specified value or 1.
180
     *
181
     * @param string $tableName the name of the table whose primary key sequence will be reset
182
     * @param mixed  $value     the value for the primary key of the next new row inserted. If this is not set,
183
     *                          the next new row's primary key will have a value 1.
184
     *
185
     * @throws \InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
186
     *
187
     * @return string the SQL statement for resetting sequence
188
     */
189
    public function resetSequence(string $tableName, $value = null): string
190
    {
191
        $table = $this->db->getTableSchema($tableName);
192
193
        if ($table !== null && $table->getSequenceName() !== null) {
194
            $tableName = $this->db->quoteTableName($tableName);
195
196
            if ($value === null) {
197
                $pk = $table->getPrimaryKey();
198
                $key = reset($pk);
199
                $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1;
200
            } else {
201
                $value = (int) $value;
202
            }
203
204
            return "ALTER TABLE $tableName AUTO_INCREMENT=$value";
205
        }
206
207
        if ($table === null) {
208
            throw new \InvalidArgumentException("Table not found: $tableName");
209
        }
210
211
        throw new \InvalidArgumentException("There is no sequence associated with table '$tableName'.");
212
    }
213
214
    /**
215
     * Builds a SQL statement for enabling or disabling integrity check.
216
     *
217
     * @param bool $check  whether to turn on or off the integrity check.
218
     * @param string $schema the schema of the tables. Meaningless for MySQL.
219
     * @param string $table  the table name. Meaningless for MySQL.
220
     *
221
     * @return string the SQL statement for checking integrity
222
     */
223
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
224
    {
225
        return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0);
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function buildLimit($limit, $offset): string
232
    {
233
        $sql = '';
234
235
        if ($this->hasLimit($limit)) {
236
            $sql = 'LIMIT ' . $limit;
237
238
            if ($this->hasOffset($offset)) {
239
                $sql .= ' OFFSET ' . $offset;
240
            }
241
        } elseif ($this->hasOffset($offset)) {
242
            /**
243
             * limit is not optional in MySQL
244
             * http://stackoverflow.com/a/271650/1106908
245
             * http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240
246
             */
247
            $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1
248
        }
249
250
        return $sql;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256
    protected function hasLimit($limit): bool
257
    {
258
        // In MySQL limit argument must be nonnegative integer constant
259
        return ctype_digit((string) $limit);
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265
    protected function hasOffset($offset): bool
266
    {
267
        // In MySQL offset argument must be nonnegative integer constant
268
        $offset = (string) $offset;
269
270
        return ctype_digit($offset) && $offset !== '0';
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
277
    {
278
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
279
        if (!$columns instanceof Query && empty($names)) {
280
            $tableSchema = $this->db->getSchema()->getTableSchema($table);
281
            if ($tableSchema !== null) {
282
                $columns = !empty($tableSchema->getPrimaryKey())
283
                    ? $tableSchema->getPrimaryKey() : [reset($tableSchema->columns)->getName()];
0 ignored issues
show
Bug introduced by
The property columns is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
284
                foreach ($columns as $name) {
285
                    $names[] = $this->db->quoteColumnName($name);
286
                    $placeholders[] = 'DEFAULT';
287
                }
288
            }
289
        }
290
291
        return [$names, $placeholders, $values, $params];
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     *
297
     * @see https://downloads.mysql.com/docs/refman-5.1-en.pdf
298
     */
299
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
300
    {
301
        $insertSql = $this->insert($table, $insertColumns, $params);
302
303
        list($uniqueNames, , $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
304
305
        if (empty($uniqueNames)) {
306
            return $insertSql;
307
        }
308
309
        if ($updateColumns === true) {
310
            $updateColumns = [];
311
            foreach ($updateNames as $name) {
312
                $updateColumns[$name] = new Expression('VALUES(' . $this->db->quoteColumnName($name) . ')');
313
            }
314
        } elseif ($updateColumns === false) {
315
            $name = $this->db->quoteColumnName(reset($uniqueNames));
316
            $updateColumns = [$name => new Expression($this->db->quoteTableName($table) . '.' . $name)];
317
        }
318
319
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
320
321
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function addCommentOnColumn(string $table, string $column, string $comment): string
328
    {
329
        // Strip existing comment which may include escaped quotes
330
        $definition = trim(
331
            preg_replace(
332
                "/COMMENT '(?:''|[^'])*'/i",
333
                '',
334
                $this->getColumnDefinition($table, $column)
335
            )
336
        );
337
338
        $checkRegex = '/CHECK *(\(([^()]|(?-2))*\))/';
339
340
        $check = preg_match($checkRegex, $definition, $checkMatches);
341
342
        if ($check === 1) {
343
            $definition = preg_replace($checkRegex, '', $definition);
344
        }
345
346
        $alterSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
347
            . ' CHANGE ' . $this->db->quoteColumnName($column)
348
            . ' ' . $this->db->quoteColumnName($column)
349
            . (empty($definition) ? '' : ' ' . $definition)
350
            . ' COMMENT ' . $this->db->quoteValue($comment);
351
352
        if ($check === 1) {
353
            $alterSql .= ' ' . $checkMatches[0];
354
        }
355
356
        return $alterSql;
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362
    public function addCommentOnTable(string $table, string $comment): string
363
    {
364
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' COMMENT ' . $this->db->quoteValue($comment);
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    public function dropCommentFromColumn(string $table, string $column): string
371
    {
372
        return $this->addCommentOnColumn($table, $column, '');
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378
    public function dropCommentFromTable(string $table): string
379
    {
380
        return $this->addCommentOnTable($table, '');
381
    }
382
383
    /**
384
     * Gets column definition.
385
     *
386
     * @param string $table  table name
387
     * @param string $column column name
388
     *
389
     * @throws Exception in case when table does not contain column
390
     *
391
     * @return string|null the column definition
392
     */
393
    private function getColumnDefinition(string $table, string $column): ?string
394
    {
395
        $quotedTable = $this->db->quoteTableName($table);
396
397
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
398
399
        if ($row === false) {
400
            throw new Exception("Unable to find column '$column' in table '$table'.");
401
        }
402
403
        if (!isset($row['Create Table'])) {
404
            $row = array_values($row);
405
            $sql = $row[1];
406
        } else {
407
            $sql = $row['Create Table'];
408
        }
409
410
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
411
            foreach ($matches[1] as $i => $c) {
412
                if ($c === $column) {
413
                    return $matches[2][$i];
414
                }
415
            }
416
        }
417
    }
418
}
419