Completed
Push — 2.1 ( b44a46...4c2160 )
by
unknown
12:30
created

QueryBuilder::buildLimit()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 4
nop 2
crap 4
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\mysql;
9
10
use yii\base\InvalidArgumentException;
11
use yii\base\NotSupportedException;
12
use yii\db\Exception;
13
use yii\db\Expression;
14
use yii\db\Query;
15
16
/**
17
 * QueryBuilder is the query builder for MySQL databases.
18
 *
19
 * @author Qiang Xue <[email protected]>
20
 * @since 2.0
21
 */
22
class QueryBuilder extends \yii\db\QueryBuilder
23
{
24
    /**
25
     * @var array mapping from abstract column types (keys) to physical column types (values).
26
     */
27
    public $typeMap = [
28
        Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
29
        Schema::TYPE_UPK => 'int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
30
        Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY',
31
        Schema::TYPE_UBIGPK => 'bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
32
        Schema::TYPE_CHAR => 'char(1)',
33
        Schema::TYPE_STRING => 'varchar(255)',
34
        Schema::TYPE_TEXT => 'text',
35
        Schema::TYPE_TINYINT => 'tinyint(3)',
36
        Schema::TYPE_SMALLINT => 'smallint(6)',
37
        Schema::TYPE_INTEGER => 'int(11)',
38
        Schema::TYPE_BIGINT => 'bigint(20)',
39
        Schema::TYPE_FLOAT => 'float',
40
        Schema::TYPE_DOUBLE => 'double',
41
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
42
        Schema::TYPE_DATETIME => 'datetime',
43
        Schema::TYPE_TIMESTAMP => 'timestamp',
44
        Schema::TYPE_TIME => 'time',
45
        Schema::TYPE_DATE => 'date',
46
        Schema::TYPE_BINARY => 'blob',
47
        Schema::TYPE_BOOLEAN => 'tinyint(1)',
48
        Schema::TYPE_MONEY => 'decimal(19,4)',
49
        Schema::TYPE_JSON => 'json'
50
    ];
51
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 496
    protected function defaultExpressionBuilders()
57
    {
58 496
        return array_merge(parent::defaultExpressionBuilders(), [
59 496
            'yii\db\JsonExpression' => 'yii\db\mysql\JsonExpressionBuilder',
60
        ]);
61
    }
62
63
    /**
64
     * Builds a SQL statement for renaming a column.
65
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
66
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
67
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
68
     * @return string the SQL statement for renaming a DB column.
69
     * @throws Exception
70
     */
71
    public function renameColumn($table, $oldName, $newName)
72
    {
73
        $quotedTable = $this->db->quoteTableName($table);
74
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
75
        if ($row === false) {
76
            throw new Exception("Unable to find column '$oldName' in table '$table'.");
77
        }
78
        if (isset($row['Create Table'])) {
79
            $sql = $row['Create Table'];
80
        } else {
81
            $row = array_values($row);
82
            $sql = $row[1];
83
        }
84
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
85
            foreach ($matches[1] as $i => $c) {
86
                if ($c === $oldName) {
87
                    return "ALTER TABLE $quotedTable CHANGE "
88
                        . $this->db->quoteColumnName($oldName) . ' '
89
                        . $this->db->quoteColumnName($newName) . ' '
90
                        . $matches[2][$i];
91
                }
92
            }
93
        }
94
        // try to give back a SQL anyway
95
        return "ALTER TABLE $quotedTable CHANGE "
96
            . $this->db->quoteColumnName($oldName) . ' '
97
            . $this->db->quoteColumnName($newName);
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     * @see https://bugs.mysql.com/bug.php?id=48875
103
     */
104 12
    public function createIndex($name, $table, $columns, $unique = false)
105
    {
106
        return 'ALTER TABLE '
107 12
        . $this->db->quoteTableName($table)
108 12
        . ($unique ? ' ADD UNIQUE INDEX ' : ' ADD INDEX ')
109 12
        . $this->db->quoteTableName($name)
110 12
        . ' (' . $this->buildColumns($columns) . ')';
111
    }
112
113
    /**
114
     * Builds a SQL statement for dropping a foreign key constraint.
115
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
116
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
117
     * @return string the SQL statement for dropping a foreign key constraint.
118
     */
119 3
    public function dropForeignKey($name, $table)
120
    {
121 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
122 3
            . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name);
123
    }
