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