Passed
Push — master ( f18622...b28039 )
by Def
05:55 queued 02:19
created

DMLQueryBuilder::resetSequence()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 21
ccs 11
cts 11
cp 1
crap 4
rs 9.9332
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\QueryBuilder\DMLQueryBuilder as AbstractDMLQueryBuilder;
16
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
17
use Yiisoft\Db\Query\QueryInterface;
18
use Yiisoft\Db\Schema\QuoterInterface;
19
use Yiisoft\Db\Schema\SchemaInterface;
20
21
use function implode;
22
use function in_array;
23
use function ltrim;
24
use function strrpos;
25
use function is_array;
26
27
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
28
{
29 499
    public function __construct(
30
        private QueryBuilderInterface $queryBuilder,
31
        private QuoterInterface $quoter,
32
        private SchemaInterface $schema
33
    ) {
34 499
        parent::__construct($queryBuilder, $quoter, $schema);
35
    }
36
37
    /**
38
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
39
     */
40 10
    public function insertEx(string $table, QueryInterface|array $columns, array &$params = []): string
41
    {
42
        /**
43
         * @psalm-var string[] $names
44
         * @psalm-var string[] $placeholders
45
         */
46 10
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
47
48
49 10
        $createdCols = $insertedCols = [];
50 10
        $tableSchema = $this->schema->getTableSchema($table);
51 10
        $returnColumns = $tableSchema?->getColumns() ?? [];
52 10
        foreach ($returnColumns as $returnColumn) {
53 10
            if ($returnColumn->isComputed()) {
54 1
                continue;
55
            }
56
57 10
            $dbType = $returnColumn->getDbType();
58 10
            if (in_array($dbType, ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary'])) {
59 1
                $dbType .= '(MAX)';
60
            }
61 10
            if ($returnColumn->getDbType() === Schema::TYPE_TIMESTAMP) {
62 3
                $dbType = $returnColumn->isAllowNull() ? 'varbinary(8)' : 'binary(8)';
63
            }
64
65 10
            $quotedName = $this->quoter->quoteColumnName($returnColumn->getName());
66 10
            $createdCols[] = $quotedName . ' ' . $dbType . ' ' . ($returnColumn->isAllowNull() ? 'NULL' : '');
67 10
            $insertedCols[] = 'INSERTED.' . $quotedName;
68
        }
69
70 10
        $sql = 'INSERT INTO '
71 10
            . $this->quoter->quoteTableName($table)
72 10
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
73 10
            . ' OUTPUT ' . implode(',', $insertedCols) . ' INTO @temporary_inserted'
74 10
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : (string) $values);
75
76
77 10
        return 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $createdCols) . ');'
78 10
            . $sql . ';SELECT * FROM @temporary_inserted;';
79
    }
80
81
    /**
82
     * @throws InvalidArgumentException
83
     */
84 4
    public function resetSequence(string $tableName, int|string $value = null): string
85
    {
86 4
        $table = $this->schema->getTableSchema($tableName);
87
88 4
        if ($table === null) {
89 1
            throw new InvalidArgumentException("Table not found: '$tableName'.");
90
        }
91
92 3
        $sequenceName = $table->getSequenceName();
93
94 3
        if ($sequenceName === null) {
95 1
            throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
96
        }
97
98 2
        $tableName = $this->quoter->quoteTableName($tableName);
99
100 2
        if ($value === null) {
101 2
            return "DBCC CHECKIDENT ('$tableName', RESEED, 0) WITH NO_INFOMSGS;DBCC CHECKIDENT ('$tableName', RESEED)";
102
        }
103
104 1
        return "DBCC CHECKIDENT ('$tableName', RESEED, $value)";
105
    }
106
107
    /**
108
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
109
     */
110 35
    public function upsert(
111
        string $table,
112
        QueryInterface|array $insertColumns,
113
        bool|array $updateColumns,
114
        array &$params = []
115
    ): string {
116 35
        if (!$insertColumns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$insertColumns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
117 22
            $insertColumns = $this->normalizeColumnNames($table, $insertColumns);
118
        }
119
120 35
        if (!is_bool($updateColumns)) {
0 ignored issues
show
introduced by
The condition is_bool($updateColumns) is always false.
Loading history...
121 11
            $updateColumns = $this->normalizeColumnNames($table, $updateColumns);
122
        }
123
124
        /** @psalm-var Constraint[] $constraints */
125 35
        $constraints = [];
126
127
        /** @psalm-var string[] $insertNames */
128 35
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
129 35
            $table,
130 35
            $insertColumns,
131 35
            $updateColumns,
132 35
            $constraints
133 35
        );
134
135 35
        if (empty($uniqueNames)) {
136 2
            return $this->insert($table, $insertColumns, $params);
137
        }
138
139 33
        $onCondition = ['or'];
140 33
        $quotedTableName = $this->quoter->quoteTableName($table);
141
142 33
        foreach ($constraints as $constraint) {
143 33
            $constraintCondition = ['and'];
144
145 33
            $columnNames = $constraint->getColumnNames() ?? [];
146
147 33
            if (is_array($columnNames)) {
148
                /** @psalm-var string[] $columnNames */
149 33
                foreach ($columnNames as $name) {
150 33
                    $quotedName = $this->quoter->quoteColumnName($name);
151 33
                    $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
152
                }
153
            }
154
155 33
            $onCondition[] = $constraintCondition;
156
        }
157
158 33
        $on = $this->queryBuilder->buildCondition($onCondition, $params);
159
160
        /** @psalm-var string[] $placeholders */
161 33
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
162 33
        $mergeSql = 'MERGE ' . $this->quoter->quoteTableName($table) . ' WITH (HOLDLOCK) '
163 33
            . 'USING (' . (!empty($placeholders)
164 20
            ? 'VALUES (' . implode(', ', $placeholders) . ')'
165 33
            : ltrim((string) $values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
166 33
        $insertValues = [];
167
168 33
        foreach ($insertNames as $name) {
169 33
            $quotedName = $this->quoter->quoteColumnName($name);
170
171 33
            if (strrpos($quotedName, '.') === false) {
172 33
                $quotedName = '[EXCLUDED].' . $quotedName;
173
            }
174
175 33
            $insertValues[] = $quotedName;
176
        }
177
178 33
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
179
180 33
        if ($updateNames === []) {
0 ignored issues
show
introduced by
The condition $updateNames === array() is always false.
Loading history...
181
            /** there are no columns to update */
182 2
            $updateColumns = false;
183
        }
184
185 33
        if ($updateColumns === false) {
0 ignored issues
show
introduced by
The condition $updateColumns === false is always false.
Loading history...
186 14
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
187
        }
188
189 19
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
190 8
            $updateColumns = [];
191
192
            /** @psalm-var string[] $updateNames */
193 8
            foreach ($updateNames as $name) {
194 8
                $quotedName = $this->quoter->quoteColumnName($name);
195 8
                if (strrpos($quotedName, '.') === false) {
196 8
                    $quotedName = '[EXCLUDED].' . $quotedName;
197
                }
198
199 8
                $updateColumns[$name] = new Expression($quotedName);
200
            }
201
        }
202
203
        /**
204
         * @var array $params
205
         * @psalm-var string[] $updates
206
         * @psalm-var array<string, ExpressionInterface|string> $updateColumns
207
         */
208 19
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
209 19
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
210
211 19
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
212
    }
213
}
214