124
125
    /**
126
     * Builds a SQL statement for removing a primary key constraint to an existing table.
127
     * @param string $name the name of the primary key constraint to be removed.
128
     * @param string $table the table that the primary key constraint will be removed from.
129
     * @return string the SQL statement for removing a primary key constraint from an existing table.
130
     */
131 2
    public function dropPrimaryKey($name, $table)
132
    {
133 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY';
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 2
    public function dropUnique($name, $table)
140
    {
141 2
        return $this->dropIndex($name, $table);
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     * @throws NotSupportedException this is not supported by MySQL.
147
     */
148
    public function addCheck($name, $table, $expression)
149
    {
150
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     * @throws NotSupportedException this is not supported by MySQL.
156
     */
157
    public function dropCheck($name, $table)
158
    {
159
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
160
    }
161
162
    /**
163
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
164
     * The sequence will be reset such that the primary key of the next new row inserted
165
     * will have the specified value or 1.
166
     * @param string $tableName the name of the table whose primary key sequence will be reset
167
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
168
     * the next new row's primary key will have a value 1.
169
     * @return string the SQL statement for resetting sequence
170
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
171
     */
172 9
    public function resetSequence($tableName, $value = null)
173
    {
174 9
        $table = $this->db->getTableSchema($tableName);
175 9
        if ($table !== null && $table->sequenceName !== null) {
176 9
            $tableName = $this->db->quoteTableName($tableName);
177 9
            if ($value === null) {
178 1
                $key = reset($table->primaryKey);
179 1
                $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1;
180
            } else {
181 9
                $value = (int) $value;
182
            }
183
184 9
            return "ALTER TABLE $tableName AUTO_INCREMENT=$value";
185
        } elseif ($table === null) {
186
            throw new InvalidArgumentException("Table not found: $tableName");
187
        }
188
189
        throw new InvalidArgumentException("There is no sequence associated with table '$tableName'.");
190
    }
191
192
    /**
193
     * Builds a SQL statement for enabling or disabling integrity check.
194
     * @param bool $check whether to turn on or off the integrity check.
195
     * @param string $schema the schema of the tables. Meaningless for MySQL.
196
     * @param string $table the table name. Meaningless for MySQL.
197
     * @return string the SQL statement for checking integrity
198
     */
199 4
    public function checkIntegrity($check = true, $schema = '', $table = '')
200
    {
201 4
        return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0);
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 396
    public function buildLimit($limit, $offset)
208
    {
209 396
        $sql = '';
210 396
        if ($this->hasLimit($limit)) {
211 31
            $sql = 'LIMIT ' . $limit;
212 31
            if ($this->hasOffset($offset)) {
213 31
                $sql .= ' OFFSET ' . $offset;
214
            }
215 389
        } elseif ($this->hasOffset($offset)) {
216
            // limit is not optional in MySQL
217
            // http://stackoverflow.com/a/271650/1106908
218
            // http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240
219 2
            $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1
220
        }
221
222 396
        return $sql;
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 396
    protected function hasLimit($limit)
229
    {
230
        // In MySQL limit argument must be nonnegative integer constant
231 396
        return ctype_digit((string) $limit);
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 396
    protected function hasOffset($offset)
238
    {
239
        // In MySQL offset argument must be nonnegative integer constant
240 396
        $offset = (string) $offset;
241 396
        return ctype_digit($offset) && $offset !== '0';
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247 200
    protected function prepareInsertValues($table, $columns, $params = [])
248
    {
249 200
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
0 ignored issues
show
Bug introduced by
The variable $names seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $placeholders seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $values does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
250 197
        if (!$columns instanceof Query && empty($names)) {
0 ignored issues
show
Bug introduced by
The variable $names seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
251 1
            $tableSchema = $this->db->getSchema()->getTableSchema($table);
252 1
            if ($tableSchema !== null) {
253 1
                $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name];
254 1
                foreach ($columns as $name) {
255 1
                    $names[] = $this->db->quoteColumnName($name);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$names was never initialized. Although not strictly required by PHP, it is generally a good practice to add $names = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
256 1
                    $placeholders[] = 'DEFAULT';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$placeholders was never initialized. Although not strictly required by PHP, it is generally a good practice to add $placeholders = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
257
                }
258
            }
259
        }
260 197
        return [$names, $placeholders, $values, $params];
0 ignored issues
show
Bug introduced by
The variable $names does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $placeholders does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     * @see https://downloads.mysql.com/docs/refman-5.1-en.pdf
266
     */
267 22
    public function upsert($table, $insertColumns, $updateColumns, &$params)
268
    {
269 22
        $insertSql = $this->insert($table, $insertColumns, $params);
270 22
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
0 ignored issues
show
Bug introduced by
The variable $uniqueNames seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $updateNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
271 22
        if (empty($uniqueNames)) {
0 ignored issues
show
Bug introduced by
The variable $uniqueNames seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
272 3
            return $insertSql;
273
        }
274
275 19
        if ($updateColumns === true) {
276 9
            $updateColumns = [];
277 9
            foreach ($updateNames as $name) {
278 9
                $updateColumns[$name] = new Expression('VALUES(' . $this->db->quoteColumnName($name) . ')');
279
            }
280 10
        } elseif ($updateColumns === false) {
281 4
            $name = $this->db->quoteColumnName(reset($uniqueNames));
282 4
            $updateColumns = [$name => new Expression($this->db->quoteTableName($table) . '.' . $name)];
283
        }
284 19
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
The variable $updates does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
285 19
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     * @since 2.0.8
291
     */
292 2
    public function addCommentOnColumn($table, $column, $comment)
293
    {
294 2
        $definition = $this->getColumnDefinition($table, $column);
295 2
        $definition = trim(preg_replace("/COMMENT '(.*?)'/i", '', $definition));
296
297 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
298 2
            . ' CHANGE ' . $this->db->quoteColumnName($column)
299 2
            . ' ' . $this->db->quoteColumnName($column)
300 2
            . (empty($definition) ? '' : ' ' . $definition)
301 2
            . ' COMMENT ' . $this->db->quoteValue($comment);
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     * @since 2.0.8
307
     */
308 1
    public function addCommentOnTable($table, $comment)
309
    {
310 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' COMMENT ' . $this->db->quoteValue($comment);
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     * @since 2.0.8
316
     */
317 2
    public function dropCommentFromColumn($table, $column)
318
    {
319 2
        return $this->addCommentOnColumn($table, $column, '');
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     * @since 2.0.8
325
     */
326 1
    public function dropCommentFromTable($table)
327
    {
328 1
        return $this->addCommentOnTable($table, '');
329
    }
330
331
332
    /**
333
     * Gets column definition.
334
     *
335
     * @param string $table table name
336
     * @param string $column column name
337
     * @return null|string the column definition
338
     * @throws Exception in case when table does not contain column
339
     */
340 2
    private function getColumnDefinition($table, $column)
341
    {
342 2
        $quotedTable = $this->db->quoteTableName($table);
343 2
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
344 2
        if ($row === false) {
345
            throw new Exception("Unable to find column '$column' in table '$table'.");
346
        }
347 2
        if (isset($row['Create Table'])) {
348 2
            $sql = $row['Create Table'];
349
        } else {
350
            $row = array_values($row);
351
            $sql = $row[1];
352
        }
353 2
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
354 2
            foreach ($matches[1] as $i => $c) {
355 2
                if ($c === $column) {
356 2
                    return $matches[2][$i];
357
                }
358
            }
359
        }
360
361
        return null;
362
    }
363
}
364