Passed
Push — master ( e5e54a...8779b5 )
by Wilmer
10:52
created

QueryBuilder::getColumnType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql;
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\ColumnSchema;
16
use Yiisoft\Db\Mssql\Schema;
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
    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(
127
        string $sql,
128
        array $orderBy,
129
        $limit,
130
        $offset,
131
        array &$params = []
132
    ): string {
133
        $orderBy = $this->buildOrderBy($orderBy, $params);
134
135
        if ($orderBy === '') {
136
            /* ORDER BY clause is required when FETCH and OFFSET are in the SQL */
137
            $orderBy = 'ORDER BY (SELECT NULL)';
138
        }
139
140
        $sql .= $this->separator . $orderBy;
141
142
        /**
143
         * {@see http://technet.microsoft.com/en-us/library/gg699618.aspx}
144
         */
145
        $offset = $this->hasOffset($offset) ? $offset : '0';
146
        $sql .= $this->separator . "OFFSET $offset ROWS";
147
148
        if ($this->hasLimit($limit)) {
149
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
150
        }
151
152
        return $sql;
153
    }
154
155
    /**
156
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
157
     *
158
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
159
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details on how to specify this
160
     * parameter.
161
     * @param Query|int|null $limit the limit number. See {@see Query::limit} for more details.
162
     * @param Query|int|null $offset the offset number. See {@see Query::offset} for more details.
163
     * @param array $params the binding parameters to be populated.
164
     *
165
     * @throws Exception
166
     * @throws InvalidArgumentException
167
     * @throws InvalidConfigException
168
     * @throws NotSupportedException
169
     *
170
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
171
     */
172
    protected function oldBuildOrderByAndLimit(
173
        string $sql,
174
        array $orderBy,
175
        $limit,
176
        $offset,
177
        array &$params = []
178
    ): string {
179
        $orderBy = $this->buildOrderBy($orderBy, $params);
180
181
        if ($orderBy === '') {
182
            /* ROW_NUMBER() requires an ORDER BY clause */
183
            $orderBy = 'ORDER BY (SELECT NULL)';
184
        }
185
186
        $sql = preg_replace(
187
            '/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i',
188
            "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),",
189
            $sql
190
        );
191
192
        if ($this->hasLimit($limit)) {
193
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
194
        } else {
195
            $sql = "SELECT * FROM ($sql) sub";
196
        }
197
198
        if ($this->hasOffset($offset)) {
199
            $sql .= $this->separator . "WHERE rowNum > $offset";
200
        }
201
202
        return $sql;
203
    }
204
205
    /**
206
     * Builds a SQL statement for renaming a DB table.
207
     *
208
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
209
     * @param string $newName the new table name. The name will be properly quoted by the method.
210
     *
211
     * @throws Exception
212
     * @throws InvalidConfigException
213
     * @throws NotSupportedException
214
     *
215
     * @return string the SQL statement for renaming a DB table.
216
     */
217
    public function renameTable(string $oldName, string $newName): string
218
    {
219
        return 'sp_rename ' .
220
            $this->getDb()->quoteTableName($oldName) . ', ' . $this->getDb()->quoteTableName($newName);
221
    }
222
223
    /**
224
     * Builds a SQL statement for renaming a column.
225
     *
226
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
227
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
228
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
229
     *
230
     * @throws Exception
231
     * @throws InvalidConfigException
232
     * @throws NotSupportedException
233
     *
234
     * @return string the SQL statement for renaming a DB column.
235
     */
236
    public function renameColumn(string $table, string $oldName, string $newName): string
237
    {
238
        $table = $this->getDb()->quoteTableName($table);
239
        $oldName = $this->getDb()->quoteColumnName($oldName);
240
        $newName = $this->getDb()->quoteColumnName($newName);
241
242
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
243
    }
244
245
    /**
246
     * Builds a SQL statement for changing the definition of a column.
247
     *
248
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
249
     * method.
250
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
251
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column
252
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
253
     * generated SQL.
254
     *
255
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
256
     * 'varchar(255) not null'.
257
     *
258
     * @throws Exception
259
     * @throws InvalidConfigException
260
     * @throws NotSupportedException
261
     *
262
     * @return string the SQL statement for changing the definition of a column.
263
     */
264
    public function alterColumn(string $table, string $column, string $type): string
265
    {
266
        $type = $this->getColumnType($type);
267
        $sql = 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ALTER COLUMN '
268
            . $this->getDb()->quoteColumnName($column) . ' '
269
            . $this->getColumnType($type);
270
271
        return $sql;
272
    }
273
274
    /**
275
     * Creates a SQL command for adding a default value constraint to an existing table.
276
     *
277
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
278
     * @param string $table the table that the default value constraint will be added to. The name will be properly
279
     * quoted by the method.
280
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
281
     * quoted by the method.
282
     * @param mixed $value default value.
283
     *
284
     * @throws Exception
285
     * @throws InvalidConfigException
286
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
287
     *
288
     * @return string the SQL statement for adding a default value constraint to an existing table.
289
     */
290
    public function addDefaultValue(string $name, string $table, string $column, $value): string
291
    {
292
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ADD CONSTRAINT '
293
            . $this->getDb()->quoteColumnName($name) . ' DEFAULT ' . $this->getDb()->quoteValue($value) . ' FOR '
294
            . $this->getDb()->quoteColumnName($column);
295
    }
296
297
    /**
298
     * Creates a SQL command for dropping a default value constraint.
299
     *
300
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
301
     * the method.
302
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
303
     * by the method.
304
     *
305
     * @throws Exception
306
     * @throws InvalidConfigException
307
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
308
     *
309
     * @return string the SQL statement for dropping a default value constraint.
310
     */
311
    public function dropDefaultValue(string $name, string $table): string
312
    {
313
        return 'ALTER TABLE ' .
314
            $this->getDb()->quoteTableName($table) . ' DROP CONSTRAINT ' . $this->getDb()->quoteColumnName($name);
315
    }
316
317
    /**
318
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
319
     *
320
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
321
     * or 1.
322
     *
323
     * @param string $tableName the name of the table whose primary key sequence will be reset.
324
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
325
     * row's primary key will have a value 1.
326
     *
327
     * @throws Exception
328
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
329
     * @throws InvalidConfigException
330
     * @throws NotSupportedException
331
     *
332
     * @return string the SQL statement for resetting sequence.
333
     */
334
    public function resetSequence(string $tableName, $value = null): string
335
    {
336
        $table = $this->getDb()->getTableSchema($tableName);
337
338
        if ($table !== null && $table->getSequenceName() !== null) {
339
            $tableName = $this->getDb()->quoteTableName($tableName);
340
341
            if ($value === null) {
342
                $key = $this->getDb()->quoteColumnName(reset($table->getPrimaryKey()));
343
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
344
            } else {
345
                $value = (int)$value;
346
            }
347
348
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
349
        }
350
351
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
352
    }
353
354
    /**
355
     * Builds a SQL statement for enabling or disabling integrity check.
356
     *
357
     * @param string $schema the schema of the tables.
358
     * @param string $table the table name.
359
     * @param bool $check whether to turn on or off the integrity check.
360
     *
361
     * @throws Exception
362
     * @throws InvalidConfigException
363
     * @throws NotSupportedException
364
     *
365
     * @return string the SQL statement for checking integrity.
366
     */
367
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
368
    {
369
        $enable = $check ? 'CHECK' : 'NOCHECK';
370
        $schema = $schema ?: $this->getDb()->getSchema()->getDefaultSchema();
371
        $tableNames = $this->getDb()->getTableSchema($table)
372
            ? [$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

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