Passed
Pull Request — master (#14)
by Wilmer
25:49 queued 10:47
created

MssqlQueryBuilder::getAllColumnNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
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,
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\Query\Y...\Conditions\InCondition was not found. Did you mean Yiisoft\Db\Query\Conditions\InCondition? If so, make sure to prefix the type with \.
Loading history...
Bug introduced by
The type Yiisoft\Db\Mssql\Query\Y...tion\InConditionBuilder was not found. Did you mean Yiisoft\Db\Mssql\Condition\InConditionBuilder? If so, make sure to prefix the type with \.
Loading history...
49
            Yiisoft\Db\Query\Conditions\LikeCondition::class => Yiisoft\Db\Mssql\Condition\LikeConditionBuilder::class,
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\Query\Y...onditions\LikeCondition was not found. Did you mean Yiisoft\Db\Query\Conditions\LikeCondition? If so, make sure to prefix the type with \.
Loading history...
Bug introduced by
The type Yiisoft\Db\Mssql\Query\Y...on\LikeConditionBuilder was not found. Did you mean Yiisoft\Db\Mssql\Condition\LikeConditionBuilder? If so, make sure to prefix the type with \.
Loading history...
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 int $limit the limit number. See {@see \Yiisoft\Db\Query::limit} for more details.
75
     * @param int $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($sql, $orderBy, $limit, $offset, &$params)
81
    {
82
        $orderBy = $this->buildOrderBy($orderBy, $params);
83
        if ($orderBy === '') {
84
            // ORDER BY clause is required when FETCH and OFFSET are in the SQL
85
            $orderBy = 'ORDER BY (SELECT NULL)';
86
        }
87
88
        $sql .= $this->separator . $orderBy;
89
90
        // http://technet.microsoft.com/en-us/library/gg699618.aspx
91
        $offset = $this->hasOffset($offset) ? $offset : '0';
92
        $sql .= $this->separator . "OFFSET $offset ROWS";
93
94
        if ($this->hasLimit($limit)) {
95
            $sql .= $this->separator . "FETCH NEXT $limit ROWS ONLY";
96
        }
97
98
        return $sql;
99
    }
100
101
    /**
102
     * Builds the ORDER BY/LIMIT/OFFSET clauses for SQL SERVER 2005 to 2008.
103
     *
104
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
105
     * @param array $orderBy the order by columns. See {@see \Yiisoft\Db\Query::orderBy} for more details on how to
106
     * specify this parameter.
107
     * @param int $limit the limit number. See {@see \Yiisoft\Db\Query::limit} for more details.
108
     * @param int $offset the offset number. See {@see \Yiisoft\Db\Query::offset} for more details.
109
     * @param array $params the binding parameters to be populated.
110
     *
111
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
112
     */
113
    protected function oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset, &$params)
114
    {
115
        $orderBy = $this->buildOrderBy($orderBy, $params);
116
        if ($orderBy === '') {
117
            // ROW_NUMBER() requires an ORDER BY clause
118
            $orderBy = 'ORDER BY (SELECT NULL)';
119
        }
120
121
        $sql = preg_replace(
122
            '/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i',
123
            "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),",
124
            $sql
125
        );
126
127
        if ($this->hasLimit($limit)) {
128
            $sql = "SELECT TOP $limit * FROM ($sql) sub";
129
        } else {
130
            $sql = "SELECT * FROM ($sql) sub";
131
        }
132
133
        if ($this->hasOffset($offset)) {
134
            $sql .= $this->separator . "WHERE rowNum > $offset";
135
        }
136
137
        return $sql;
138
    }
139
140
    /**
141
     * Builds a SQL statement for renaming a DB table.
142
     *
143
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
144
     * @param string $newName the new table name. The name will be properly quoted by the method.
145
     *
146
     * @return string the SQL statement for renaming a DB table.
147
     */
148
    public function renameTable(string $oldName, string $newName): string
149
    {
150
        return 'sp_rename ' .
151
            $this->getDb()->quoteTableName($oldName) . ', ' . $this->getDb()->quoteTableName($newName);
152
    }
153
154
    /**
155
     * Builds a SQL statement for renaming a column.
156
     *
157
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
158
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
159
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
160
     *
161
     * @return string the SQL statement for renaming a DB column.
162
     */
163
    public function renameColumn(string $table, string $oldName, string $newName): string
