Passed
Push — master ( add693...b324e9 )
by Wilmer
13:51
created

MssqlQueryBuilder::buildRemoveCommentSql()   A

Complexity

Conditions 6
Paths 17

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

362
            ? [$table] : $this->getDb()->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
363
        $viewNames = $this->getDb()->getSchema()->getViewNames($schema);
364
        $tableNames = array_diff($tableNames, $viewNames);
365
        $command = '';
366
367
        foreach ($tableNames as $tableName) {
368
            $tableName = $this->getDb()->quoteTableName("{$schema}.{$tableName}");
369
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
370
        }
371
372
        return $command;
373
    }
374
375
    /**
376
     * Builds a SQL command for adding or updating a comment to a table or a column. The command built will check if a
377
     * comment already exists. If so, it will be updated, otherwise, it will be added.
378
     *
379
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
380
     * @param string $table the table to be commented or whose column is to be commented. The table name will be
381
     * properly quoted by the method.
382
     * @param string $column optional. The name of the column to be commented. If empty, the command will add the
383
     * comment to the table instead. The column name will be properly quoted by the method.
384
     *
385
     * @throws Exception
386
     * @throws InvalidArgumentException if the table does not exist.
387
     * @throws InvalidConfigException
388
     * @throws NotSupportedException
389
     *
390
     * @return string the SQL statement for adding a comment.
391
     */
392
    protected function buildAddCommentSql(string $comment, string $table, string $column = null): string
393
    {
394
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
395
396
        if ($tableSchema === null) {
397
            throw new InvalidArgumentException("Table not found: $table");
398
        }
399
400
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
401
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
402
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
403
        $comment = "N" . $this->getDb()->quoteValue($comment);
404
405
        $functionParams = "
406
            @name = N'MS_description',
407
            @value = $comment,
408
            @level0type = N'SCHEMA', @level0name = $schemaName,
409
            @level1type = N'TABLE', @level1name = $tableName"
410
            . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
411
412
        return "
413
            IF NOT EXISTS (
414
                    SELECT 1
415
                    FROM fn_listextendedproperty (
416
                        N'MS_description',
417
                        'SCHEMA', $schemaName,
418
                        'TABLE', $tableName,
419
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
420
                    )
421
            )
422
                EXEC sys.sp_addextendedproperty $functionParams
423
            ELSE
424
                EXEC sys.sp_updateextendedproperty $functionParams
425
        ";
426
    }
427
428
    /**
429
     * Builds a SQL command for adding comment to column.
430
     *
431
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
432
     * method.
433
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
434
     * method.
435
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
436
     *
437
     * @throws Exception
438
     * @throws InvalidConfigException
439
     * @throws NotSupportedException
440
     *
441
     * @return string the SQL statement for adding comment on column.
442
     */
443
    public function addCommentOnColumn(string $table, string $column, string $comment): string
444
    {
445
        return $this->buildAddCommentSql($comment, $table, $column);
446
    }
447
448
    /**
449
     * Builds a SQL command for adding comment to table.
450
     *
451
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
452
     * method.
453
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
454
     *
455
     * @throws Exception
456
     * @throws InvalidConfigException
457
     * @throws NotSupportedException
458
     *
459
     * @return string the SQL statement for adding comment on table.
460
     */
461
    public function addCommentOnTable(string $table, string $comment): string
462
    {
463
        return $this->buildAddCommentSql($comment, $table);
464
    }
465
466
    /**
467
     * Builds a SQL command for removing a comment from a table or a column. The command built will check if a comment
468
     * already exists before trying to perform the removal.
469
     *
470
     * @param string $table the table that will have the comment removed or whose column will have the comment removed.
471
     * The table name will be properly quoted by the method.
472
     * @param string|null $column optional. The name of the column whose comment will be removed. If empty, the command
473
     * will remove the comment from the table instead. The column name will be properly quoted by the method.
474
     *
475
     * @throws Exception
476
     * @throws InvalidArgumentException if the table does not exist.
477
     * @throws InvalidConfigException
478
     * @throws NotSupportedException
479
     *
480
     * @return string the SQL statement for removing the comment.
481
     */
