Completed
Pull Request — 2.1 (#15718)
by Alex
17:00
created

QueryBuilder::getAllColumnNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 8
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 6
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\mssql;
9
10
use yii\base\InvalidArgumentException;
11
use yii\db\Constraint;
12
use yii\db\Expression;
13
14
/**
15
 * QueryBuilder is the query builder for MS SQL Server databases (version 2008 and above).
16
 *
17
 * @author Timur Ruziev <[email protected]>
18
 * @since 2.0
19
 */
20
class QueryBuilder extends \yii\db\QueryBuilder
21
{
22
    /**
23
     * @var array mapping from abstract column types (keys) to physical column types (values).
24
     */
25
    public $typeMap = [
26
        Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY',
27
        Schema::TYPE_UPK => 'int IDENTITY PRIMARY KEY',
28
        Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY',
29
        Schema::TYPE_UBIGPK => 'bigint IDENTITY PRIMARY KEY',
30
        Schema::TYPE_CHAR => 'nchar(1)',
31
        Schema::TYPE_STRING => 'nvarchar(255)',
32
        Schema::TYPE_TEXT => 'nvarchar(max)',
33
        Schema::TYPE_TINYINT => 'tinyint',
34
        Schema::TYPE_SMALLINT => 'smallint',
35
        Schema::TYPE_INTEGER => 'int',
36
        Schema::TYPE_BIGINT => 'bigint',
37
        Schema::TYPE_FLOAT => 'float',
38
        Schema::TYPE_DOUBLE => 'float',
39
        Schema::TYPE_DECIMAL => 'decimal(18,0)',
40
        Schema::TYPE_DATETIME => 'datetime',
41
        Schema::TYPE_TIMESTAMP => 'datetime',
42
        Schema::TYPE_TIME => 'time',
43
        Schema::TYPE_DATE => 'date',
44
        Schema::TYPE_BINARY => 'varbinary(max)',
45
        Schema::TYPE_BOOLEAN => 'bit',
46
        Schema::TYPE_MONEY => 'decimal(19,4)',
47
    ];
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    protected function defaultExpressionBuilders()
53
    {
54
        return array_merge(parent::defaultExpressionBuilders(), [
55
            'yii\db\conditions\InCondition' => 'yii\db\mssql\conditions\InConditionBuilder',
56
            'yii\db\conditions\LikeCondition' => 'yii\db\mssql\conditions\LikeConditionBuilder',
57
        ]);
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
64
    {
65
        if (!$this->hasOffset($offset) && !$this->hasLimit($limit)) {
66
            $orderBy = $this->buildOrderBy($orderBy);
0 ignored issues
show
Bug introduced by
The call to buildOrderBy() misses a required argument $params.

This check looks for function calls that miss required arguments.

Loading history...
67
            return $orderBy === '' ? $sql : $sql . $this->separator . $orderBy;
68
        }
69
70
        if (version_compare($this->db->getSchema()->getServerVersion(), '11', '<')) {
71
            return $this->oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
72
        }
73
74
        return $this->newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
75
    }
76
77
    /**
78
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2012 or newer.
79
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
80
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
81
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
82
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
83
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
84
     */
85
    protected function newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
86
    {
87
        $orderBy = $this->buildOrderBy($orderBy);
0 ignored issues
show
Bug introduced by
The call to buildOrderBy() misses a required argument $params.

This check looks for function calls that miss required arguments.

Loading history...
88
        if ($orderBy === '') {
89
            // ORDER BY clause is required when FETCH and OFFSET are in the SQL
90
            $orderBy = 'ORDER BY (SELECT NULL)';
91
        }
92
        $sql .= $this->separator . $orderBy;
93
94
        // http://technet.microsoft.com/en-us/library/gg699618.aspx
95
        $offset = $this->hasOffset($offset) ? $offset : '0';
96
        $sql .= $this->separator . "OFFSET $offset ROWS";
97
        if ($this->hasLimit($limit)) {
98
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
99
        }
100
101
        return $sql;
102
    }
103
104
    /**
105
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
106
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
107
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
108
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
109
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
110
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
111
     */
112
    protected function oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
113
    {
114
        $orderBy = $this->buildOrderBy($orderBy);
0 ignored issues
show
Bug introduced by
The call to buildOrderBy() misses a required argument $params.

This check looks for function calls that miss required arguments.

Loading history...
115
        if ($orderBy === '') {
116
            // ROW_NUMBER() requires an ORDER BY clause
117
            $orderBy = 'ORDER BY (SELECT NULL)';
118
        }
119
120
        $sql = preg_replace('/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i', "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),", $sql);
121
122
        if ($this->hasLimit($limit)) {
123
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
124
        } else {
125
            $sql = "SELECT * FROM ($sql) sub";
126
        }
127
        if ($this->hasOffset($offset)) {
128
            $sql .= $this->separator . "WHERE rowNum > $offset";
129
        }
130
131
        return $sql;
132
    }
133
134
    /**
135
     * Builds a SQL statement for renaming a DB table.
136
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
137
     * @param string $newName the new table name. The name will be properly quoted by the method.
138
     * @return string the SQL statement for renaming a DB table.
139
     */
140
    public function renameTable($oldName, $newName)
141
    {
142
        return 'sp_rename ' . $this->db->quoteTableName($oldName) . ', ' . $this->db->quoteTableName($newName);
143
    }
144
145
    /**
146
     * Builds a SQL statement for renaming a column.
147
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
148
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
149
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
150
     * @return string the SQL statement for renaming a DB column.
151
     */
152
    public function renameColumn($table, $oldName, $newName)
153
    {
154
        $table = $this->db->quoteTableName($table);
155
        $oldName = $this->db->quoteColumnName($oldName);
156
        $newName = $this->db->quoteColumnName($newName);
157
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
158
    }
159
160
    /**
161
     * Builds a SQL statement for changing the definition of a column.
162
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
163
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
164
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column type (if any)
165
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
166
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
167
     * @return string the SQL statement for changing the definition of a column.
168
     */
169
    public function alterColumn($table, $column, $type)
170
    {
171
        $type = $this->getColumnType($type);
172
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
173
            . $this->db->quoteColumnName($column) . ' '
174
            . $this->getColumnType($type);
175
176
        return $sql;
177
    }
178
179
    /**
180
     * @inheritDoc
181
     */
182
    public function addDefaultValue($name, $table, $column, $value)
183
    {
184
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
185
            . $this->db->quoteColumnName($name) . ' DEFAULT ' . $this->db->quoteValue($value) . ' FOR '
186
            . $this->db->quoteColumnName($column);
187
    }
188
189
    /**
190
     * @inheritDoc
191
     */
192
    public function dropDefaultValue($name, $table)
193
    {
194
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
195
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
196
    }
197
198
    /**
199
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
200
     * The sequence will be reset such that the primary key of the next new row inserted
201
     * will have the specified value or 1.
202
     * @param string $tableName the name of the table whose primary key sequence will be reset
203
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
204
     * the next new row's primary key will have a value 1.
205
     * @return string the SQL statement for resetting sequence
206
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
207
     */
208
    public function resetSequence($tableName, $value = null)
209
    {
210
        $table = $this->db->getTableSchema($tableName);
211
        if ($table !== null && $table->sequenceName !== null) {
212
            $tableName = $this->db->quoteTableName($tableName);
213
            if ($value === null) {
214
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
0 ignored issues
show
Security Bug introduced by
It seems like reset($table->primaryKey) targeting reset() can also be of type false; however, yii\db\Connection::quoteColumnName() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
215
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
216
            } else {
217
                $value = (int) $value;
218
            }
219
220
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
221
        } elseif ($table === null) {
222
            throw new InvalidArgumentException("Table not found: $tableName");
223
        }
224
225
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
226
    }
227
228
    /**
229
     * Builds a SQL statement for enabling or disabling integrity check.
230
     * @param bool $check whether to turn on or off the integrity check.
231
     * @param string $schema the schema of the tables.
232
     * @param string $table the table name.
233
     * @return string the SQL statement for checking integrity
234
     */
235
    public function checkIntegrity($check = true, $schema = '', $table = '')
236
    {
237
        $enable = $check ? 'CHECK' : 'NOCHECK';
238
        $schema = $schema ?: $this->db->getSchema()->defaultSchema;
239
        $tableNames = $this->db->getTableSchema($table) ? [$table] : $this->db->getSchema()->getTableNames($schema);
240
        $viewNames = $this->db->getSchema()->getViewNames($schema);
0 ignored issues
show
Documentation Bug introduced by
The method getViewNames does not exist on object<yii\db\Schema>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
241
        $tableNames = array_diff($tableNames, $viewNames);
242
        $command = '';
243
244
        foreach ($tableNames as $tableName) {
245
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
246
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
247
        }
248
249
        return $command;
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     * @since 2.0.8
255
     */
256
    public function addCommentOnColumn($table, $column, $comment)
257
    {
258
        return "sp_updateextendedproperty @name = N'MS_Description', @value = {$this->db->quoteValue($comment)}, @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}, @level2type = N'Column', @level2name = {$this->db->quoteColumnName($column)}";
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     * @since 2.0.8
264
     */
265
    public function addCommentOnTable($table, $comment)
266
    {
267
        return "sp_updateextendedproperty @name = N'MS_Description', @value = {$this->db->quoteValue($comment)}, @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     * @since 2.0.8
273
     */
274
    public function dropCommentFromColumn($table, $column)
275
    {
276
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}, @level2type = N'Column', @level2name = {$this->db->quoteColumnName($column)}";
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     * @since 2.0.8
282
     */
283
    public function dropCommentFromTable($table)
284
    {
285
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
286
    }
287
288
    /**
289
     * Returns an array of column names given model name.
290
     *
291
     * @param string $modelClass name of the model class
292
     * @return array|null array of column names
293
     */
294
    protected function getAllColumnNames($modelClass = null)
295
    {
296
        if (!$modelClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $modelClass of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
297
            return null;
298
        }
299
        /* @var $modelClass \yii\db\ActiveRecord */
300
        $schema = $modelClass::getTableSchema();
301
        return array_keys($schema->columns);
302
    }
303
304
    /**
305
     * @return bool whether the version of the MSSQL being used is older than 2012.
306
     * @throws \yii\base\InvalidConfigException
307
     * @throws \yii\db\Exception
308
     * @deprecated 2.0.14 Use [[Schema::getServerVersion]] with [[\version_compare()]].
309
     */
310
    protected function isOldMssql()
311
    {
312
        return version_compare($this->db->getSchema()->getServerVersion(), '11', '<');
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     * @since 2.0.8
318
     */
319
    public function selectExists($rawSql)
320
    {
321
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
322
    }
323
324
    /**
325
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
326
     * @param string $table the table that data will be saved into.
327
     * @param array $columns the column data (name => value) to be saved into the table.
328
     * @return array normalized columns
329
     */
330
    private function normalizeTableRowData($table, $columns, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
331
    {
332
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
333
            $columnSchemas = $tableSchema->columns;
334
            foreach ($columns as $name => $value) {
335
                // @see https://github.com/yiisoft/yii2/issues/12599
336
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && is_string($value)) {
337
                    $exParams = [];
338
                    $phName = $this->bindParam($value, $exParams);
339
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
340
                }
341
            }
342
        }
343
344
        return $columns;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350
    public function insert($table, $columns, &$params)
351
    {
352
        return parent::insert($table, $this->normalizeTableRowData($table, $columns, $params), $params);
353
    }
354
355
    /**
356
     * @inheritdoc
357
     * @see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql
358
     * @see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
359
     */
360
    public function upsert($table, $insertColumns, $updateColumns, &$params)
361
    {
362
        /** @var Constraint[] $constraints */
363
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
364
        if (empty($uniqueNames)) {
365
            return $this->insert($table, $insertColumns, $params);
366
        }
367
368
        $onCondition = ['or'];
369
        $quotedTableName = $this->db->quoteTableName($table);
370
        foreach ($constraints as $constraint) {
371
            $constraintCondition = ['and'];
372
            foreach ($constraint->columnNames as $name) {
0 ignored issues
show
Bug introduced by
The expression $constraint->columnNames of type array<integer,string>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
373
                $quotedName = $this->db->quoteColumnName($name);
374
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
375
            }
376
            $onCondition[] = $constraintCondition;
377
        }
378
        $on = $this->buildCondition($onCondition, $params);
379
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
380
        $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
381
            . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') '
382
            . 'ON ' . $on;
383
        $insertValues = [];
384
        foreach ($insertNames as $name) {
385
            $quotedName = $this->db->quoteColumnName($name);
386
            if (strrpos($quotedName, '.') === false) {
387
                $quotedName = '[EXCLUDED].' . $quotedName;
388
            }
389
            $insertValues[] = $quotedName;
390
        }
391
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
392
            . ' VALUES (' . implode(', ', $insertValues) . ')';
393
        if ($updateColumns === false) {
394
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
395
        }
396
397
        if ($updateColumns === true) {
398
            $updateColumns = [];
399
            foreach ($updateNames as $name) {
400
                $quotedName = $this->db->quoteColumnName($name);
401
                if (strrpos($quotedName, '.') === false) {
402
                    $quotedName = '[EXCLUDED].' . $quotedName;
403
                }
404
                $updateColumns[$name] = new Expression($quotedName);
405
            }
406
        }
407
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
408
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
409
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415
    public function update($table, $columns, $condition, &$params)
416
    {
417
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
418
    }
419
}
420