164
    {
165
        $table = $this->getDb()->quoteTableName($table);
166
        $oldName = $this->getDb()->quoteColumnName($oldName);
167
        $newName = $this->getDb()->quoteColumnName($newName);
168
169
        return "sp_rename '{$table}.{$oldName}', {$newName}, 'COLUMN'";
170
    }
171
172
    /**
173
     * Builds a SQL statement for changing the definition of a column.
174
     *
175
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
176
     * method.
177
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
178
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column
179
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
180
     * generated SQL.
181
     *
182
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
183
     * 'varchar(255) not null'.
184
     *
185
     * @return string the SQL statement for changing the definition of a column.
186
     */
187
    public function alterColumn(string $table, string $column, string $type): string
188
    {
189
        $type = $this->getColumnType($type);
190
        $sql = 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ALTER COLUMN '
191
            . $this->getDb()->quoteColumnName($column) . ' '
192
            . $this->getColumnType($type);
193
194
        return $sql;
195
    }
196
197
    public function addDefaultValue(string $name, string $table, string $column, $value): string
198
    {
199
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' ADD CONSTRAINT '
200
            . $this->getDb()->quoteColumnName($name) . ' DEFAULT ' . $this->getDb()->quoteValue($value) . ' FOR '
201
            . $this->getDb()->quoteColumnName($column);
202
    }
203
204
    public function dropDefaultValue(string $name, string $table): string
205
    {
206
        return 'ALTER TABLE ' .
207
            $this->getDb()->quoteTableName($table) . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
0 ignored issues
show
Bug introduced by
The method quoteColumnName() does not exist on null. ( Ignorable by Annotation )

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

207
            $this->getDb()->quoteTableName($table) . ' DROP CONSTRAINT ' . $this->db->/** @scrutinizer ignore-call */ quoteColumnName($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
208
    }
209
210
    /**
211
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
212
     *
213
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
214
     * or 1.
215
     *
216
     * @param string $tableName the name of the table whose primary key sequence will be reset.
217
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
218
     * row's primary key will have a value 1.
219
     *
220
     * @return string the SQL statement for resetting sequence.
221
     *
222
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
223
     */
224
    public function resetSequence(string $tableName, $value = null): string
225
    {
226
        $table = $this->getDb()->getTableSchema($tableName);
227
228
        if ($table !== null && $table->getSequenceName() !== null) {
229
            $tableName = $this->db->quoteTableName($tableName);
230
231
            if ($value === null) {
232
                $key = $this->getDb()->quoteColumnName(reset($table->getPrimaryKey()));
233
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
234
            } else {
235
                $value = (int) $value;
236
            }
237
238
            return "DBCC CHECKIDENT ('{$tableName}', RESEED, {$value})";
239
        } elseif ($table === null) {
240
            throw new InvalidArgumentException("Table not found: $tableName");
241
        }
242
243
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
244
    }
245
246
    /**
247
     * Builds a SQL statement for enabling or disabling integrity check.
248
     *
249
     * @param bool $check whether to turn on or off the integrity check.
250
     * @param string $schema the schema of the tables.
251
     * @param string $table the table name.
252
     *
253
     * @return string the SQL statement for checking integrity.
254
     */
255
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
256
    {
257
        $enable = $check ? 'CHECK' : 'NOCHECK';
258
        $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...
259
        $tableNames = $this->getDb()->getTableSchema($table)
260
            ? [$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

260
            ? [$table] : $this->getDb()->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
261
        $viewNames = $this->getDb()->getSchema()->getViewNames($schema);
262
        $tableNames = array_diff($tableNames, $viewNames);
263
        $command = '';
264
265
        foreach ($tableNames as $tableName) {
266
            $tableName = $this->getDb()->quoteTableName("{$schema}.{$tableName}");
267
            $command .= "ALTER TABLE $tableName $enable CONSTRAINT ALL; ";
268
        }
269
270
        return $command;
271
    }
272
273
    public function addCommentOnColumn(string $table, string $column, string $comment): string
274
    {
275
        return "sp_updateextendedproperty @name = N'MS_Description', @value = {$this->db->quoteValue($comment)}, @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}, @level2type = N'Column', @level2name = {$this->db->quoteColumnName($column)}";
276
    }
277
278
    public function addCommentOnTable(string $table, string $comment): string
279
    {
280
        return "sp_updateextendedproperty @name = N'MS_Description', @value = {$this->db->quoteValue($comment)}, @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
281
    }
282
283
    public function dropCommentFromColumn(string $table, string $column): string
284
    {
285
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}, @level2type = N'Column', @level2name = {$this->db->quoteColumnName($column)}";
286
    }
287
288
    public function dropCommentFromTable(string $table): string
289
    {
290
        return "sp_dropextendedproperty @name = N'MS_Description', @level1type = N'Table',  @level1name = {$this->db->quoteTableName($table)}";
291
    }
292
293
    /**
294
     * Returns an array of column names given model name.
295
     *
296
     * @param string $modelClass name of the model class
297
     * @return array|null array of column names
298
     */
299
    protected function getAllColumnNames($modelClass = null)
300
    {
301
        if (!$modelClass) {
302
            return null;
303
        }
304
        /* @var $modelClass \Yiisoft\Db\ActiveRecord */
305
        $schema = $modelClass::getTableSchema();
306
        return array_keys($schema->columns);
307
    }
308
309
    public function selectExists(string $rawSql): string
310
    {
311
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END';
312
    }
313
314
    /**
315
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
316
     *
317
     * @param string $table the table that data will be saved into.
318
     * @param array $columns the column data (name => value) to be saved into the table.
319
     *
320
     * @return array normalized columns
321
     */
322
    private function normalizeTableRowData($table, $columns, &$params)
323
    {
324
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
325
            $columnSchemas = $tableSchema->columns;
0 ignored issues
show
Bug introduced by
The property columns is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
326
            foreach ($columns as $name => $value) {
327
                // @see https://github.com/yiisoft/yii2/issues/12599
328
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === MssqlSchema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && is_string($value)) {
329
                    $exParams = [];
330
                    $phName = $this->bindParam($value, $exParams);
331
                    $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
332
                }
333
            }
334
        }
