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