Completed
Push — master ( f1a1d9...18cd95 )
by Paweł
30s queued 17s
created

QueryBuilder   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 647
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 239
c 4
b 0
f 0
dl 0
loc 647
ccs 0
cts 363
cp 0
rs 2
wmc 96

28 Methods

Rating   Name   Duplication   Size   Complexity  
A newBuildOrderByAndLimit() 0 17 4
A buildOrderByAndLimit() 0 12 5
A defaultExpressionBuilders() 0 5 1
A renameColumn() 0 6 1
A renameTable() 0 3 1
A oldBuildOrderByAndLimit() 0 23 5
A addCommentOnColumn() 0 3 1
A dropConstraintsForColumn() 0 21 2
B normalizeTableRowData() 0 15 8
A isOldMssql() 0 3 1
C upsert() 0 68 13
A selectExists() 0 3 1
A buildRemoveCommentSql() 0 27 6
A update() 0 3 1
C insert() 0 36 12
A dropColumn() 0 4 1
A dropDefaultValue() 0 4 1
A buildAddCommentSql() 0 33 6
A dropCommentFromColumn() 0 3 1
A checkIntegrity() 0 15 5
A addCommentOnTable() 0 3 1
A getAllColumnNames() 0 8 2
A dropCommentFromTable() 0 3 1
A extractAlias() 0 7 2
A resetSequence() 0 18 5
A addDefaultValue() 0 5 1
A getColumnType() 0 8 1
B alterColumn() 0 38 7

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

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\base\NotSupportedException;
12
use yii\db\Constraint;
13
use yii\db\Expression;
14
15
/**
16
 * QueryBuilder is the query builder for MS SQL Server databases (version 2008 and above).
17
 *
18
 * @author Timur Ruziev <[email protected]>
19
 * @since 2.0
20
 */
21
class QueryBuilder extends \yii\db\QueryBuilder
22
{
23
    /**
24
     * @var array mapping from abstract column types (keys) to physical column types (values).
25
     */
26
    public $typeMap = [
27
        Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY',
28
        Schema::TYPE_UPK => 'int IDENTITY PRIMARY KEY',
29
        Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY',
30
        Schema::TYPE_UBIGPK => 'bigint IDENTITY PRIMARY KEY',
31
        Schema::TYPE_CHAR => 'nchar(1)',
32
        Schema::TYPE_STRING => 'nvarchar(255)',
33
        Schema::TYPE_TEXT => 'nvarchar(max)',
34
        Schema::TYPE_TINYINT => 'tinyint',
35
        Schema::TYPE_SMALLINT => 'smallint',
36
        Schema::TYPE_INTEGER => 'int',
37
        Schema::TYPE_BIGINT => 'bigint',
38
        Schema::TYPE_FLOAT => 'float',
39
        Schema::TYPE_DOUBLE => 'float',
40
        Schema::TYPE_DECIMAL => 'decimal(18,0)',
41
        Schema::TYPE_DATETIME => 'datetime',
42
        Schema::TYPE_TIMESTAMP => 'datetime',
43
        Schema::TYPE_TIME => 'time',
44
        Schema::TYPE_DATE => 'date',
45
        Schema::TYPE_BINARY => 'varbinary(max)',
46
        Schema::TYPE_BOOLEAN => 'bit',
47
        Schema::TYPE_MONEY => 'decimal(19,4)',
48
    ];
49
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    protected function defaultExpressionBuilders()
55
    {
56
        return array_merge(parent::defaultExpressionBuilders(), [
57
            'yii\db\conditions\InCondition' => 'yii\db\mssql\conditions\InConditionBuilder',
58
            'yii\db\conditions\LikeCondition' => 'yii\db\mssql\conditions\LikeConditionBuilder',
59
        ]);
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
66
    {
67
        if (!$this->hasOffset($offset) && !$this->hasLimit($limit)) {
68
            $orderBy = $this->buildOrderBy($orderBy);
69
            return $orderBy === '' ? $sql : $sql . $this->separator . $orderBy;
70
        }
71
72
        if (version_compare($this->db->getSchema()->getServerVersion(), '11', '<')) {
73
            return $this->oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
74
        }
75
76
        return $this->newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset);
77
    }
78
79
    /**
80
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2012 or newer.
81
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
82
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
83
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
84
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
85
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
86
     */
87
    protected function newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
88
    {
89
        $orderBy = $this->buildOrderBy($orderBy);
90
        if ($orderBy === '') {
91
            // ORDER BY clause is required when FETCH and OFFSET are in the SQL
92
            $orderBy = 'ORDER BY (SELECT NULL)';
93
        }
94
        $sql .= $this->separator . $orderBy;
95
96
        // http://technet.microsoft.com/en-us/library/gg699618.aspx
97
        $offset = $this->hasOffset($offset) ? $offset : '0';
98
        $sql .= $this->separator . "OFFSET $offset ROWS";
99
        if ($this->hasLimit($limit)) {
100
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
101
        }
102
103
        return $sql;
104
    }
105
106
    /**
107
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
108
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
109
     * @param array $orderBy the order by columns. See [[\yii\db\Query::orderBy]] for more details on how to specify this parameter.
110
     * @param int $limit the limit number. See [[\yii\db\Query::limit]] for more details.
111
     * @param int $offset the offset number. See [[\yii\db\Query::offset]] for more details.
112
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
113
     */
114
    protected function oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset)
