Passed
Push — dev ( cc6591...c9f34d )
by Wilmer
04:23 queued 23s
created

DMLQueryBuilder   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 75
c 1
b 0
f 0
dl 0
loc 157
ccs 73
cts 73
cp 1
rs 10
wmc 24

4 Methods

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