Passed
Pull Request — master (#124)
by Def
24:26 queued 20:17
created

DMLQueryBuilder::insertEx()   B

Complexity

Conditions 9
Paths 56

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9

Importance

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