Completed
Push — master ( 2b9374...55b06d )
by Alexander
18:38
created

QueryBuilder::getColumnType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
    /**
51
     * {@inheritdoc}
52
     */
53 273
    protected function defaultExpressionBuilders()
54
    {
55 273
        return array_merge(parent::defaultExpressionBuilders(), [
56 273
            'yii\db\conditions\InCondition' => 'yii\db\mssql\conditions\InConditionBuilder',
57
            'yii\db\conditions\LikeCondition' => 'yii\db\mssql\conditions\LikeConditionBuilder',
58
        ]);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 209
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
65
    {
66 209
        if (!$this->hasOffset($offset) && !$this->hasLimit($limit)) {
67 202
            $orderBy = $this->buildOrderBy($orderBy);
68 202
            return $orderBy === '' ? $sql : $sql . $this->separator . $orderBy;
69
        }
70
71 26
        if (version_compare($this->db->getSchema()->getServerVersion(), '11', '<')) {
72
            return $this->oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
73
        }
74
75 26
        return $this->newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
76
    }
77
78
    /**
79
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2012 or newer.
80
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
81
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
82
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
83
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
84
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
85
     */
86 26
    protected function newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
87
    {
88 26
        $orderBy = $this->buildOrderBy($orderBy);
89 26
        if ($orderBy === '') {
90
            // ORDER BY clause is required when FETCH and OFFSET are in the SQL
91 20
            $orderBy = 'ORDER BY (SELECT NULL)';
92
        }
93 26
        $sql .= $this->separator . $orderBy;
94
95
        // http://technet.microsoft.com/en-us/library/gg699618.aspx
96 26
        $offset = $this->hasOffset($offset) ? $offset : '0';
97 26
        $sql .= $this->separator . "OFFSET $offset ROWS";
98 26
        if ($this->hasLimit($limit)) {
99 24
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
100
        }
101
102 26
        return $sql;
103
    }
104
105
    /**
106
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
107
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
108
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
109
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
110
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
111
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
112
     */
113
    protected function oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
114
    {
115
        $orderBy = $this->buildOrderBy($orderBy);
116
        if ($orderBy === '') {
117
            // ROW_NUMBER() requires an ORDER BY clause
118
            $orderBy = 'ORDER BY (SELECT NULL)';
119
        }
120
121
        $sql = preg_replace('/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i', "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),", $sql);
122
123
        if ($this->hasLimit($limit)) {
124
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
125
        } else {
126
            $sql = "SELECT * FROM ($sql) sub";
127
        }
128
        if ($this->hasOffset($offset)) {
129
            $sql .= $this->separator . "WHERE rowNum > $offset";
130
        }
131
132
        return $sql;
133
    }
134
135
    /**
136
     * Builds a SQL statement for renaming a DB table.
137
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
138
     * @param string $newName the new table name. The name will be properly quoted by the method.
139
     * @return string the SQL statement for renaming a DB table.
140
     */
141 2
    public function renameTable($oldName, $newName)
142
    {
143 2
        return 'sp_rename ' . $this->db->quoteTableName($oldName) . ', ' . $this->db->quoteTableName($newName);
144
    }
145
146
    /**
147
     * Builds a SQL statement for renaming a column.
148
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
149
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
150
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
151
     * @return string the SQL statement for renaming a DB column.
152
     */
153
    public function renameColumn($table, $oldName, $newName)
154
    {
155
        $table = $this->db->quoteTableName($table);
156
        $oldName = $this->db->quoteColumnName($oldName);
157
        $newName = $this->db->quoteColumnName($newName);
158
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
159
    }
160
161
    /**
162
     * Builds a SQL statement for changing the definition of a column.
163
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
164
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
165
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column type (if any)
166
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
167
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
168
     * @return string the SQL statement for changing the definition of a column.
169
     */
170 1
    public function alterColumn($table, $column, $type)
171
    {
172 1
        $type = $this->getColumnType($type);
173 1
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
174 1
            . $this->db->quoteColumnName($column) . ' '
175 1
            . $this->getColumnType($type);
176
177 1
        return $sql;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 2
    public function addDefaultValue($name, $table, $column, $value)
184
    {
185 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
186 2
            . $this->db->quoteColumnName($name) . ' DEFAULT ' . $this->db->quoteValue($value) . ' FOR '
187 2
            . $this->db->quoteColumnName($column);
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 2
    public function dropDefaultValue($name, $table)
194
    {
195 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
196 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
197
    }
198
199
    /**
200
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
201
     * The sequence will be reset such that the primary key of the next new row inserted
202
     * will have the specified value or 1.
203
     * @param string $tableName the name of the table whose primary key sequence will be reset
204
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
205
     * the next new row's primary key will have a value 1.
206
     * @return string the SQL statement for resetting sequence
207
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
208
     */
209 2
    public function resetSequence($tableName, $value = null)
210
    {
211 2
        $table = $this->db->getTableSchema($tableName);
212 2
        if ($table !== null && $table->sequenceName !== null) {
213 2
            $tableName = $this->db->quoteTableName($tableName);
214 2
            if ($value === null) {
215 1
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
216 1
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
217
            } else {
218 2
                $value = (int) $value;
219
            }
220
221 2
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
222
        } elseif ($table === null) {
223
            throw new InvalidArgumentException("Table not found: $tableName");
224
        }
225
226
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
227
    }
228
229
    /**
230
     * Builds a SQL statement for enabling or disabling integrity check.
231
     * @param bool $check whether to turn on or off the integrity check.
232
     * @param string $schema the schema of the tables.
233
     * @param string $table the table name.
234
     * @return string the SQL statement for checking integrity
235
     */
236
    public function checkIntegrity($check = true, $schema = '', $table = '')
237
    {
238
        $enable = $check ? 'CHECK' : 'NOCHECK';
239
        $schema = $schema ?: $this->db->getSchema()->defaultSchema;
240
        $tableNames = $this->db->getTableSchema($table) ? [$table] : $this->db->getSchema()->getTableNames($schema);
241
        $viewNames = $this->db->getSchema()->getViewNames($schema);
0 ignored issues
show
Bug introduced by
The method getViewNames() does not exist on yii\db\Schema. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

241
        $viewNames = $this->db->getSchema()->/** @scrutinizer ignore-call */ getViewNames($schema);
Loading history...
242
        $tableNames = array_diff($tableNames, $viewNames);
243
        $command = '';
244
245
        foreach ($tableNames as $tableName) {
246
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
247
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
248
        }
249
250
        return $command;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     * @since 2.0.8
256
     */
257
    public function addCommentOnColumn($table, $column, $comment)
258
    {
259
        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)}";
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     * @since 2.0.8
265
     */
266
    public function addCommentOnTable($table, $comment)
267
    {
268
        return "sp_updateextendedproperty @name = N'MS_Description', @value = {$this->db->quoteValue($comment)}, @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     * @since 2.0.8
274
     */
275
    public function dropCommentFromColumn($table, $column)
276
    {
277
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}, @level2type = N'Column', @level2name = {$this->db->quoteColumnName($column)}";
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     * @since 2.0.8
283
     */
284
    public function dropCommentFromTable($table)
285
    {
286
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
287
    }
288
289
    /**
290
     * Returns an array of column names given model name.
291
     *
292
     * @param string $modelClass name of the model class
293
     * @return array|null array of column names
294
     */
295
    protected function getAllColumnNames($modelClass = null)
296
    {
297
        if (!$modelClass) {
298
            return null;
299
        }
300
        /* @var $modelClass \yii\db\ActiveRecord */
301
        $schema = $modelClass::getTableSchema();
302
        return array_keys($schema->columns);
303
    }
304
305
    /**
306
     * @return bool whether the version of the MSSQL being used is older than 2012.
307
     * @throws \yii\base\InvalidConfigException
308
     * @throws \yii\db\Exception
309
     * @deprecated 2.0.14 Use [[Schema::getServerVersion]] with [[\version_compare()]].
310
     */
311
    protected function isOldMssql()
312
    {
313
        return version_compare($this->db->getSchema()->getServerVersion(), '11', '<');
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     * @since 2.0.8
319
     */
320 10
    public function selectExists($rawSql)
321
    {
322 10
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
323
    }
324
325
    /**
326
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
327
     * @param string $table the table that data will be saved into.
328
     * @param array $columns the column data (name => value) to be saved into the table.
329
     * @return array normalized columns
330
     */
331 56
    private function normalizeTableRowData($table, $columns, &$params)
332
    {
333 56
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
334 56
            $columnSchemas = $tableSchema->columns;
335 56
            foreach ($columns as $name => $value) {
336
                // @see https://github.com/yiisoft/yii2/issues/12599
337 55
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value) || $value === null)) {
338
                    $phName = $this->bindParam($value, $params);
339 55
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $params);
340
                }
341
            }
342
        }
343
344 56
        return $columns;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350 45
    public function insert($table, $columns, &$params)
351
    {
352 45
        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 23
    public function upsert($table, $insertColumns, $updateColumns, &$params)
361
    {
362
        /** @var Constraint[] $constraints */
363 23
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
364 23
        if (empty($uniqueNames)) {
365 3
            return $this->insert($table, $insertColumns, $params);
366
        }
367
368 20
        $onCondition = ['or'];
369 20
        $quotedTableName = $this->db->quoteTableName($table);
370 20
        foreach ($constraints as $constraint) {
371 20
            $constraintCondition = ['and'];
372 20
            foreach ($constraint->columnNames as $name) {
373 20
                $quotedName = $this->db->quoteColumnName($name);
374 20
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
375
            }
376 20
            $onCondition[] = $constraintCondition;
377
        }
378 20
        $on = $this->buildCondition($onCondition, $params);
379 20
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
380 20
        $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
381 20
            . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') '
382 20
            . "ON ($on)";
383 20
        $insertValues = [];
384 20
        foreach ($insertNames as $name) {
385 20
            $quotedName = $this->db->quoteColumnName($name);
386 20
            if (strrpos($quotedName, '.') === false) {
387 20
                $quotedName = '[EXCLUDED].' . $quotedName;
388
            }
389 20
            $insertValues[] = $quotedName;
390
        }
391 20
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
392 20
            . ' VALUES (' . implode(', ', $insertValues) . ')';
393 20
        if ($updateColumns === false) {
394 4
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
395
        }
396
397 16
        if ($updateColumns === true) {
398 10
            $updateColumns = [];
399 10
            foreach ($updateNames as $name) {
400 10
                $quotedName = $this->db->quoteColumnName($name);
401 10
                if (strrpos($quotedName, '.') === false) {
402 10
                    $quotedName = '[EXCLUDED].' . $quotedName;
403
                }
404 10
                $updateColumns[$name] = new Expression($quotedName);
405
            }
406
        }
407 16
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
408 16
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
409 16
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415 16
    public function update($table, $columns, $condition, &$params)
416
    {
417 16
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     */
423 20
    public function getColumnType($type)
424
    {
425 20
        $columnType = parent::getColumnType($type);
426
        // remove unsupported keywords
427 20
        $columnType = preg_replace("/\s*comment '.*'/i", '', $columnType);
428 20
        $columnType = preg_replace('/ first$/i', '', $columnType);
429
430 20
        return $columnType;
431
    }
432
}
433