Passed
Push — master ( e1f353...34075b )
by Def
31:15 queued 08:44
created

DMLQueryBuilder::upsert()   C

Complexity

Conditions 13
Paths 109

Size

Total Lines 98
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 13.0016

Importance

Changes 0
Metric Value
cc 13
eloc 52
c 0
b 0
f 0
nc 109
nop 4
dl 0
loc 98
ccs 46
cts 47
cp 0.9787
crap 13.0016
rs 6.5416

How to fix   Long Method    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\Oracle;
6
7
use Generator;
8
use JsonException;
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 ltrim;
23
use function strrpos;
24
use function count;
25
use function reset;
26
27
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
28
{
29 350
    public function __construct(
30
        private QueryBuilderInterface $queryBuilder,
31
        private QuoterInterface $quoter,
32
        private SchemaInterface $schema
33
    ) {
34 350
        parent::__construct($queryBuilder, $quoter, $schema);
35
    }
36
37
    /**
38
     * @psalm-suppress MixedArrayOffset
39
     */
40 17
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
41
    {
42 17
        if (empty($rows)) {
43 2
            return '';
44
        }
45
46 16
        if (($tableSchema = $this->schema->getTableSchema($table)) !== null) {
47 16
            $columnSchemas = $tableSchema->getColumns();
48
        } else {
49
            $columnSchemas = [];
50
        }
51
52 16
        $values = [];
53
54
        /** @psalm-var array<array-key, array<array-key, string>> $rows */
55 16
        foreach ($rows as $row) {
56 16
            $placeholders = [];
57 16
            foreach ($row as $index => $value) {
58 16
                if (isset($columns[$index], $columnSchemas[$columns[$index]])) {
59
                    /** @var mixed $value */
60 13
                    $value = $this->getTypecastValue($value, $columnSchemas[$columns[$index]]);
61
                }
62
63 16
                if ($value instanceof ExpressionInterface) {
64 3
                    $placeholders[] = $this->queryBuilder->buildExpression($value, $params);
65
                } else {
66 16
                    $placeholders[] = $this->queryBuilder->bindParam($value, $params);
67
                }
68
            }
69 16
            $values[] = '(' . implode(', ', $placeholders) . ')';
70
        }
71
72 16
        if (empty($values)) {
73
            return '';
74
        }
75
76
        /** @psalm-var string[] $columns */
77 16
        foreach ($columns as $i => $name) {
78 15
            $columns[$i] = $this->quoter->quoteColumnName($name);
79
        }
80
81 16
        $tableAndColumns = ' INTO ' . $this->quoter->quoteTableName($table)
82 16
            . ' (' . implode(', ', $columns) . ') VALUES ';
83
84 16
        return 'INSERT ALL ' . $tableAndColumns . implode($tableAndColumns, $values) . ' SELECT 1 FROM SYS.DUAL';
85
    }
86
87
    /**
88
     * @link https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606
89
     *
90
     * @param string $table
91
     * @param array|QueryInterface $insertColumns
92
     * @param array|bool $updateColumns
93
     * @param array $params
94
     *
95
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
96
     *
97
     * @return string
98
     */
99 17
    public function upsert(
100
        string $table,
101
        QueryInterface|array $insertColumns,
102
        array|bool $updateColumns,
103
        array &$params = []
104
    ): string {
105 17
        $usingValues = null;
106 17
        $constraints = [];
107
108 17
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
109
            $table,
110
            $insertColumns,
111
            $updateColumns,
112
            $constraints
113
        );
114
115 17
        if (empty($uniqueNames)) {
116 3
            return $this->insert($table, $insertColumns, $params);
117
        }
118
119 14
        if ($updateNames === []) {
0 ignored issues
show
introduced by
The condition $updateNames === array() is always false.
Loading history...
120
            /** there are no columns to update */
121
            $updateColumns = false;
122
        }
123
124 14
        $onCondition = ['or'];
125 14
        $quotedTableName = $this->quoter->quoteTableName($table);
126
127 14
        foreach ($constraints as $constraint) {
128 14
            $columnNames = $constraint->getColumnNames() ?? [];
129 14
            $constraintCondition = ['and'];
130
            /** @psalm-var string[] $columnNames */
131 14
            foreach ($columnNames as $name) {
132 14
                $quotedName = $this->quoter->quoteColumnName($name);
133 14
                $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
134
            }
135
136 14
            $onCondition[] = $constraintCondition;
137
        }
138
139 14
        $on = $this->queryBuilder->buildCondition($onCondition, $params);
140
        /** @psalm-var string[] $placeholders */
141 14
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
142
143 14
        if (!empty($placeholders)) {
144 6
            $usingSelectValues = [];
145
            /** @psalm-var string[] $insertNames */
146 6
            foreach ($insertNames as $index => $name) {
147 6
                $usingSelectValues[$name] = new Expression($placeholders[$index]);
148
            }
149
150
            /** @psalm-var array $params */
151 6
            $usingValues = $this->queryBuilder->buildSelect($usingSelectValues, $params) . ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params);
152
        }