482
    protected function buildRemoveCommentSql(string $table, ?string $column = null): string
483
    {
484
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
485
486
        if ($tableSchema === null) {
487
            throw new InvalidArgumentException("Table not found: $table");
488
        }
489
490
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
491
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
492
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
493
494
        return "
495
            IF EXISTS (
496
                    SELECT 1
497
                    FROM fn_listextendedproperty (
498
                        N'MS_description',
499
                        'SCHEMA', $schemaName,
500
                        'TABLE', $tableName,
501
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
502
                    )
503
            )
504
                EXEC sys.sp_dropextendedproperty
505
                    @name = N'MS_description',
506
                    @level0type = N'SCHEMA', @level0name = $schemaName,
507
                    @level1type = N'TABLE', @level1name = $tableName"
508
                    . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
509
    }
510
511
    /**
512
     * Builds a SQL command for adding comment to column.
513
     *
514
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
515
     * method.
516
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
517
     * method.
518
     *
519
     * @throws Exception
520
     * @throws InvalidConfigException
521
     * @throws NotSupportedException
522
     *
523
     * @return string the SQL statement for adding comment on column.
524
     */
525
    public function dropCommentFromColumn(string $table, string $column): string
526
    {
527
        return $this->buildRemoveCommentSql($table, $column);
528
    }
529
530
    /**
531
     * Builds a SQL command for adding comment to table.
532
     *
533
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
534
     * method.
535
     *
536
     * @throws Exception
537
     * @throws InvalidConfigException
538
     * @throws NotSupportedException
539
     *
540
     * @return string the SQL statement for adding comment on column.
541
     */
542
    public function dropCommentFromTable(string $table): string
543
    {
544
        return $this->buildRemoveCommentSql($table);
545
    }
546
547
    /**
548
     * Returns an array of column names given model name.
549
     *
550
     * @param string|null $modelClass name of the model class.
551
     *
552
     * @return array|null array of column names
553
     */
554
    protected function getAllColumnNames(string $modelClass = null): ?array
555
    {
556
        if (!$modelClass) {
557
            return null;
558
        }
559
560
        $schema = $modelClass::getTableSchema();
561
562
        return array_keys($schema->columns);
563
    }
564
565
    /**
566
     * Creates a SELECT EXISTS() SQL statement.
567
     *
568
     * @param string $rawSql the subquery in a raw form to select from.
569
     *
570
     * @return string the SELECT EXISTS() SQL statement.
571
     */
572
    public function selectExists(string $rawSql): string
573
    {
574
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
575
    }
576
577
    /**
578
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
579
     *
580
     * @param string $table the table that data will be saved into.
581
     * @param MssqlColumnSchema|array $columns the column data (name => value) to be saved into the table.
582
     * @param array $params
583
     *
584
     * @throws Exception
585
     * @throws InvalidConfigException
586
     * @throws NotSupportedException
587
     *
588
     * @return MssqlColumnSchema|array normalized columns.
589
     */
590
    private function normalizeTableRowData(string $table, $columns, array &$params = [])
591
    {
592
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
593
594
        if ($tableSchema !== null) {
595
            $columnSchemas = $tableSchema->getColumns();
596
            foreach ($columns as $name => $value) {
597
                /**
598
                 * {@see https://github.com/yiisoft/yii2/issues/12599}
599
                 */
600
                if (
601
                    isset($columnSchemas[$name]) &&
602
                    $columnSchemas[$name]->getDbtype() === MssqlSchema::TYPE_BINARY &&
603
                    $columnSchemas[$name]->getDbType() === 'varbinary' && is_string($value)
604
                ) {
605
                    $exParams = [];
606
                    $phName = $this->bindParam($value, $exParams);
607
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
608
                }
609
            }
610
        }
611
612
        return $columns;
613
    }
