Test Failed
Pull Request — dev (#69)
by Def
09:39 queued 06:21
created

DMLQueryBuilder::batchInsert()   C

Complexity

Conditions 13
Paths 85

Size

Total Lines 60
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0096

Importance

Changes 0
Metric Value
cc 13
eloc 32
c 0
b 0
f 0
nc 85
nop 4
dl 0
loc 60
rs 6.6166
ccs 25
cts 26
cp 0.9615
crap 13.0096

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\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Expression\ExpressionInterface;
16
use Yiisoft\Db\Query\DMLQueryBuilder as AbstractDMLQueryBuilder;
17
use Yiisoft\Db\Query\Query;
18
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...
19
use Yiisoft\Db\Query\QueryInterface;
20
use Yiisoft\Strings\NumericHelper;
21
22
use function implode;
23
use function ltrim;
24 344
use function strrpos;
25
use function count;
26 344
use function is_string;
27
use function reset;
28
29
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
30
{
31
    public function __construct(private QueryBuilderInterface $queryBuilder)
32
    {
33
        parent::__construct($queryBuilder);
34
    }
35
36
    /**
37
     * @psalm-suppress MixedArrayOffset
38
     */
39
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
40
    {
41 17
        if (empty($rows)) {
42
            return '';
43
        }
44
45
        $schema = $this->schema;
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
46
47 17
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
48 17
            $columnSchemas = $tableSchema->getColumns();
49
        } else {
50
            $columnSchemas = [];
51 17
        }
52
53
        $values = [];
54
55
        /** @psalm-var string[][] $rows */
56
        foreach ($rows as $row) {
57
            $vs = [];
58 17
            foreach ($row as $i => $value) {
59 3
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
60
                    /** @var mixed $value */
61
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
62 14
                }
63
64
                if (is_string($value)) {
65
                    /** @var mixed $value */
66
                    $value = $this->quoter->quoteValue($value);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
67 14
                } elseif (is_float($value)) {
68 14
                    /* ensure type cast always has . as decimal separator in all locales */
69
                    $value = NumericHelper::normalize($value);
70 14
                } elseif ($value === false) {
71 14
                    $value = 0;
72 14
                } elseif ($value === null) {
73
                    $value = 'NULL';
74 14
                } elseif ($value instanceof ExpressionInterface) {
75 14
                    $value = $this->queryBuilder->buildExpression($value, $params);
76 14
                }
77
78
                /** @var mixed */
79 14
                $vs[] = $value;
80
            }
81
82 14
            /** @psalm-var string[] $vs */
83
            $values[] = '(' . implode(', ', $vs) . ')';
84 14
        }
85
86 14
        if (empty($values)) {
87 6
            return '';
88
        }
89 6
90 6
        /** @psalm-var string[] $columns */
91
        foreach ($columns as $i => $name) {
92
            $columns[$i] = $this->quoter->quoteColumnName($name);
93
        }
94 6
95
        $tableAndColumns = ' INTO ' . $this->quoter->quoteTableName($table)
96
            . ' (' . implode(', ', $columns) . ') VALUES ';
97 14
98 14
        return 'INSERT ALL ' . $tableAndColumns . implode($tableAndColumns, $values) . ' SELECT 1 FROM SYS.DUAL';
99 14
    }
100
101 14
    /**
102
     * @link https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606
103 14
     *
104
     * @param string $table
105
     * @param array|QueryInterface $insertColumns
106 14
     * @param array|bool $updateColumns
107 14
     * @param array $params
108
     *
109 14
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
110 14
     *
111
     * @return string
112
     */
113 14
    public function upsert(
114
        string $table,
115
        QueryInterface|array $insertColumns,
116 14
        array|bool $updateColumns,
117
        array &$params = []
118 14
    ): string {
119 4
        $usingValues = null;
120
        $constraints = [];
121
122 10
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
0 ignored issues
show
Bug introduced by
The method prepareUpsertColumns() does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

122
        /** @scrutinizer ignore-call */ 
123
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
123 4
            $table,
124
            $insertColumns,
125 4
            $updateColumns,
126 4
            $constraints
127
        );
128 4
129 4
        if (empty($uniqueNames)) {
130
            return $this->insert($table, $insertColumns, $params);
131 4
        }
132
133
        if ($updateNames === []) {
134
            /** there are no columns to update */
135
            $updateColumns = false;
136 10
        }
137 10
138
        $onCondition = ['or'];
139 10
        $quotedTableName = $this->quoter->quoteTableName($table);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
140
141
        foreach ($constraints as $constraint) {
142
            $columnNames = $constraint->getColumnNames() ?? [];
143
            $constraintCondition = ['and'];
144
            /** @psalm-var string[] $columnNames */
145
            foreach ($columnNames as $name) {
146
                $quotedName = $this->quoter->quoteColumnName($name);
147
                $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
148
            }
149
150
            $onCondition[] = $constraintCondition;
151
        }
