Passed
Pull Request — master (#56)
by Def
06:27
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 13
Paths 85

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 13

Importance

Changes 0
Metric Value
cc 13
eloc 31
nc 85
nop 4
dl 0
loc 50
ccs 19
cts 19
cp 1
crap 13
rs 6.6166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

344
            ? [$table] : $db->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
345
        $viewNames = $db->getSchema()->getViewNames($schema);
346
        $tableNames = array_diff($tableNames, $viewNames);
347
        $command = '';
348
349
        foreach ($tableNames as $tableName) {
350
            $tableName = $db->quoteTableName("{$schema}.{$tableName}");
351
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
352
        }
353
354
        return $command;
355
    }
356
357
    /**
358
     * Builds a SQL command for adding or updating a comment to a table or a column. The command built will check if a
359
     * comment already exists. If so, it will be updated, otherwise, it will be added.
360
     *
361
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
362
     * @param string $table the table to be commented or whose column is to be commented. The table name will be
363
     * properly quoted by the method.
364
     * @param string|null $column optional. The name of the column to be commented. If empty, the command will add the
365
     * comment to the table instead. The column name will be properly quoted by the method.
366
     *
367 4
     * @throws Exception|InvalidArgumentException if the table does not exist.
368
     *
369 4
     * @return string the SQL statement for adding a comment.
370
     */
371 4
    protected function buildAddCommentSql(string $comment, string $table, ?string $column = null): string
372
    {
373
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
374
375 4
        if ($tableSchema === null) {
376 4
            throw new InvalidArgumentException("Table not found: $table");
377 4
        }
378 4
379
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
380 4
        $tableName = 'N' . $this->getDb()->quoteValue($tableSchema->getName());
381
        $columnName = $column ? 'N' . $this->getDb()->quoteValue($column) : null;
382 4
        $comment = 'N' . $this->getDb()->quoteValue($comment);
383 4
384 4
        $functionParams = "
385 4
            @name = N'MS_description',
386
            @value = $comment,
387 4
            @level0type = N'SCHEMA', @level0name = $schemaName,
388
            @level1type = N'TABLE', @level1name = $tableName"
389
            . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
390
391
        return "
392 4
            IF NOT EXISTS (
393 4
                    SELECT 1
394 4
                    FROM fn_listextendedproperty (
395
                        N'MS_description',
396
                        'SCHEMA', $schemaName,
397 4
                        'TABLE', $tableName,
398
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
399 4
                    )
400
            )
401
                EXEC sys.sp_addextendedproperty $functionParams
402
            ELSE
403
                EXEC sys.sp_updateextendedproperty $functionParams
404
        ";
405
    }
406
407
    /**
408
     * Builds a SQL command for adding comment to column.
409
     *
410
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
411
     * method.
412
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
413
     * method.
414
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
415
     *
416 4
     * @throws Exception
417
     *
418 4
     * @return string the SQL statement for adding comment on column.
419
     */
420
    public function addCommentOnColumn(string $table, string $column, string $comment): string
421
    {
422
        return $this->buildAddCommentSql($comment, $table, $column);
423
    }
424
425
    /**
426
     * Builds a SQL command for adding comment to table.
427
     *
428
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
429
     * method.
430
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
431
     *
432 4
     * @throws Exception
433
     *
434 4
     * @return string the SQL statement for adding comment on table.
435
     */
436
    public function addCommentOnTable(string $table, string $comment): string
437
    {
438
        return $this->buildAddCommentSql($comment, $table);
439
    }
440
441
    /**
442
     * Builds a SQL command for removing a comment from a table or a column. The command built will check if a comment
443
     * already exists before trying to perform the removal.
444
     *
445
     * @param string $table the table that will have the comment removed or whose column will have the comment removed.
446
     * The table name will be properly quoted by the method.
447
     * @param string|null $column optional. The name of the column whose comment will be removed. If empty, the command
448
     * will remove the comment from the table instead. The column name will be properly quoted by the method.
449
     *
450 2
     * @throws Exception|InvalidArgumentException if the table does not exist.
451
     *
452 2
     * @return string the SQL statement for removing the comment.
453
     */
454 2
    protected function buildRemoveCommentSql(string $table, ?string $column = null): string