335
336
        return $columns;
337
    }
338
339
    public function insert(string $table, $columns, array &$params = []): string
340
    {
341
        return parent::insert($table, $this->normalizeTableRowData($table, $columns, $params), $params);
342
    }
343
344
    /**
345
     * {@inheritdoc}
346
     *
347
     * @see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql
348
     * @see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
349
     */
350
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
351
    {
352
        /** @var Constraint[] $constraints */
353
        $constraints = [];
354
355
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
356
            $table,
357
            $insertColumns,
358
            $updateColumns,
359
            $constraints
360
        );
361
362
        if (empty($uniqueNames)) {
363
            return $this->insert($table, $insertColumns, $params);
364
        }
365
366
        $onCondition = ['or'];
367
        $quotedTableName = $this->getDb()->quoteTableName($table);
368
369
        foreach ($constraints as $constraint) {
370
            $constraintCondition = ['and'];
371
372
            foreach ($constraint->columnNames as $name) {
373
                $quotedName = $this->getDb()->quoteColumnName($name);
374
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
375
            }
376
            $onCondition[] = $constraintCondition;
377
        }
378
379
        $on = $this->buildCondition($onCondition, $params);
380
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
381
        $mergeSql = 'MERGE ' . $this->getDb()->quoteTableName($table) . ' WITH (HOLDLOCK) '
382
            . 'USING (' . (!empty($placeholders)
383
            ? 'VALUES (' . implode(', ', $placeholders) . ')'
384
            : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
385
386
        $insertValues = [];
387
        foreach ($insertNames as $name) {
388
            $quotedName = $this->db->quoteColumnName($name);
389
390
            if (strrpos($quotedName, '.') === false) {
391
                $quotedName = '[EXCLUDED].' . $quotedName;
392
            }
393
            $insertValues[] = $quotedName;
394
        }
395
396
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
397
398
        if ($updateColumns === false) {
399
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
400
        }
401
402
        if ($updateColumns === true) {
403
            $updateColumns = [];
404
405
            foreach ($updateNames as $name) {
406
                $quotedName = $this->getDb()->quoteColumnName($name);
407
                if (strrpos($quotedName, '.') === false) {
408
                    $quotedName = '[EXCLUDED].' . $quotedName;
409
                }
410
411
                $updateColumns[$name] = new Expression($quotedName);
412
            }
413
        }
414
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
415
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
416
417
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
418
    }
419
420
    public function update(string $table, array $columns, $condition, array &$params = []): string
421
    {
422
        return parent::update($table, $this->normalizeTableRowData($table, $columns, $params), $condition, $params);
423
    }
424
}
425