Passed
Pull Request — master (#28)
by
unknown
06:43
created

QueryBuilder::checkIntegrity()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 12
c 0
b 0
f 0
nc 8
nop 3
dl 0
loc 19
ccs 0
cts 13
cp 0
crap 30
rs 9.5555
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql;
6
7
use JsonException;
8
use Yiisoft\Db\Connection\ConnectionInterface;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Mssql\Condition\InConditionBuilder;
16
use Yiisoft\Db\Mssql\Condition\LikeConditionBuilder;
17
use Yiisoft\Db\Query\Conditions\InCondition;
18
use Yiisoft\Db\Query\Conditions\LikeCondition;
19
use Yiisoft\Db\Query\Query;
20
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
21
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
22
23
use function array_diff;
24
use function array_keys;
25
use function implode;
26
use function in_array;
27
use function is_string;
28
use function ltrim;
29
use function preg_match;
30
use function preg_replace;
31
use function reset;
32
use function strrpos;
33
use function version_compare;
34
35
/**
36
 * QueryBuilder is the query builder for MS SQL Server databases (version 2008 and above).
37
 */
38
final class QueryBuilder extends AbstractQueryBuilder
39
{
40
    /**
41
     * @var array mapping from abstract column types (keys) to physical column types (values).
42
     */
43
    protected array $typeMap = [
44
        Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY',
45
        Schema::TYPE_UPK => 'int IDENTITY PRIMARY KEY',
46
        Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY',
47
        Schema::TYPE_UBIGPK => 'bigint IDENTITY PRIMARY KEY',
48
        Schema::TYPE_CHAR => 'nchar(1)',
49
        Schema::TYPE_STRING => 'nvarchar(255)',
50
        Schema::TYPE_TEXT => 'nvarchar(max)',
51
        Schema::TYPE_TINYINT => 'tinyint',
52
        Schema::TYPE_SMALLINT => 'smallint',
53
        Schema::TYPE_INTEGER => 'int',
54
        Schema::TYPE_BIGINT => 'bigint',
55
        Schema::TYPE_FLOAT => 'float',
56
        Schema::TYPE_DOUBLE => 'float',
57
        Schema::TYPE_DECIMAL => 'decimal(18,0)',
58
        Schema::TYPE_DATETIME => 'datetime',
59
        Schema::TYPE_TIMESTAMP => 'datetime',
60
        Schema::TYPE_TIME => 'time',
61
        Schema::TYPE_DATE => 'date',
62
        Schema::TYPE_BINARY => 'varbinary(max)',
63
        Schema::TYPE_BOOLEAN => 'bit',
64
        Schema::TYPE_MONEY => 'decimal(19,4)',
65
    ];
66
67 249
    protected function defaultExpressionBuilders(): array
68
    {
69 249
        return array_merge(parent::defaultExpressionBuilders(), [
70 249
            InCondition::class => InConditionBuilder::class,
71
            LikeCondition::class => LikeConditionBuilder::class,
72
        ]);
73
    }
74
75
    /**
76
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
77
     *
78
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
79
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details
80
     * on how to specify this
81
     * parameter.
82
     * @param object|int|null $limit the limit number. See {@see Query::limit} for more details.
83
     * @param object|int|null $offset the offset number. See {@see Query::offset} for more
84
     * details.
85
     * @param array $params the binding parameters to be populated.
86
     *
87
     * @throws Exception|InvalidArgumentException
88
     *
89
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
90
     */
91 171
    public function buildOrderByAndLimit(string $sql, array $orderBy, $limit, $offset, array &$params = []): string
92
    {
93 171
        if (!$this->hasOffset($offset) && !$this->hasLimit($limit)) {
94 162
            $orderBy = $this->buildOrderBy($orderBy, $params);
95
96 162
            return $orderBy === '' ? $sql : $sql . $this->separator . $orderBy;
97
        }
98
99 13
        if (version_compare($this->getDb()->getSchema()->getServerVersion(), '11', '<')) {
100
            return $this->oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset, $params);
101
        }
102
103 13
        return $this->newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset, $params);
104
    }