614
615
    /**
616
     * Creates an INSERT SQL statement.
617
     *
618
     * For example,
619
     *
620
     * ```php
621
     * $sql = $queryBuilder->insert('user', [
622
     *     'name' => 'Sam',
623
     *     'age' => 30,
624
     * ], $params);
625
     * ```
626
     *
627
     * The method will properly escape the table and column names.
628
     *
629
     * @param string $table the table that new rows will be inserted into.
630
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
631
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
632
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
633
     * DB command later.
634
     *
635
     * @throws Exception
636
     * @throws InvalidArgumentException
637
     * @throws InvalidConfigException
638
     * @throws NotSupportedException
639
     *
640
     * @return string the INSERT SQL
641
     */
642
    public function insert(string $table, $columns, array &$params = []): string
643
    {
644
        $columns = $this->normalizeTableRowData($table, $columns, $params);
645
646
        $version2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
647
648
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
649
650
        $sql = 'INSERT INTO ' . $this->getDb()->quoteTableName($table)
651
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
652
            . ($version2005orLater ? ' OUTPUT INSERTED.* INTO @temporary_inserted' : '')
653
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
654
655
        if ($version2005orLater) {
656
            $schema = $this->getDb()->getTableSchema($table);
657
            $cols = [];
658
            foreach ($schema->getColumns() as $column) {
659
                $cols[] = $this->getDb()->quoteColumnName($column->getName()) . ' '
660
                    . $column->getDbType()
661
                    . (in_array(
662
                        $column->getDbType(),
663
                        ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']
664
                    ) ? "(MAX)" : "")
665
                    . ' ' . ($column->isAllowNull() ? "NULL" : "");
666
            }
667
            $sql = "SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (" . implode(", ", $cols) . ");"
668
                . $sql . ";SELECT * FROM @temporary_inserted";
669
        }
670
671
        return $sql;
672
    }
673
674
    /**
675
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
676
     * constraints), or update them if they do.
677
     *
678
     * For example,
679
     *
680
     * ```php
681
     * $sql = $queryBuilder->upsert('pages', [
682
     *     'name' => 'Front page',
683
     *     'url' => 'http://example.com/', // url is unique
684
     *     'visits' => 0,
685
     * ], [
686
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
687
     * ], $params);
688
     * ```
689
     *
690
     * The method will properly escape the table and column names.
691
     *
692
     * @param string $table the table that new rows will be inserted into/updated in.
693
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
694
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
695
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
696
     * If `true` is passed, the column data will be updated to match the insert column data.
697
     * If `false` is passed, no update will be performed if the column data already exists.
698
     * @param array $params the binding parameters that will be generated by this method.
699
     * They should be bound to the DB command later.
700
     *
701
     * @throws Exception
702
     * @throws InvalidConfigException
703
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
704
     *
705
     * @return string the resulting SQL.
706
     *
707
     * {@see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql}
708
     * {@see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx}
709
     */
710
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
711
    {
712
        /** @var Constraint[] $constraints */
713
        $constraints = [];
714
715
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
716
            $table,
717
            $insertColumns,
718
            $updateColumns,
719
            $constraints
720
        );
721
722
        if (empty($uniqueNames)) {
723
            return $this->insert($table, $insertColumns, $params);
724
        }
725
726
        $onCondition = ['or'];
727
        $quotedTableName = $this->getDb()->quoteTableName($table);
728
729
        foreach ($constraints as $constraint) {
730
            $constraintCondition = ['and'];
731
732
            foreach ($constraint->getColumnNames() as $name) {
733
                $quotedName = $this->getDb()->quoteColumnName($name);
734
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
735
            }
736
737
            $onCondition[] = $constraintCondition;
738
        }
739
740
        $on = $this->buildCondition($onCondition, $params);
741
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
742
        $mergeSql = 'MERGE ' . $this->getDb()->quoteTableName($table) . ' WITH (HOLDLOCK) '
