Passed
Push — dev ( a54f58...65e869 )
by Def
09:39 queued 06:21
created

DMLQueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 73
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 14.0054

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 33
c 1
b 0
f 0
nc 97
nop 4
dl 0
loc 73
ccs 32
cts 33
cp 0.9697
crap 14.0054
rs 6.2666

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\Pgsql;
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\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
use Yiisoft\Strings\NumericHelper;
19
20
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
21
{
22 414
    public function __construct(private QueryBuilderInterface $queryBuilder)
23
    {
24 414
        parent::__construct($queryBuilder);
25
    }
26
27 1
    public function insertEx(string $table, QueryInterface|array $columns, array &$params = []): string
28
    {
29 1
        $sql = $this->insert($table, $columns, $params);
30
31 1
        $tableSchema = $this->queryBuilder->schema()->getTableSchema($table);
32
33 1
        $returnColumns = [];
34 1
        if ($tableSchema !== null) {
35 1
            $returnColumns = $tableSchema->getPrimaryKey();
36
        }
37
38 1
        if (!empty($returnColumns)) {
39 1
            $returning = [];
40 1
            foreach ($returnColumns as $name) {
41 1
                $returning[] = $this->queryBuilder->quoter()->quoteColumnName($name);
42
            }
43 1
            $sql .= ' RETURNING ' . implode(', ', $returning);
44
        }
45
46 1
        return $sql;
47
    }
48
49
    /**
50
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
51
     *
52
     * @psalm-suppress MixedArrayOffset
53
     */
54 20
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
55
    {
56 20
        if (empty($rows)) {
57 2
            return '';
58
        }
59
60
        /**
61
         * @psalm-var array<array-key, ColumnSchema> $columnSchemas
62
         */
63 19
        $columnSchemas = [];
64 19
        $schema = $this->queryBuilder->schema();
65
66 19
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
67 19
            $columnSchemas = $tableSchema->getColumns();
68
        }
69
70 19
        $values = [];
71
72
        /**
73
         * @var array $row
74
         */
75 19
        foreach ($rows as $row) {
76 19
            $vs = [];
77
            /**
78
             *  @var int $i
79
             *  @var mixed $value
80
             */
81 19
            foreach ($row as $i => $value) {
82 19
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
83
                    /**
84
                     * @var bool|ExpressionInterface|float|int|string|null $value
85
                     */
86 16
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
87
                }
88
89 19
                if (is_string($value)) {
90
                    /** @var mixed */
91 9
                    $value = $this->queryBuilder->quoter()->quoteValue($value);
92 13
                } elseif (is_float($value)) {
93
                    /** ensure type cast always has . as decimal separator in all locales */
94 1
                    $value = NumericHelper::normalize((string) $value);
95 13
                } elseif ($value === true) {
96 3
                    $value = 'TRUE';
97 13
                } elseif ($value === false) {
98 5
                    $value = 'FALSE';
99 11
                } elseif ($value === null) {
100 4
                    $value = 'NULL';
101 8
                } elseif ($value instanceof ExpressionInterface) {
102 6
                    $value = $this->queryBuilder->buildExpression($value, $params);
103
                }
104
105
                /** @var bool|ExpressionInterface|float|int|string|null $value */
106 19
                $vs[] = $value;
107
            }
108 19
            $values[] = '(' . implode(', ', $vs) . ')';
109
        }
110
111 19
        if (empty($values)) {
112
            return '';
113
        }
114
115
        /** @var string name */
116 19
        foreach ($columns as $i => $name) {
117 18
            $columns[$i] = $this->queryBuilder->quoter()->quoteColumnName($name);
118
        }
119
120
        /**
121
         * @psalm-var string[] $columns
122
         * @psalm-var string[] $values
123
         */
124
        return 'INSERT INTO '
125 19
            . $this->queryBuilder->quoter()->quoteTableName($table)
126 19
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
127
    }
128
129
    /**
130
     * Creates an INSERT SQL statement.
131
     *
132
     * For example,
133
     *
134
     * ```php
135
     * $sql = $queryBuilder->insert('user', [
136
     *     'name' => 'Sam',
137
     *     'age' => 30,
138
     * ], $params);
139
     * ```
140
     *
141
     * The method will properly escape the table and column names.
142
     *
143
     * @param string $table the table that new rows will be inserted into.
144
     * @param array|QueryInterface $columns the column data (name => value) to be inserted into the table or instance of
145
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
146
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
147
     * DB command later.
148
     *
149
     * @return string the INSERT SQL
150
     *
151
     * @psalm-suppress UndefinedInterfaceMethod
152
     * @psalm-suppress MixedArgument
153
     */
154 49
    public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
155
    {
156 49
        return parent::insert($table, $this->queryBuilder->normalizeTableRowData($table, $columns), $params);
157
    }
158
159
    /**
160
     * {@see upsert()} implementation for PostgresSQL 9.5 or higher.
161
     *
162
     * @param string $table
163
     * @param array|QueryInterface $insertColumns
164
     * @param array|bool|QueryInterface $updateColumns
165
     * @param array $params
166
     *
167
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
168
     *
169
     * @return string
170
     */
