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