Passed
Pull Request — master (#14)
by Wilmer
11:43
created

MssqlQueryBuilder::alterColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

264
            ? [$table] : $this->getDb()->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
265
        $viewNames = $this->getDb()->getSchema()->getViewNames($schema);
266
        $tableNames = array_diff($tableNames, $viewNames);
267
        $command = '';
268
269
        foreach ($tableNames as $tableName) {
270
            $tableName = $this->getDb()->quoteTableName("{$schema}.{$tableName}");
271
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
272
        }
273
274
        return $command;
275
    }
276
277
    /**
278
     * Builds a SQL command for adding or updating a comment to a table or a column. The command built will check if a
279
     * comment already exists. If so, it will be updated, otherwise, it will be added.
280
     *
281
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
282
     * @param string $table the table to be commented or whose column is to be commented. The table name will be
283
     * properly quoted by the method.
284
     * @param string $column optional. The name of the column to be commented. If empty, the command will add the
285
     * comment to the table instead. The column name will be properly quoted by the method.
286
     *
287
     * @return string the SQL statement for adding a comment.
288
     *
289
     * @throws InvalidArgumentException if the table does not exist.
290
     */
291
    protected function buildAddCommentSql(string $comment, string $table, string $column = null): string
292
    {
293
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
294
295
        if ($tableSchema === null) {
296
            throw new InvalidArgumentException("Table not found: $table");
297
        }
298
299
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
300
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
301
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
302
        $comment = "N" . $this->getDb()->quoteValue($comment);
303
304
        $functionParams = "
305
            @name = N'MS_description',
306
            @value = $comment,
307
            @level0type = N'SCHEMA', @level0name = $schemaName,
308
            @level1type = N'TABLE', @level1name = $tableName"
309
            . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
310
311
        return "
312
            IF NOT EXISTS (
313
                    SELECT 1
314
                    FROM fn_listextendedproperty (
315
                        N'MS_description',
316
                        'SCHEMA', $schemaName,
317
                        'TABLE', $tableName,
318
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
319
                    )
320
            )
321
                EXEC sys.sp_addextendedproperty $functionParams
322
            ELSE
323
                EXEC sys.sp_updateextendedproperty $functionParams
324
        ";
325
    }
326
327
    public function addCommentOnColumn(string $table, string $column, string $comment): string
328
    {
329
        return $this->buildAddCommentSql($comment, $table, $column);
330
    }
331
332
    public function addCommentOnTable(string $table, string $comment): string
333
    {
334
        return $this->buildAddCommentSql($comment, $table);
335
    }
336
337
    /**
338
     * Builds a SQL command for removing a comment from a table or a column. The command built will check if a comment
339
     * already exists before trying to perform the removal.
340
     *
341
     * @param string $table the table that will have the comment removed or whose column will have the comment removed.
342
     * The table name will be properly quoted by the method.
343
     * @param string|null $column optional. The name of the column whose comment will be removed. If empty, the command
344
     * will remove the comment from the table instead. The column name will be properly quoted by the method.
345
     *
346
     * @return string the SQL statement for removing the comment.
347
     *
348
     * @throws InvalidArgumentException if the table does not exist.
349
     */
350
    protected function buildRemoveCommentSql(string $table, ?string $column = null): string
351
    {
352
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
353
354
        if ($tableSchema === null) {
355
            throw new InvalidArgumentException("Table not found: $table");
356
        }
357
358
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
359
        $tableName = "N" . $this->getDb()->quoteValue($tableSchema->getName());
360
        $columnName = $column ? "N" . $this->getDb()->quoteValue($column) : null;
361
362
        return "
363
            IF EXISTS (
364
                    SELECT 1
365
                    FROM fn_listextendedproperty (
366
                        N'MS_description',
367
                        'SCHEMA', $schemaName,
368
                        'TABLE', $tableName,
369
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
370
                    )
371
            )
372
                EXEC sys.sp_dropextendedproperty
373
                    @name = N'MS_description',
374
                    @level0type = N'SCHEMA', @level0name = $schemaName,
375
                    @level1type = N'TABLE', @level1name = $tableName"
376
                    . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
377
    }
378
379
    public function dropCommentFromColumn(string $table, string $column): string
380
    {
381
        return $this->buildRemoveCommentSql($table, $column);
382
    }
383
384
    public function dropCommentFromTable(string $table): string
385
    {
386
        return $this->buildRemoveCommentSql($table);
387
    }
388
389
    /**
390
     * Returns an array of column names given model name.
391
     *
392
     * @param string $modelClass name of the model class
393
     * @return array|null array of column names
394
     */
395
    protected function getAllColumnNames($modelClass = null)
396
    {
397
        if (!$modelClass) {
398
            return null;
399
        }
400
401
        /* @var $modelClass \Yiisoft\Db\ActiveRecord */
402
        $schema = $modelClass::getTableSchema();
403
404
        return array_keys($schema->columns);
405
    }
406
407
    public function selectExists(string $rawSql): string
408
    {
409
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
410
    }
411
412
    /**
413
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
414
     *
415
     * @param string $table the table that data will be saved into.
416
     * @param array $columns the column data (name => value) to be saved into the table.
417
     *
418
     * @return array normalized columns
419
     */