455
    {
456
        $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
457
458 2
        if ($tableSchema === null) {
459 2
            throw new InvalidArgumentException("Table not found: $table");
460 2
        }
461
462 2
        $schemaName = $tableSchema->getSchemaName() ? "N'" . $tableSchema->getSchemaName() . "'" : 'SCHEMA_NAME()';
463
        $tableName = 'N' . $this->getDb()->quoteValue($tableSchema->getName());
464
        $columnName = $column ? 'N' . $this->getDb()->quoteValue($column) : null;
465
466
        return "
467 2
            IF EXISTS (
468 2
                    SELECT 1
469 2
                    FROM fn_listextendedproperty (
470
                        N'MS_description',
471
                        'SCHEMA', $schemaName,
472
                        'TABLE', $tableName,
473
                        " . ($column ? "'COLUMN', $columnName " : ' DEFAULT, DEFAULT ') . "
474 2
                    )
475 2
            )
476 2
                EXEC sys.sp_dropextendedproperty
477
                    @name = N'MS_description',
478
                    @level0type = N'SCHEMA', @level0name = $schemaName,
479
                    @level1type = N'TABLE', @level1name = $tableName"
480
                    . ($column ? ", @level2type = N'COLUMN', @level2name = $columnName" : '') . ';';
481
    }
482
483
    /**
484
     * Builds a SQL command for adding comment to column.
485
     *
486
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
487
     * method.
488
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
489
     * method.
490
     *
491 2
     * @throws Exception
492
     *
493 2
     * @return string the SQL statement for adding comment on column.
494
     */
495
    public function dropCommentFromColumn(string $table, string $column): string
496
    {
497
        return $this->buildRemoveCommentSql($table, $column);
498
    }
499
500
    /**
501
     * Builds a SQL command for adding comment to table.
502
     *
503
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
504
     * method.
505
     *
506 2
     * @throws Exception
507
     *
508 2
     * @return string the SQL statement for adding comment on column.
509
     */
510
    public function dropCommentFromTable(string $table): string
511
    {
512
        return $this->buildRemoveCommentSql($table);
513
    }
514
515
    /**
516
     * Returns an array of column names given model name.
517
     *
518
     * @param string|null $modelClass name of the model class.
519
     *
520
     * @return array|null array of column names
521
     */
522
    protected function getAllColumnNames(string $modelClass = null): ?array
523
    {
524
        if (!$modelClass) {
525
            return null;
526
        }
527
528
        $schema = $modelClass::getTableSchema();
529
530
        return array_keys($schema->columns);
531
    }
532
533
    /**
534
     * Creates a SELECT EXISTS() SQL statement.
535
     *
536 1
     * @param string $rawSql the subquery in a raw form to select from.
537
     *
538 1
     * @return string the SELECT EXISTS() SQL statement.
539
     */
540
    public function selectExists(string $rawSql): string
541
    {
542
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
543
    }
544
545
    /**
546
     * Creates an INSERT SQL statement.
547
     *
548
     * For example,
549
     *
550 27
     * ```php
551
     * $sql = $queryBuilder->insert('user', [
552 27
     *     'name' => 'Sam',
553
     *     'age' => 30,
554 27
     * ], $params);
555 27
     * ```
556 27
     *
557
     * The method will properly escape the table and column names.
558
     *
559
     * @param string $table the table that new rows will be inserted into.
560
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
561 23
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
562 23
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
563 23
     * DB command later.
564
     *
565
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
566
     *
567
     * @return string the INSERT SQL.
568
     */
569
    public function insert(string $table, $columns, array &$params = []): string
570
    {
571
        $version2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
572
573 27
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
574
575
        $sql = 'INSERT INTO ' . $this->getDb()->quoteTableName($table)
576
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
577
            . ($version2005orLater ? ' OUTPUT INSERTED.* INTO @temporary_inserted' : '')
578
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
579
580
        if ($version2005orLater) {
581
            $schema = $this->getDb()->getTableSchema($table);
582
583
            $cols = [];
584
            foreach ($schema->getColumns() as $column) {
585
                $cols[] = $this->getDb()->quoteColumnName($column->getName()) . ' '
586
                    . $column->getDbType()
587
                    . (in_array(
588
                        $column->getDbType(),
589
                        ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']
590
                    ) ? '(MAX)' : '')
591
                    . ' ' . ($column->isAllowNull() ? 'NULL' : '');
592
            }
593
594
            $sql = 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $cols) . ');'
595
                . $sql . ';SELECT * FROM @temporary_inserted';
596
        }
597
598
        return $sql;
599
    }
600 26
601
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
602 26
    {
603
        if (empty($rows)) {
604 26
            return '';
605
        }
606 26
607
        $schema = $this->getDb()->getSchema();
608 23
609 23
610 23
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
611 23
            $columnSchemas = $tableSchema->getColumns();
612
        } else {
613 23
            $columnSchemas = [];
614 23
        }
615
616 23
        $values = [];
617 23
618 23
        foreach ($rows as $row) {
619 23
            $vs = [];
620
            foreach ($row as $i => $value) {
621 23
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
622
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
623 23
                }
624 23
                if (is_string($value)) {
625
                    $value = $schema->quoteValue($value);
626
                } elseif (is_float($value)) {
627 23
                    /* ensure type cast always has . as decimal separator in all locales */
628 23
                    $value = NumericHelper::normalize((string) $value);
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\NumericHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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