153
154 14
        $insertValues = [];
155 14
        $mergeSql = 'MERGE INTO '
156 14
            . $this->quoter->quoteTableName($table)
157
            . ' '
158 14
            . 'USING (' . ($usingValues ?? ltrim((string) $values, ' '))
159
            . ') "EXCLUDED" '
160 14
            . "ON ($on)";
161
162
        /** @psalm-var string[] $insertNames */
163 14
        foreach ($insertNames as $name) {
164 14
            $quotedName = $this->quoter->quoteColumnName($name);
165
166 14
            if (strrpos($quotedName, '.') === false) {
167 14
                $quotedName = '"EXCLUDED".' . $quotedName;
168
            }
169
170 14
            $insertValues[] = $quotedName;
171
        }
172
173 14
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
174
175 14
        if ($updateColumns === false) {
0 ignored issues
show
introduced by
The condition $updateColumns === false is always false.
Loading history...
176 4
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
177
        }
178
179 10
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
180 4
            $updateColumns = [];
181
            /** @psalm-var string[] $updateNames */
182 4
            foreach ($updateNames as $name) {
183 4
                $quotedName = $this->quoter->quoteColumnName($name);
184
185 4
                if (strrpos($quotedName, '.') === false) {
186 4
                    $quotedName = '"EXCLUDED".' . $quotedName;
187
                }
188 4
                $updateColumns[$name] = new Expression($quotedName);
189
            }
190
        }
191
192
        /** @psalm-var string[] $updates */
193 10
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, (array) $params);
194 10
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
195
196 10
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
197
    }
198
199 46
    protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array
200
    {
201
        /**
202
         * @var array $names
203
         * @var array $placeholders
204
         */
205 46
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
206
207 43
        if (!$columns instanceof QueryInterface && empty($names)) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
208
            $tableSchema = $this->schema->getTableSchema($table);
209
210
            if ($tableSchema !== null) {
211
                $tableColumns = $tableSchema->getColumns();
212
                $columns = !empty($tableSchema->getPrimaryKey())
213
                    ? $tableSchema->getPrimaryKey() : [reset($tableColumns)->getName()];
214
                foreach ($columns as $name) {
215
                    /** @var mixed */
216
                    $names[] = $this->quoter->quoteColumnName($name);
217
                    $placeholders[] = 'DEFAULT';
218
                }
219
            }
220
        }
221
222 43
        return [$names, $placeholders, $values, $params];
223
    }
224
225
    /**
226
     * @throws InvalidArgumentException
227
     */
228 1
    public function resetSequence(string $tableName, array|int|string|null $value = null): string
229
    {
230 1
        $tableSchema = $this->schema->getTableSchema($tableName);
231
232 1
        if ($tableSchema === null) {
233
            throw new InvalidArgumentException("Unknown table: $tableName");
234
        }
235
236 1
        $sequenceName = $tableSchema->getSequenceName();
237 1
        if ($sequenceName === null) {
238
            throw new InvalidArgumentException("There is no sequence associated with table: $tableName");
239
        }
240
241 1
        if ($value !== null) {
242 1
            $value = (int) $value;
243 1
        } elseif (count($tableSchema->getPrimaryKey()) > 1) {
244
            throw new InvalidArgumentException("Can't reset sequence for composite primary key in table: $tableName");
245
        }
246
247
        /**
248
         *  Oracle needs at least many queries to reset sequence (see adding transactions and/or use alter method to avoid grants issue?)
249
         */
250
        return 'declare
251 1
    lastSeq number' . ($value !== null ? (' := ' . $value) : '') . ';
252 1
begin' . ($value === null ? '
253 1
    SELECT MAX("' . $tableSchema->getPrimaryKey()[0] . '") + 1 INTO lastSeq FROM "' . $tableSchema->getName() . '";' : '') . '
254
    if lastSeq IS NULL then lastSeq := 1; end if;
255
    execute immediate \'DROP SEQUENCE "' . $sequenceName . '"\';
256
    execute immediate \'CREATE SEQUENCE "' . $sequenceName . '" START WITH \' || lastSeq || \' INCREMENT BY 1 NOMAXVALUE NOCACHE\';
257
end;';
258
    }
259
}
260