105
106
    /**
107
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2012 or newer.
108
     *
109
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
110
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details on how to specify this
111
     * parameter.
112
     * @param Query|int|null $limit the limit number. See {@see Query::limit} for more details.
113
     * @param Query|int|null $offset the offset number. See {@see Query::offset} for more details.
114
     * @param array $params the binding parameters to be populated.
115
     *
116
     * @throws Exception|InvalidArgumentException
117
     *
118
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
119
     */
120 13
    protected function newBuildOrderByAndLimit(
121
        string $sql,
122
        array $orderBy,
123
        $limit,
124
        $offset,
125
        array &$params = []
126
    ): string {
127 13
        $orderBy = $this->buildOrderBy($orderBy, $params);
128
129 13
        if ($orderBy === '') {
130
            /** ORDER BY clause is required when FETCH and OFFSET are in the SQL */
131 10
            $orderBy = 'ORDER BY (SELECT NULL)';
132
        }
133
134 13
        $sql .= $this->separator . $orderBy;
135
136
        /**
137
         * {@see http://technet.microsoft.com/en-us/library/gg699618.aspx}
138
         */
139 13
        $offset = $this->hasOffset($offset) ? $offset : '0';
140 13
        $sql .= $this->separator . "OFFSET $offset ROWS";
141
142 13
        if ($this->hasLimit($limit)) {
143 12
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
144
        }
145
146 13
        return $sql;
147
    }
148
149
    /**
150
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
151
     *
152
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
153
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details on how to specify this
154
     * parameter.
155
     * @param Query|int|null $limit the limit number. See {@see Query::limit} for more details.
156
     * @param Query|int|null $offset the offset number. See {@see Query::offset} for more details.
157
     * @param array $params the binding parameters to be populated.
158
     *
159
     * @throws Exception|InvalidArgumentException
160
     *
161
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
162
     */
163
    protected function oldBuildOrderByAndLimit(
164
        string $sql,
165
        array $orderBy,
166
        $limit,
167
        $offset,
168
        array &$params = []
169
    ): string {
170
        $orderBy = $this->buildOrderBy($orderBy, $params);
171
172
        if ($orderBy === '') {
173
            /** ROW_NUMBER() requires an ORDER BY clause */
174
            $orderBy = 'ORDER BY (SELECT NULL)';
175
        }
176
177
        $sql = preg_replace(
178
            '/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i',
179
            "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),",
180
            $sql
181
        );
182
183
        if ($this->hasLimit($limit)) {
184
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
185
        } else {
186
            $sql = "SELECT * FROM ($sql) sub";
187
        }
188
189
        if ($this->hasOffset($offset)) {
190
            $sql .= $this->separator . "WHERE rowNum > $offset";
191
        }
192
193
        return $sql;
194
    }
195
196
    /**
197
     * Builds a SQL statement for renaming a DB table.
198
     *
199
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
200
     * @param string $newName the new table name. The name will be properly quoted by the method.
201
     *
202
     * @return string the SQL statement for renaming a DB table.
203
     */
204 2
    public function renameTable(string $oldName, string $newName): string
205
    {
206
        return 'sp_rename ' .
207 2
            $this->getDb()->quoteTableName($oldName) . ', ' . $this->getDb()->quoteTableName($newName);
208
    }
209
210
    /**
211
     * Builds a SQL statement for renaming a column.
212
     *
213
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
214
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
215
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
216
     *
217
     * @return string the SQL statement for renaming a DB column.
218
     */
219
    public function renameColumn(string $table, string $oldName, string $newName): string
220
    {
221
        $table = $this->getDb()->quoteTableName($table);
222
        $oldName = $this->getDb()->quoteColumnName($oldName);
223
        $newName = $this->getDb()->quoteColumnName($newName);
224
225
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
226
    }
