Passed
Pull Request — dev (#128)
by Def
38:26 queued 25:50
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 PDO;
10
use Yiisoft\Db\Driver\PDO\PDOValue;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Expression\ExpressionInterface;
17
use Yiisoft\Db\Query\DMLQueryBuilder as AbstractDMLQueryBuilder;
18
use Yiisoft\Db\Query\Query;
19
use Yiisoft\Db\Query\QueryBuilderInterface;
20
use Yiisoft\Db\Query\QueryInterface;
21
use Yiisoft\Db\Schema\QuoterInterface;
22
use Yiisoft\Db\Schema\Schema;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Db\Pgsql\Schema. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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