152
153
        $on = $this->queryBuilder->buildCondition($onCondition, $params);
154
        /** @psalm-var string[] $placeholders */
155
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
156
157
        if (!empty($placeholders)) {
158
            $usingSelectValues = [];
159
            /** @psalm-var string[] $insertNames */
160
            foreach ($insertNames as $index => $name) {
161
                $usingSelectValues[$name] = new Expression($placeholders[$index]);
162
            }
163
164
            /** @psalm-var array $params */
165
            $usingValues = $this->queryBuilder->buildSelect($usingSelectValues, $params) . ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params);
166
        }
167
168
        $insertValues = [];
169
        $mergeSql = 'MERGE INTO '
170
            . $this->quoter->quoteTableName($table)
171
            . ' '
172
            . 'USING (' . ($usingValues ?? ltrim((string) $values, ' '))
173
            . ') "EXCLUDED" '
174
            . "ON ($on)";
175
176
        /** @psalm-var string[] $insertNames */
177
        foreach ($insertNames as $name) {
178
            $quotedName = $this->quoter->quoteColumnName($name);
179
180
            if (strrpos($quotedName, '.') === false) {
181
                $quotedName = '"EXCLUDED".' . $quotedName;
182
            }
183
184
            $insertValues[] = $quotedName;
185
        }
186
187
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';
188
189
        if ($updateColumns === false) {
190
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
191
        }
192
193
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
194
            $updateColumns = [];
195
            /** @psalm-var string[] $updateNames */
196
            foreach ($updateNames as $name) {
197
                $quotedName = $this->quoter->quoteColumnName($name);
198
199
                if (strrpos($quotedName, '.') === false) {
200
                    $quotedName = '"EXCLUDED".' . $quotedName;
201
                }
202
                $updateColumns[$name] = new Expression($quotedName);
203
            }
204
        }
205
206
        /** @psalm-var string[] $updates */
207
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, (array) $params);
0 ignored issues
show
Bug introduced by
The method prepareUpdateSets() does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

207
        /** @scrutinizer ignore-call */ 
208
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, (array) $params);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
208
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
209
210
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
211
    }
212
213
    protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array
214
    {
215
        /**
216
         * @var array $names
217
         * @var array $placeholders
218
         */
219
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
220
221
        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...
222
            $tableSchema = $this->schema->getTableSchema($table);
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
223
224
            if ($tableSchema !== null) {
225
                $tableColumns = $tableSchema->getColumns();
226
                $columns = !empty($tableSchema->getPrimaryKey())
227
                    ? $tableSchema->getPrimaryKey() : [reset($tableColumns)->getName()];
228
                foreach ($columns as $name) {
229
                    /** @var mixed */
230
                    $names[] = $this->quoter->quoteColumnName($name);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
231
                    $placeholders[] = 'DEFAULT';
232
                }
233
            }
234
        }
235
236
        return [$names, $placeholders, $values, $params];
237
    }
238
239
    public function resetSequence(string $tableName, array|int|string|null $value = null): string
240
    {
241
        $tableSchema = $this->schema->getTableSchema($tableName);
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Oracle\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
242
243
        if ($tableSchema === null) {
244
            throw new \Yiisoft\Db\Exception\InvalidArgumentException("Unknown table: $tableName");
245
        }
246
247
        $sequenceName = $tableSchema->getSequenceName();
248
        if ($sequenceName === null) {
249
            throw new InvalidArgumentException("There is no sequence associated with table: $tableName");
250
        }
251
252
        if ($value !== null) {
253
            $value = (int) $value;
254
        } elseif (count($tableSchema->getPrimaryKey()) > 1) {
255
            throw new InvalidArgumentException("Can't reset sequence for composite primary key in table: $tableName");
256
        }
257
258
        /**
259
         *  Oracle needs at least many queries to reset sequence (see adding transactions and/or use alter method to avoid grants issue?)
260
         */
261
        return 'declare
262
    lastSeq number' . ($value !== null ? (' := ' . $value) : ''). ';
263
begin' . ($value === null ? '
264
    SELECT MAX("' . $tableSchema->getPrimaryKey()[0] . '") + 1 INTO lastSeq FROM "' . $tableSchema->getName() . '";' : '') . '
265
    if lastSeq IS NULL then lastSeq := 1; end if;
266
    execute immediate \'DROP SEQUENCE "' . $sequenceName . '"\';
267
    execute immediate \'CREATE SEQUENCE "' . $sequenceName . '" START WITH \' || lastSeq || \' INCREMENT BY 1 NOMAXVALUE NOCACHE\';
268
end;';
269
    }
270
}
271