115
    {
116
        $orderBy = $this->buildOrderBy($orderBy);
117
        if ($orderBy === '') {
118
            // ROW_NUMBER() requires an ORDER BY clause
119
            $orderBy = 'ORDER BY (SELECT NULL)';
120
        }
121
122
        $sql = preg_replace('/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i', "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),", $sql);
123
124
        if ($this->hasLimit($limit)) {
125
            if ($limit instanceof Expression) {
0 ignored issues
show
introduced by
$limit is never a sub-type of yii\db\Expression.
Loading history...
126
                $limit = '('. (string)$limit . ')';
127
            }
128
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
129
        } else {
130
            $sql = "SELECT * FROM ($sql) sub";
131
        }
132
        if ($this->hasOffset($offset)) {
133
            $sql .= $this->separator . "WHERE rowNum > $offset";
134
        }
135
136
        return $sql;
137
    }
138
139
    /**
140
     * Builds a SQL statement for renaming a DB table.
141
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
142
     * @param string $newName the new table name. The name will be properly quoted by the method.
143
     * @return string the SQL statement for renaming a DB table.
144
     */
145
    public function renameTable($oldName, $newName)
146
    {
147
        return 'sp_rename ' . $this->db->quoteTableName($oldName) . ', ' . $this->db->quoteTableName($newName);
148
    }
149
150
    /**
151
     * Builds a SQL statement for renaming a column.
152
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
153
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
154
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
155
     * @return string the SQL statement for renaming a DB column.
156
     */
157
    public function renameColumn($table, $oldName, $newName)
158
    {
159
        $table = $this->db->quoteTableName($table);
160
        $oldName = $this->db->quoteColumnName($oldName);
161
        $newName = $this->db->quoteColumnName($newName);
162
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
163
    }
164
165
    /**
166
     * Builds a SQL statement for changing the definition of a column.
167
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
168
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
169
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column type (if any)
170
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
171
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
172
     * @return string the SQL statement for changing the definition of a column.
173
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
174
     */
175
    public function alterColumn($table, $column, $type)
176
    {
177
        $sqlAfter = [$this->dropConstraintsForColumn($table, $column, 'D')];
178
179
        $columnName = $this->db->quoteColumnName($column);
180
        $tableName = $this->db->quoteTableName($table);
181
        $constraintBase = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column);
182
183
        if ($type instanceof \yii\db\mssql\ColumnSchemaBuilder) {
0 ignored issues
show
introduced by
$type is never a sub-type of yii\db\mssql\ColumnSchemaBuilder.
Loading history...
184
            $type->setAlterColumnFormat();
185
186
187
            $defaultValue = $type->getDefaultValue();
188
            if ($defaultValue !== null) {
189
                $sqlAfter[] = $this->addDefaultValue(
190
                    "DF_{$constraintBase}",
191
                    $table,
192
                    $column,
193
                    $defaultValue instanceof Expression ? $defaultValue : new Expression($defaultValue)
194
                );
195
            }
196
197
            $checkValue = $type->getCheckValue();
198
            if ($checkValue !== null) {
199
                $sqlAfter[] = "ALTER TABLE {$tableName} ADD CONSTRAINT " .
200
                    $this->db->quoteColumnName("CK_{$constraintBase}") .
201
                    " CHECK (" . ($defaultValue instanceof Expression ?  $checkValue : new Expression($checkValue)) . ")";
202
            }
203
204
            if ($type->isUnique()) {
205
                $sqlAfter[] = "ALTER TABLE {$tableName} ADD CONSTRAINT " . $this->db->quoteColumnName("UQ_{$constraintBase}") . " UNIQUE ({$columnName})";
206
            }
207
        }