420
    private function normalizeTableRowData($table, $columns, &$params)
421
    {
422
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
423
424
        if ($tableSchema !== null) {
425
            $columnSchemas = $tableSchema->getColumns();
426
            foreach ($columns as $name => $value) {
427
                /**
428
                 * {@see https://github.com/yiisoft/yii2/issues/12599}
429
                 */
430
                if (
431
                    isset($columnSchemas[$name]) &&
432
                    $columnSchemas[$name]->getDbtype() === MssqlSchema::TYPE_BINARY &&
433
                    $columnSchemas[$name]->getDbType() === 'varbinary' && is_string($value)
434
                ) {
435
                    $exParams = [];
436
                    $phName = $this->bindParam($value, $exParams);
437
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
438
                }
439
            }
440
        }
441
442
        return $columns;
443
    }
444
    /**
445
     * Added OUTPUT construction for getting inserted data (for SQL Server 2005 or later) OUTPUT clause - The OUTPUT
446
     * clause is new to SQL Server 2005 and has the ability to access the INSERTED and DELETED tables as is the case
447
     * with a trigger.
448
     */
449
    public function insert(string $table, $columns, array &$params = []): string
450
    {
451
        $columns = $this->normalizeTableRowData($table, $columns, $params);
452
453
        $version2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
454
455
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
456
457
        $sql = 'INSERT INTO ' . $this->getDb()->quoteTableName($table)
458
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
459
            . ($version2005orLater ? ' OUTPUT INSERTED.* INTO @temporary_inserted' : '')
460
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
461
462
        if ($version2005orLater) {
463
            $schema = $this->getDb()->getTableSchema($table);
464
            $cols = [];
465
            foreach ($schema->getColumns() as $column) {
466
                $cols[] = $this->getDb()->quoteColumnName($column->getName()) . ' '
467
                    . $column->getDbType()
468
                    . (in_array(
469
                        $column->getDbType(),
470
                        ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']
471
                    ) ? "(MAX)" : "")
472
                    . ' ' . ($column->isAllowNull() ? "NULL" : "");
473
            }
474
            $sql = "SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (" . implode(", ", $cols) . ");"
475
                . $sql . ";SELECT * FROM @temporary_inserted";
476
        }
477
478
        return $sql;
479
    }
480
481
    /**
482
     * {@see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql}
483
     * {@see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx}
484
     */
485
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
486
    {
487
        /** @var Constraint[] $constraints */
488
        $constraints = [];
489
490
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
491
            $table,
492
            $insertColumns,
493
            $updateColumns,
494
            $constraints
495
        );
496
497
        if (empty($uniqueNames)) {
498
            return $this->insert($table, $insertColumns, $params);
499
        }
500
501
        $onCondition = ['or'];
502
        $quotedTableName = $this->getDb()->quoteTableName($table);
503
504
        foreach ($constraints as $constraint) {
505
            $constraintCondition = ['and'];
506
507
            foreach ($constraint->getColumnNames() as $name) {
508
                $quotedName = $this->getDb()->quoteColumnName($name);
509
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
510
            }
511
512
            $onCondition[] = $constraintCondition;
513
        }
514
515
        $on = $this->buildCondition($onCondition, $params);
516
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
517
        $mergeSql = 'MERGE ' . $this->getDb()->quoteTableName($table) . ' WITH (HOLDLOCK) '
518
            . 'USING (' . (!empty($placeholders)
519
            ? 'VALUES (' . implode(', ', $placeholders) . ')'
520
            : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
521
522
        $insertValues = [];
523
524
        foreach ($insertNames as $name) {
525
            $quotedName = $this->getDb()->quoteColumnName($name);
526
527
            if (strrpos($quotedName, '.') === false) {
528
                $quotedName = '[EXCLUDED].' . $quotedName;
529
            }
530
531
            $insertValues[] = $quotedName;
532
        }
533
534
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
535
536
        if ($updateColumns === false) {
537
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
538
        }
539
540
        if ($updateColumns === true) {
541
            $updateColumns = [];
542
543
            foreach ($updateNames as $name) {
544
                $quotedName = $this->getDb()->quoteColumnName($name);
545
                if (strrpos($quotedName, '.') === false) {
546
                    $quotedName = '[EXCLUDED].' . $quotedName;
547
                }
548
549
                $updateColumns[$name] = new Expression($quotedName);
550
            }
551
        }
552
553
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
554
555
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
556
557
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
558
    }
559
560
    public function update(string $table, array $columns, $condition, array &$params = []): string
561
    {
562
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
563
    }
564
565
    public function getColumnType($type): string
566
    {
567
        $columnType = parent::getColumnType($type);
568
569
        /* remove unsupported keywords*/
570
        $columnType = preg_replace("/\s*comment '.*'/i", '', $columnType);
571
        $columnType = preg_replace('/ first$/i', '', $columnType);
572
573
        return $columnType;
574
    }
575
576
    protected function extractAlias($table)
577
    {
578
        if (preg_match('/^\[.*\]$/', $table)) {
579
            return false;
580
        }
581
582
        return parent::extractAlias($table);
583
    }
584
}
585