227
228
    /**
229
     * Builds a SQL statement for changing the definition of a column.
230
     *
231
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
232
     * method.
233
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
234
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column
235
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
236
     * generated SQL.
237
     *
238
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
239
     * 'varchar(255) not null'.
240
     *
241
     * @return string the SQL statement for changing the definition of a column.
242
     */
243 1
    public function alterColumn(string $table, string $column, string $type): string
244
    {
245 1
        $type = $this->getColumnType($type);
246
247 1
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ALTER COLUMN '
248 1
            . $this->getDb()->quoteColumnName($column) . ' '
249 1
            . $this->getColumnType($type);
250
    }
251
252
    /**
253
     * Creates a SQL command for adding a default value constraint to an existing table.
254
     *
255
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
256
     * @param string $table the table that the default value constraint will be added to. The name will be properly
257
     * quoted by the method.
258
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
259
     * quoted by the method.
260
     * @param mixed $value default value.
261
     *
262
     * @return string the SQL statement for adding a default value constraint to an existing table.
263
     */
264 1
    public function addDefaultValue(string $name, string $table, string $column, $value): string
265
    {
266 1
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ADD CONSTRAINT '
267 1
            . $this->getDb()->quoteColumnName($name) . ' DEFAULT ' . $this->getDb()->quoteValue($value) . ' FOR '
268 1
            . $this->getDb()->quoteColumnName($column);
269
    }
270
271
    /**
272
     * Creates a SQL command for dropping a default value constraint.
273
     *
274
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
275
     * the method.
276
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
277
     * by the method.
278
     *
279
     * @return string the SQL statement for dropping a default value constraint.
280
     */
281 1
    public function dropDefaultValue(string $name, string $table): string
282
    {
283
        return 'ALTER TABLE ' .
284 1
            $this->getDb()->quoteTableName($table) . ' DROP CONSTRAINT ' . $this->getDb()->quoteColumnName($name);
285
    }
286
287
    /**
288
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
289
     *
290
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
291
     * or 1.
292
     *
293
     * @param string $tableName the name of the table whose primary key sequence will be reset.
294
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
295
     * row's primary key will have a value 1.
296
     *
297
     * @throws Exception|InvalidArgumentException if the table does not exist or there is no sequence associated with
298
     * the table.
299
     *
300
     * @return string the SQL statement for resetting sequence.
301
     */
302
    public function resetSequence(string $tableName, $value = null): string
303
    {
304
        $table = $this->getDb()->getTableSchema($tableName);
305
306
        if ($table !== null && $table->getSequenceName() !== null) {
307
            $tableName = $this->getDb()->quoteTableName($tableName);
308
309
            if ($value === null) {
310
                $pk = $table->getPrimaryKey();
311
                $key = $this->getDb()->quoteColumnName(reset($pk));
312
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
313
            } else {
314
                $value = (int)$value;
315
            }
316
317
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
318
        }
319
320
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
321
    }
322
323
    /**
324
     * Builds a SQL statement for enabling or disabling integrity check.
325
     *
326
     * @param string $schema the schema of the tables.
327
     * @param string $table the table name.
328
     * @param bool $check whether to turn on or off the integrity check.
329
     *
330
     * @return string the SQL statement for checking integrity.
331
     */