208
209
        return 'ALTER TABLE ' . $tableName . ' ALTER COLUMN '
210
            . $columnName . ' '
211
            . $this->getColumnType($type) . "\n"
212
            . implode("\n", $sqlAfter);
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function addDefaultValue($name, $table, $column, $value)
219
    {
220
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
221
            . $this->db->quoteColumnName($name) . ' DEFAULT ' . $this->db->quoteValue($value) . ' FOR '
222
            . $this->db->quoteColumnName($column);
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    public function dropDefaultValue($name, $table)
229
    {
230
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
231
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
232
    }
233
234
    /**
235
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
236
     * The sequence will be reset such that the primary key of the next new row inserted
237
     * will have the specified value or 1.
238
     * @param string $tableName the name of the table whose primary key sequence will be reset
239
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
240
     * the next new row's primary key will have a value 1.
241
     * @return string the SQL statement for resetting sequence
242
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
243
     */
244
    public function resetSequence($tableName, $value = null)
245
    {
246
        $table = $this->db->getTableSchema($tableName);
247
        if ($table !== null && $table->sequenceName !== null) {
248
            $tableName = $this->db->quoteTableName($tableName);
249
            if ($value === null) {
250
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
251
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
252
            } else {
253
                $value = (int) $value;
254
            }
255
256
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
257
        } elseif ($table === null) {
258
            throw new InvalidArgumentException("Table not found: $tableName");
259
        }
260
261
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
262
    }
263
264
    /**
265
     * Builds a SQL statement for enabling or disabling integrity check.
266
     * @param bool $check whether to turn on or off the integrity check.
267
     * @param string $schema the schema of the tables.
268
     * @param string $table the table name.
269
     * @return string the SQL statement for checking integrity
270
     */
271
    public function checkIntegrity($check = true, $schema = '', $table = '')
272
    {
273
        $enable = $check ? 'CHECK' : 'NOCHECK';
274
        $schema = $schema ?: $this->db->getSchema()->defaultSchema;
275
        $tableNames = $this->db->getTableSchema($table) ? [$table] : $this->db->getSchema()->getTableNames($schema);
276
        $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

276
        $viewNames = $this->db->getSchema()->/** @scrutinizer ignore-call */ getViewNames($schema);
Loading history...
277
        $tableNames = array_diff($tableNames, $viewNames);
278
        $command = '';
279
280
        foreach ($tableNames as $tableName) {
281
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
282
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
283
        }
284
285
        return $command;
286
    }
287
288
     /**
289
      * Builds a SQL command for adding or updating a comment to a table or a column. The command built will check if a comment
290
      * already exists. If so, it will be updated, otherwise, it will be added.
291
      *
292
      * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
293
      * @param string $table the table to be commented or whose column is to be commented. The table name will be
294
      * properly quoted by the method.
295
      * @param string $column optional. The name of the column to be commented. If empty, the command will add the
296
      * comment to the table instead. The column name will be properly quoted by the method.
297
      * @return string the SQL statement for adding a comment.
298
      * @throws InvalidArgumentException if the table does not exist.
299
      * @since 2.0.24
300
      */
301
    protected function buildAddCommentSql($comment, $table, $column = null)
302
    {
303
        $tableSchema = $this->db->schema->getTableSchema($table);
304
305
        if ($tableSchema === null) {
306
            throw new InvalidArgumentException("Table not found: $table");
307
        }
308
309
        $schemaName = $tableSchema->schemaName ? "N'" . $tableSchema->schemaName . "'": 'SCHEMA_NAME()';
310
        $tableName = "N" . $this->db->quoteValue($tableSchema->name);
311
        $columnName = $column ? "N" . $this->db->quoteValue($column) : null;
312
        $comment = "N" . $this->db->quoteValue($comment);
313
314
        $functionParams = "
315
            @name = N'MS_description',
316
            @value = $comment,
317
            @level0type = N'SCHEMA', @level0name = $schemaName,
318
            @level1type = N'TABLE', @level1name = $tableName"
319
            . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
320
321
        return "
322
            IF NOT EXISTS (
323
                    SELECT 1
324
                    FROM fn_listextendedproperty (
325
                        N'MS_description',
326
                        'SCHEMA', $schemaName,
327
                        'TABLE', $tableName,
328
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
329
                    )
330
            )
331
                EXEC sys.sp_addextendedproperty $functionParams
332
            ELSE
333
                EXEC sys.sp_updateextendedproperty $functionParams
334
        ";
335
    }
336
337
    /**
338
     * {@inheritdoc}
339
     * @since 2.0.8
340
     */
341
    public function addCommentOnColumn($table, $column, $comment)
342
    {
343
        return $this->buildAddCommentSql($comment, $table, $column);
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     * @since 2.0.8
349
     */
350
    public function addCommentOnTable($table, $comment)
351
    {
352
        return $this->buildAddCommentSql($comment, $table);
353
    }
354
355
    /**
356
     * Builds a SQL command for removing a comment from a table or a column. The command built will check if a comment
357
     * already exists before trying to perform the removal.
358
     *
359
     * @param string $table the table that will have the comment removed or whose column will have the comment removed.
360
     * The table name will be properly quoted by the method.
361
     * @param string $column optional. The name of the column whose comment will be removed. If empty, the command
362
     * will remove the comment from the table instead. The column name will be properly quoted by the method.
363
     * @return string the SQL statement for removing the comment.
364
     * @throws InvalidArgumentException if the table does not exist.
365
     * @since 2.0.24
366
     */
367
    protected function buildRemoveCommentSql($table, $column = null)
368
    {
369
        $tableSchema = $this->db->schema->getTableSchema($table);
370
371
        if ($tableSchema === null) {
372
            throw new InvalidArgumentException("Table not found: $table");
373
        }
374
375
        $schemaName = $tableSchema->schemaName ? "N'" . $tableSchema->schemaName . "'": 'SCHEMA_NAME()';
376
        $tableName = "N" . $this->db->quoteValue($tableSchema->name);
377
        $columnName = $column ? "N" . $this->db->quoteValue($column) : null;
378
379
        return "
380
            IF EXISTS (
381
                    SELECT 1
382
                    FROM fn_listextendedproperty (
383
                        N'MS_description',
384
                        'SCHEMA', $schemaName,
385
                        'TABLE', $tableName,
386
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
387
                    )
388
            )
389
                EXEC sys.sp_dropextendedproperty
390
                    @name = N'MS_description',
391
                    @level0type = N'SCHEMA', @level0name = $schemaName,
392
                    @level1type = N'TABLE', @level1name = $tableName"
393
                    . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     * @since 2.0.8
399
     */
400
    public function dropCommentFromColumn($table, $column)
401
    {
402
        return $this->buildRemoveCommentSql($table, $column);
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     * @since 2.0.8
408
     */
409
    public function dropCommentFromTable($table)
410
    {
411
        return $this->buildRemoveCommentSql($table);
412
    }
413
414
    /**
415
     * Returns an array of column names given model name.
416
     *
417
     * @param string $modelClass name of the model class
418
     * @return array|null array of column names
419
     */
420
    protected function getAllColumnNames($modelClass = null)
421
    {
422
        if (!$modelClass) {
423
            return null;
424
        }
425
        /* @var $modelClass \yii\db\ActiveRecord */
426
        $schema = $modelClass::getTableSchema();
427
        return array_keys($schema->columns);
428
    }
429
430
    /**
431
     * @return bool whether the version of the MSSQL being used is older than 2012.
432
     * @throws \yii\base\InvalidConfigException
433
     * @throws \yii\db\Exception
434
     * @deprecated 2.0.14 Use [[Schema::getServerVersion]] with [[\version_compare()]].
435
     */
436
    protected function isOldMssql()
437
    {
438
        return version_compare($this->db->getSchema()->getServerVersion(), '11', '<');
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare($...erVersion(), '11', '<') also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     * @since 2.0.8
444
     */
445
    public function selectExists($rawSql)
446
    {
447
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
448
    }
449
450
    /**
451
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
452
     * @param string $table the table that data will be saved into.
453
     * @param array $columns the column data (name => value) to be saved into the table.
454
     * @return array normalized columns
455
     */
456
    private function normalizeTableRowData($table, $columns, &$params)
457
    {
458
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
459
            $columnSchemas = $tableSchema->columns;
460
            foreach ($columns as $name => $value) {
461
                // @see https://github.com/yiisoft/yii2/issues/12599
462
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value) || $value === null)) {
463
                    $phName = $this->bindParam($value, $params);
464
                    // @see https://github.com/yiisoft/yii2/issues/12599
465
                    $columns[$name] = new Expression("CONVERT(VARBINARY(MAX), $phName)", $params);
466
                }
467
            }
468
        }
469
470
        return $columns;
471
    }
472
473
    /**
474
     * {@inheritdoc}
475
     * Added OUTPUT construction for getting inserted data (for SQL Server 2005 or later)
476
     * OUTPUT clause - The OUTPUT clause is new to SQL Server 2005 and has the ability to access
477
     * the INSERTED and DELETED tables as is the case with a trigger.
478
     */
479
    public function insert($table, $columns, &$params)
480
    {
481
        $columns = $this->normalizeTableRowData($table, $columns, $params);
482
483
        $version2005orLater = version_compare($this->db->getSchema()->getServerVersion(), '9', '>=');
484
485
        list($names, $placeholders, $values, $params) = $this->prepareInsertValues($table, $columns, $params);
486
        if ($version2005orLater) {
487
            $schema = $this->db->getTableSchema($table);
488
            $cols = [];
489
            $columns = [];
490
            foreach ($schema->columns as $column) {
491
                if ($column->isComputed) {
0 ignored issues
show
Bug Best Practice introduced by
The property isComputed does not exist on yii\db\ColumnSchema. Since you implemented __get, consider adding a @property annotation.
Loading history...
492
                    continue;
493
                }
494
                $quoteColumnName = $this->db->quoteColumnName($column->name);
495
                $cols[] = $quoteColumnName . ' '
496
                    . $column->dbType
497
                    . (in_array($column->dbType, ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']) ? "(MAX)" : "")
498
                    . ' ' . ($column->allowNull ? "NULL" : "");
499
                $columns[] = 'INSERTED.' . $quoteColumnName;
500
            }
501
        }
502
        $countColumns = count($columns);
503
504
        $sql = 'INSERT INTO ' . $this->db->quoteTableName($table)
505
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
506
            . (($version2005orLater && $countColumns) ? ' OUTPUT ' . implode(',', $columns) . ' INTO @temporary_inserted' : '')
507
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
508
509
        if ($version2005orLater && $countColumns) {
510
            $sql = 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $cols) . ');' . $sql .
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cols does not seem to be defined for all execution paths leading up to this point.
Loading history...
511
                ';SELECT * FROM @temporary_inserted';
512
        }
513
514
        return $sql;
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     * @see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql
520
     * @see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
521
     */
522
    public function upsert($table, $insertColumns, $updateColumns, &$params)
523
    {
524
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns, $params);
525
526
        /** @var Constraint[] $constraints */
527
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
528
        if (empty($uniqueNames)) {
529
            return $this->insert($table, $insertColumns, $params);
530
        }
531
        if ($updateNames === []) {
532
            // there are no columns to update
533
            $updateColumns = false;
534
        }
535
536
        $onCondition = ['or'];
537
        $quotedTableName = $this->db->quoteTableName($table);
538
        foreach ($constraints as $constraint) {
539
            $constraintCondition = ['and'];
540
            foreach ($constraint->columnNames as $name) {
541
                $quotedName = $this->db->quoteColumnName($name);
542
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
543
            }
544
            $onCondition[] = $constraintCondition;
545
        }
546
        $on = $this->buildCondition($onCondition, $params);
547
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
548
549
        /**
550
         * Fix number of select query params for old MSSQL version that does not support offset correctly.
551
         * @see QueryBuilder::oldBuildOrderByAndLimit
552
         */
553
        $insertNamesUsing = $insertNames;
554
        if (strstr($values, 'rowNum = ROW_NUMBER()') !== false) {
555
            $insertNamesUsing = array_merge(['[rowNum]'], $insertNames);
556
        }
557
558
        $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
559
            . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNamesUsing) . ') '
560
            . "ON ($on)";
561
        $insertValues = [];
562
        foreach ($insertNames as $name) {
563
            $quotedName = $this->db->quoteColumnName($name);
564
            if (strrpos($quotedName, '.') === false) {
565
                $quotedName = '[EXCLUDED].' . $quotedName;
566
            }
567
            $insertValues[] = $quotedName;
568
        }
569
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
570
            . ' VALUES (' . implode(', ', $insertValues) . ')';
571
        if ($updateColumns === false) {
572
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
573
        }
574
575
        if ($updateColumns === true) {
576
            $updateColumns = [];
577
            foreach ($updateNames as $name) {
578
                $quotedName = $this->db->quoteColumnName($name);
579
                if (strrpos($quotedName, '.') === false) {
580
                    $quotedName = '[EXCLUDED].' . $quotedName;
581
                }
582
                $updateColumns[$name] = new Expression($quotedName);
583
            }
584
        }
585
        $updateColumns = $this->normalizeTableRowData($table, $updateColumns, $params);
586
587
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
588
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
589
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     */
595
    public function update($table, $columns, $condition, &$params)
596
    {
597
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
598
    }
599
600
    /**
601
     * {@inheritdoc}
602
     */
603
    public function getColumnType($type)
604
    {
605
        $columnType = parent::getColumnType($type);
606
        // remove unsupported keywords
607
        $columnType = preg_replace("/\s*comment '.*'/i", '', $columnType);
608
        $columnType = preg_replace('/ first$/i', '', $columnType);
609
610
        return $columnType;
611
    }
612
613
    /**
614
     * {@inheritdoc}
615
     */
616
    protected function extractAlias($table)
617
    {
618
        if (preg_match('/^\[.*\]$/', $table)) {
619
            return false;
620
        }
621
622
        return parent::extractAlias($table);
623
    }
624
625
    /**
626
     * Builds a SQL statement for dropping constraints for column of table.
627
     *
628
     * @param string $table the table whose constraint is to be dropped. The name will be properly quoted by the method.
629
     * @param string $column the column whose constraint is to be dropped. The name will be properly quoted by the method.
630
     * @param string $type type of constraint, leave empty for all type of constraints(for example: D - default, 'UQ' - unique, 'C' - check)
631
     * @see https://docs.microsoft.com/sql/relational-databases/system-catalog-views/sys-objects-transact-sql
632
     * @return string the DROP CONSTRAINTS SQL
633
     */
634
    private function dropConstraintsForColumn($table, $column, $type='')
635
    {
636
        return "DECLARE @tableName VARCHAR(MAX) = '" . $this->db->quoteTableName($table) . "'
637
DECLARE @columnName VARCHAR(MAX) = '{$column}'
638
639
WHILE 1=1 BEGIN
640
    DECLARE @constraintName NVARCHAR(128)
641
    SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id])
642
        FROM (
643
            SELECT sc.[constid] object_id
644
            FROM [sys].[sysconstraints] sc
645
            JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName
646
            WHERE sc.[id] = OBJECT_ID(@tableName)
647
            UNION
648
            SELECT object_id(i.[name]) FROM [sys].[indexes] i
649
            JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName
650
            JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id]
651
            WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName)
652
        ) cons
653
        JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id]
654
        " . (!empty($type) ? " WHERE so.[type]='{$type}'" : "") . ")
655
    IF @constraintName IS NULL BREAK
656
    EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']')
657
END";
658
    }
659
660
    /**
661
     * Drop all constraints before column delete
662
     * {@inheritdoc}
663
     */
664
    public function dropColumn($table, $column)
665
    {
666
        return $this->dropConstraintsForColumn($table, $column) . "\nALTER TABLE " . $this->db->quoteTableName($table)
667
            . " DROP COLUMN " . $this->db->quoteColumnName($column);
668
    }
669
}
670