171 18
    public function newUpsert(
172
        string $table,
173
        QueryInterface|array $insertColumns,
174
        bool|array|QueryInterface $updateColumns,
175
        array &$params = []
176
    ): string {
177 18
        $insertSql = $this->insert($table, $insertColumns, $params);
178
179
        /** @var array $uniqueNames */
180 18
        [$uniqueNames, , $updateNames] = $this->queryBuilder->prepareUpsertColumns(
181
            $table,
182
            $insertColumns,
183
            $updateColumns,
184
        );
185
186 18
        if (empty($uniqueNames)) {
187 3
            return $insertSql;
188
        }
189
190 15
        if ($updateNames === []) {
191
            /** there are no columns to update */
192
            $updateColumns = false;
193
        }
194
195 15
        if ($updateColumns === false) {
196 5
            return "$insertSql ON CONFLICT DO NOTHING";
197
        }
198
199 10
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
200 4
            $updateColumns = [];
201
202
            /** @var string $name */
203 4
            foreach ($updateNames as $name) {
204 4
                $updateColumns[$name] = new Expression(
205 4
                    'EXCLUDED.' . $this->queryBuilder->quoter()->quoteColumnName($name)
206
                );
207
            }
208
        }
209
210
        /**
211
         * @var array $updateColumns
212
         *
213
         * @psalm-var string[] $uniqueNames
214
         * @psalm-var string[] $updates
215
         */
216 10
        [$updates, $params] = $this->queryBuilder->prepareUpdateSets($table, $updateColumns, $params);
217
218
        return $insertSql
219 10
            . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates);
220
    }
221
222
    /**
223
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
224
     *
225
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
226
     * or 1.
227
     *
228
     * @param string $tableName the name of the table whose primary key sequence will be reset.
229
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
230
     * row's primary key will have a value 1.
231
     *
232
     * @throws Exception|InvalidArgumentException if the table does not exist or there is no sequence
233
     * associated with the table.
234
     *
235
     * @return string the SQL statement for resetting sequence.
236
     */
237 2
    public function resetSequence(string $tableName, $value = null): string
238
    {
239 2
        $table = $this->queryBuilder->schema()->getTableSchema($tableName);
240
241 2
        if ($table !== null && ($sequence = $table->getSequenceName()) !== null) {
242
            /**
243
             * {@see http://www.postgresql.org/docs/8.1/static/functions-sequence.html}
244
             */
245 2
            $sequence = $this->queryBuilder->quoter()->quoteTableName($sequence);
246 2
            $tableName = $this->queryBuilder->quoter()->quoteTableName($tableName);
247
248 2
            if ($value === null) {
249 2
                $pk = $table->getPrimaryKey();
250 2
                $key = $this->queryBuilder->quoter()->quoteColumnName(reset($pk));
251 2
                $value = "(SELECT COALESCE(MAX($key),0) FROM $tableName)+1";
252
            } else {
253 2
                $value = (int) $value;
254
            }
255
256 2
            return "SELECT SETVAL('$sequence',$value,false)";
257
        }
258
259
        if ($table === null) {
260
            throw new InvalidArgumentException("Table not found: $tableName");
261
        }
262
263
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
264
    }
265
266
    /**
267
     * Creates an UPDATE SQL statement.
268
     *
269
     * For example,
270
     *
271
     * ```php
272
     * $params = [];
273
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
274
     * ```
275
     *
276
     * The method will properly escape the table and column names.
277
     *
278
     * @param string $table the table to be updated.
279
     * @param array $columns the column data (name => value) to be updated.
280
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
281
     * {@see Query::where()} on how to specify condition.
282
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
283
     * DB command later.
284
     *
285
     * @return string the UPDATE SQL.
286
     *
287
     * @psalm-suppress UndefinedInterfaceMethod
288
     */
289 4
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
290
    {
291
        /** @var array|QueryInterface */
292 4
        $normalizeTableRowData = $this->queryBuilder->normalizeTableRowData($table, $columns);
293
294 4
        return parent::update(
295
            $table,
296 4
            is_array($normalizeTableRowData) ? $normalizeTableRowData : [],
297
            $condition,
298
            $params,
299
        );
300
    }
301
302
    /**
303
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
304
     * constraints), or update them if they do.
305
     *
306
     * For example,
307
     *
308
     * ```php
309
     * $sql = $queryBuilder->upsert('pages', [
310
     *     'name' => 'Front page',
311
     *     'url' => 'http://example.com/', // url is unique
312
     *     'visits' => 0,
313
     * ], [
314
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
315
     * ], $params);
316
     * ```
317
     *
318
     * The method will properly escape the table and column names.
319
     *
320
     * @param string $table the table that new rows will be inserted into/updated in.
321
     * @param array|QueryInterface $insertColumns the column data (name => value) to be inserted into the table or
322
     * instance of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
323
     * @param array|bool|QueryInterface $updateColumns the column data (name => value) to be updated if they already
324
     * exist.
325
     * If `true` is passed, the column data will be updated to match the insert column data.
326
     * If `false` is passed, no update will be performed if the column data already exists.
327
     * @param array $params the binding parameters that will be generated by this method.
328
     * They should be bound to the DB command later.
329
     *
330
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
331
     * underlying DBMS.
332
     *
333
     * @return string the resulting SQL.
334
     *
335
     * @link https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
336
     * @link https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
337
     *
338
     * @psalm-suppress UndefinedInterfaceMethod
339
     */
340 18
    public function upsert(
341
        string $table,
342
        QueryInterface|array $insertColumns,
343
        $updateColumns,
344
        array &$params = []
345
    ): string {
346
        /** @var array|QueryInterface $insertColumns */
347 18
        $insertColumns = $this->queryBuilder->normalizeTableRowData($table, $insertColumns);
348
349 18
        if (!is_bool($updateColumns)) {
350
            /** @var array|QueryInterface $updateColumns */
351 7
            $updateColumns = $this->queryBuilder->normalizeTableRowData($table, $updateColumns);
352
        }
353
354 18
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
355
    }
356
}
357