332
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
333
    {
334
        /** @psalm-var Connection $db */
335
        $db = $this->getDb();
336
337
        $enable = $check ? 'CHECK' : 'NOCHECK';
338
        $schema = $schema ?: $db->getSchema()->getDefaultSchema();
339
        $tableNames = $db->getTableSchema($table)
340
            ? [$table] : $db->getSchema()->getTableNames($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $schema of Yiisoft\Db\Schema\Schema::getTableNames() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

340
            ? [$table] : $db->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
341
        $viewNames = $db->getSchema()->getViewNames($schema);
342
        $tableNames = array_diff($tableNames, $viewNames);
343
        $command = '';
344
345
        foreach ($tableNames as $tableName) {
346
            $tableName = $db->quoteTableName("{$schema}.{$tableName}");
347
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
348
        }
349
350
        return $command;
351
    }
352
353
    /**
354
     * Builds a SQL command for adding or updating a comment to a table or a column. The command built will check if a
355
     * comment already exists. If so, it will be updated, otherwise, it will be added.
356
     *
357
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
358
     * @param string $table the table to be commented or whose column is to be commented. The table name will be
359
     * properly quoted by the method.
360
     * @param string|null $column optional. The name of the column to be commented. If empty, the command will add the
361
     * comment to the table instead. The column name will be properly quoted by the method.
362
     *
363
     * @throws Exception|InvalidArgumentException if the table does not exist.
364
     *
365
     * @return string the SQL statement for adding a comment.
366
     */
367 4
    protected function buildAddCommentSql(string $comment, string $table, ?string $column = null): string
368
    {
369 4
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
370
371 4
        if ($tableSchema === null) {
372
            throw new InvalidArgumentException("Table not found: $table");
373
        }
374
375 4
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
376 4
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
377 4
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
378 4
        $comment = "N" . $this->getDb()->quoteValue($comment);
379
380 4
        $functionParams = "
381
            @name = N'MS_description',
382 4
            @value = $comment,
383 4
            @level0type = N'SCHEMA', @level0name = $schemaName,
384 4
            @level1type = N'TABLE', @level1name = $tableName"
385 4
            . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
386
387 4
        return "
388
            IF NOT EXISTS (
389
                    SELECT 1
390
                    FROM fn_listextendedproperty (
391
                        N'MS_description',
392 4
                        'SCHEMA', $schemaName,
393 4
                        'TABLE', $tableName,
394 4
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
395
                    )
396
            )
397 4
                EXEC sys.sp_addextendedproperty $functionParams
398
            ELSE
399 4
                EXEC sys.sp_updateextendedproperty $functionParams
400
        ";
401
    }
402
403
    /**
404
     * Builds a SQL command for adding comment to column.
405
     *
406
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
407
     * method.
408
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
409
     * method.
410
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
411
     *
412
     * @throws Exception
413
     *
414
     * @return string the SQL statement for adding comment on column.
415
     */
416 4
    public function addCommentOnColumn(string $table, string $column, string $comment): string
417
    {
418 4
        return $this->buildAddCommentSql($comment, $table, $column);
419
    }
420
421
    /**
422
     * Builds a SQL command for adding comment to table.
423
     *
424
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
425
     * method.
426
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
427
     *
428
     * @throws Exception
429
     *
430
     * @return string the SQL statement for adding comment on table.
431
     */
432 4
    public function addCommentOnTable(string $table, string $comment): string
433
    {
434 4
        return $this->buildAddCommentSql($comment, $table);
435
    }
436
437
    /**
438
     * Builds a SQL command for removing a comment from a table or a column. The command built will check if a comment
439
     * already exists before trying to perform the removal.
440
     *
441
     * @param string $table the table that will have the comment removed or whose column will have the comment removed.
442
     * The table name will be properly quoted by the method.
443
     * @param string|null $column optional. The name of the column whose comment will be removed. If empty, the command
444
     * will remove the comment from the table instead. The column name will be properly quoted by the method.
445
     *
446
     * @throws Exception|InvalidArgumentException if the table does not exist.
447
     *
448
     * @return string the SQL statement for removing the comment.
449
     */
450 2
    protected function buildRemoveCommentSql(string $table, ?string $column = null): string
451
    {
452 2
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
453
454 2
        if ($tableSchema === null) {
455
            throw new InvalidArgumentException("Table not found: $table");
456
        }
457
458 2
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
459 2
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
460 2
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
461
462 2
        return "
463
            IF EXISTS (
464
                    SELECT 1
465
                    FROM fn_listextendedproperty (
466
                        N'MS_description',
467 2
                        'SCHEMA', $schemaName,
468 2
                        'TABLE', $tableName,
469 2
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
470
                    )
471
            )
472
                EXEC sys.sp_dropextendedproperty
473
                    @name = N'MS_description',
474 2
                    @level0type = N'SCHEMA', @level0name = $schemaName,
475 2
                    @level1type = N'TABLE', @level1name = $tableName"
476 2
                    . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
477
    }
478
479
    /**
480
     * Builds a SQL command for adding comment to column.
481
     *
482
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
483
     * method.
484
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
485
     * method.
486
     *
487
     * @throws Exception
488
     *
489
     * @return string the SQL statement for adding comment on column.
490
     */
491 2
    public function dropCommentFromColumn(string $table, string $column): string
492
    {
493 2
        return $this->buildRemoveCommentSql($table, $column);
494
    }
495
496
    /**
497
     * Builds a SQL command for adding comment to table.
498
     *
499
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
500
     * method.
501
     *
502
     * @throws Exception
503
     *
504
     * @return string the SQL statement for adding comment on column.
505
     */
506 2
    public function dropCommentFromTable(string $table): string
507
    {
508 2
        return $this->buildRemoveCommentSql($table);
509
    }
510
511
    /**
512
     * Returns an array of column names given model name.
513
     *
514
     * @param string|null $modelClass name of the model class.
515
     *
516
     * @return array|null array of column names
517
     */
518
    protected function getAllColumnNames(string $modelClass = null): ?array
519
    {
520
        if (!$modelClass) {
521
            return null;
522
        }
523
524
        $schema = $modelClass::getTableSchema();
525
526
        return array_keys($schema->columns);
527
    }
528
529
    /**
530
     * Creates a SELECT EXISTS() SQL statement.
531
     *
532
     * @param string $rawSql the subquery in a raw form to select from.
533
     *
534
     * @return string the SELECT EXISTS() SQL statement.
535
     */
536 1
    public function selectExists(string $rawSql): string
537
    {
538 1
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
539
    }
540
541
    /**
542
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
543
     *
544
     * @param string $table the table that data will be saved into.
545
     * @param ColumnSchema|array $columns the column data (name => value) to be saved into the table.
546
     * @param array $params
547
     *
548
     * @return ColumnSchema|array normalized columns.
549
     */
550 27
    private function normalizeTableRowData(string $table, $columns, array &$params = [])
551
    {
552 27
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
553
554 27
        if ($tableSchema !== null) {
555 27
            $columnSchemas = $tableSchema->getColumns();
556 27
            foreach ($columns as $name => $value) {
557
                /**
558
                 * {@see https://github.com/yiisoft/yii2/issues/12599}
559
                 */
560
                if (
561 23
                    isset($columnSchemas[$name]) &&
562 23
                    $columnSchemas[$name]->getDbtype() === Schema::TYPE_BINARY &&
563 23
                    $columnSchemas[$name]->getDbType() === 'varbinary' && is_string($value)
564
                ) {
565
                    $exParams = [];
566
                    $phName = $this->bindParam($value, $exParams);
567
                    /** @psalm-suppress UndefinedMethod */
568
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
569
                }
570
            }
571
        }
572
573 27
        return $columns;
574
    }
575
576
    /**
577
     * Creates an INSERT SQL statement.
578
     *
579
     * For example,
580
     *
581
     * ```php
582
     * $sql = $queryBuilder->insert('user', [
583
     *     'name' => 'Sam',
584
     *     'age' => 30,
585
     * ], $params);
586
     * ```
587
     *
588
     * The method will properly escape the table and column names.
589
     *
590
     * @param string $table the table that new rows will be inserted into.
591
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
592
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
593
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
594
     * DB command later.
595
     *
596
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
597
     *
598
     * @return string the INSERT SQL
599
     */
600 26
    public function insert(string $table, $columns, array &$params = []): string
601
    {
602 26
        $columns = $this->normalizeTableRowData($table, $columns, $params);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type Yiisoft\Db\Query\Query; however, parameter $columns of Yiisoft\Db\Mssql\QueryBu...normalizeTableRowData() does only seem to accept Yiisoft\Db\Mssql\ColumnSchema|array, maybe add an additional type check? ( Ignorable by Annotation )

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

602
        $columns = $this->normalizeTableRowData($table, /** @scrutinizer ignore-type */ $columns, $params);
Loading history...
603
604 26
        $version2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
605
606 26
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
607
608 23
        $sql = 'INSERT INTO ' . $this->getDb()->quoteTableName($table)
609 23
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
610 23
            . ($version2005orLater ? ' OUTPUT INSERTED.* INTO @temporary_inserted' : '')
611 23
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
612
613 23
        if ($version2005orLater) {
614 23
            $schema = $this->getDb()->getTableSchema($table);
615
616 23
            $cols = [];
617 23
            foreach ($schema->getColumns() as $column) {
618 23
                $cols[] = $this->getDb()->quoteColumnName($column->getName()) . ' '
619 23
                    . $column->getDbType()
620
                    . (in_array(
621 23
                        $column->getDbType(),
622
                        ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']
623 23
                    ) ? "(MAX)" : "")
624 23
                    . ' ' . ($column->isAllowNull() ? "NULL" : "");
625
            }
626
627 23
            $sql = "SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (" . implode(", ", $cols) . ");"
628 23
                . $sql . ";SELECT * FROM @temporary_inserted";
629
        }
630
631 23
        return $sql;
632
    }
633
634
    /**
635
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
636
     * constraints), or update them if they do.
637
     *
638
     * For example,
639
     *
640
     * ```php
641
     * $sql = $queryBuilder->upsert('pages', [
642
     *     'name' => 'Front page',
643
     *     'url' => 'http://example.com/', // url is unique
644
     *     'visits' => 0,
645
     * ], [
646
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
647
     * ], $params);
648
     * ```
649
     *
650
     * The method will properly escape the table and column names.
651
     *
652
     * @param string $table the table that new rows will be inserted into/updated in.
653
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
654
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
655
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
656
     * If `true` is passed, the column data will be updated to match the insert column data.
657
     * If `false` is passed, no update will be performed if the column data already exists.
658
     * @param array $params the binding parameters that will be generated by this method.
659
     * They should be bound to the DB command later.
660
     *
661
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
662
     * underlying DBMS.
663
     *
664
     * @return string the resulting SQL.
665
     *
666
     * {@see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql}
667
     * {@see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx}
668
     */
669 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
670
    {
671
        /** @var Constraint[] $constraints */
672 18
        $constraints = [];
673
674 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
675 18
            $table,
676
            $insertColumns,
677
            $updateColumns,
678
            $constraints
679
        );
680
681 18
        if (empty($uniqueNames)) {
682 3
            return $this->insert($table, $insertColumns, $params);
683
        }
684
685 15
        $onCondition = ['or'];
686 15
        $quotedTableName = $this->getDb()->quoteTableName($table);
687
688 15
        foreach ($constraints as $constraint) {
689 15
            $constraintCondition = ['and'];
690
691 15
            foreach ($constraint->getColumnNames() as $name) {
692 15
                $quotedName = $this->getDb()->quoteColumnName($name);
693 15
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
694
            }
695
696 15
            $onCondition[] = $constraintCondition;
697
        }
698
699 15
        $on = $this->buildCondition($onCondition, $params);
700
701 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
702
703 15
        $mergeSql = 'MERGE ' . $this->getDb()->quoteTableName($table) . ' WITH (HOLDLOCK) '
704 15
            . 'USING (' . (!empty($placeholders)
705 7
            ? 'VALUES (' . implode(', ', $placeholders) . ')'
706 15
            : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
707
708 15
        $insertValues = [];
709
710 15
        foreach ($insertNames as $name) {
711 15
            $quotedName = $this->getDb()->quoteColumnName($name);
712
713 15
            if (strrpos($quotedName, '.') === false) {
714 15
                $quotedName = '[EXCLUDED].' . $quotedName;
715
            }
716
717 15
            $insertValues[] = $quotedName;
718
        }
719
720 15
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
721
722 15
        if ($updateColumns === false) {
723 5
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
724
        }
725
726 10
        if ($updateColumns === true) {
727 4
            $updateColumns = [];
728
729 4
            foreach ($updateNames as $name) {
730 4
                $quotedName = $this->getDb()->quoteColumnName($name);
731 4
                if (strrpos($quotedName, '.') === false) {
732 4
                    $quotedName = '[EXCLUDED].' . $quotedName;
733
                }
734
735 4
                $updateColumns[$name] = new Expression($quotedName);
736
            }
737
        }
738
739 10
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
740
741 10
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
742
743 10
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
744
    }
745
746
    /**
747
     * Creates an UPDATE SQL statement.
748
     *
749
     * For example,
750
     *
751
     * ```php
752
     * $params = [];
753
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
754
     * ```
755
     *
756
     * The method will properly escape the table and column names.
757
     *
758
     * @param string $table the table to be updated.
759
     * @param array $columns the column data (name => value) to be updated.
760
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
761
     * {@see Query::where()} on how to specify condition.
762
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
763
     * DB command later.
764
     *
765
     * @throws Exception|InvalidArgumentException
766
     *
767
     * @return string the UPDATE SQL.
768
     */
769 2
    public function update(string $table, array $columns, $condition, array &$params = []): string
770
    {
771 2
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
772
    }
773
774
    /**
775
     * Converts an abstract column type into a physical column type.
776
     *
777
     * The conversion is done using the type map specified in {@see typeMap}.
778
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
779
     * physical types):
780
     *
781
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
782
     *    KEY".
783
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
784
     *    PRIMARY KEY".
785
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
786
     *    AUTO_INCREMENT PRIMARY KEY".
787
     * - `char`: char type, will be converted into "char(1)".
788
     * - `string`: string type, will be converted into "varchar(255)".
789
     * - `text`: a long string type, will be converted into "text".
790
     * - `smallint`: a small integer type, will be converted into "smallint(6)".
791
     * - `integer`: integer type, will be converted into "int(11)".
792
     * - `bigint`: a big integer type, will be converted into "bigint(20)".
793
     * - `boolean`: boolean type, will be converted into "tinyint(1)".
794
     * - `float``: float number type, will be converted into "float".
795
     * - `decimal`: decimal number type, will be converted into "decimal".
796
     * - `datetime`: datetime type, will be converted into "datetime".
797
     * - `timestamp`: timestamp type, will be converted into "timestamp".
798
     * - `time`: time type, will be converted into "time".
799
     * - `date`: date type, will be converted into "date".
800
     * - `money`: money type, will be converted into "decimal(19,4)".
801
     * - `binary`: binary data type, will be converted into "blob".
802
     *
803
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
804
     * part will be converted, and the rest of the parts will be appended to the converted result.
805
     *
806
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
807
     *
808
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
809
     * brackets directly to the type.
810
     *
811
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
812
     * not support these kind of constraints for a type it will be ignored.
813
     *
814
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
815
     *
816
     * @param string|ColumnSchemaBuilder $type abstract column type
817
     *
818
     * @return string physical column type.
819
     */
820 12
    public function getColumnType($type): string
821
    {
822 12
        $columnType = parent::getColumnType($type);
823
824
        /** remove unsupported keywords*/
825 12
        $columnType = preg_replace("/\s*comment '.*'/i", '', $columnType);
826 12
        $columnType = preg_replace('/ first$/i', '', $columnType);
827
828 12
        return $columnType;
829
    }
830
831
    /**
832
     * Extracts table alias if there is one or returns false
833
     *
834
     * @param $table
835
     *
836
     * @return bool|array
837
     */
838 57
    protected function extractAlias($table)
839
    {
840 57
        if (preg_match('/^\[.*]$/', $table)) {
841
            return false;
842
        }
843
844 57
        return parent::extractAlias($table);
845
    }
846
}
847