743
            . 'USING (' . (!empty($placeholders)
744
            ? 'VALUES (' . implode(', ', $placeholders) . ')'
745
            : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
746
747
        $insertValues = [];
748
749
        foreach ($insertNames as $name) {
750
            $quotedName = $this->getDb()->quoteColumnName($name);
751
752
            if (strrpos($quotedName, '.') === false) {
753
                $quotedName = '[EXCLUDED].' . $quotedName;
754
            }
755
756
            $insertValues[] = $quotedName;
757
        }
758
759
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
760
761
        if ($updateColumns === false) {
762
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
763
        }
764
765
        if ($updateColumns === true) {
766
            $updateColumns = [];
767
768
            foreach ($updateNames as $name) {
769
                $quotedName = $this->getDb()->quoteColumnName($name);
770
                if (strrpos($quotedName, '.') === false) {
771
                    $quotedName = '[EXCLUDED].' . $quotedName;
772
                }
773
774
                $updateColumns[$name] = new Expression($quotedName);
775
            }
776
        }
777
778
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
779
780
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
781
782
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
783
    }
784
785
    /**
786
     * Creates an UPDATE SQL statement.
787
     *
788
     * For example,
789
     *
790
     * ```php
791
     * $params = [];
792
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
793
     * ```
794
     *
795
     * The method will properly escape the table and column names.
796
     *
797
     * @param string $table the table to be updated.
798
     * @param array $columns the column data (name => value) to be updated.
799
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
800
     * {@see Query::where()} on how to specify condition.
801
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
802
     * DB command later.
803
     *
804
     * @throws Exception
805
     * @throws InvalidArgumentException
806
     * @throws InvalidConfigException
807
     * @throws NotSupportedException
808
     *
809
     * @return string the UPDATE SQL.
810
     */
811
    public function update(string $table, array $columns, $condition, array &$params = []): string
812
    {
813
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
814
    }
815
816
    /**
817
     * Converts an abstract column type into a physical column type.
818
     *
819
     * The conversion is done using the type map specified in {@see typeMap}.
820
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
821
     * physical types):
822
     *
823
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
824
     *    KEY".
825
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
826
     *    PRIMARY KEY".
827
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
828
     *    AUTO_INCREMENT PRIMARY KEY".
829
     * - `char`: char type, will be converted into "char(1)".
830
     * - `string`: string type, will be converted into "varchar(255)".
831
     * - `text`: a long string type, will be converted into "text".
832
     * - `smallint`: a small integer type, will be converted into "smallint(6)".
833
     * - `integer`: integer type, will be converted into "int(11)".
834
     * - `bigint`: a big integer type, will be converted into "bigint(20)".
835
     * - `boolean`: boolean type, will be converted into "tinyint(1)".
836
     * - `float``: float number type, will be converted into "float".
837
     * - `decimal`: decimal number type, will be converted into "decimal".
838
     * - `datetime`: datetime type, will be converted into "datetime".
839
     * - `timestamp`: timestamp type, will be converted into "timestamp".
840
     * - `time`: time type, will be converted into "time".
841
     * - `date`: date type, will be converted into "date".
842
     * - `money`: money type, will be converted into "decimal(19,4)".
843
     * - `binary`: binary data type, will be converted into "blob".
844
     *
845
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
846
     * part will be converted, and the rest of the parts will be appended to the converted result.
847
     *
848
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
849
     *
850
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
851
     * brackets directly to the type.
852
     *
853
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
854
     * not support these kind of constraints for a type it will be ignored.
855
     *
856
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
857
     *
858
     * @param string|ColumnSchemaBuilder $type abstract column type
859
     *
860
     * @return string physical column type.
861
     */
862
    public function getColumnType($type): string
863
    {
864
        $columnType = parent::getColumnType($type);
865
866
        /* remove unsupported keywords*/
867
        $columnType = preg_replace("/\s*comment '.*'/i", '', $columnType);
868
        $columnType = preg_replace('/ first$/i', '', $columnType);
869
870
        return $columnType;
871
    }
872
873
    /**
874
     * Extracts table alias if there is one or returns false
875
     *
876
     * @param $table
877
     *
878
     * @return bool|array
879
     */
880
    protected function extractAlias($table)
881
    {
882
        if (preg_match('/^\[.*\]$/', $table)) {
883
            return false;
884
        }
885
886
        return parent::extractAlias($table);
887
    }
888
}
889