Test Failed
Pull Request — dev (#123)
by Def
11:01 queued 08:38
created

DMLQueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 73
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 14.0072

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 29
cts 30
cp 0.9667
crap 14.0072
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;
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...
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 414
use Yiisoft\Strings\NumericHelper;
23
24 414
use function implode;
25
use function is_array;
26
use function is_string;
27 1
use function reset;
28
29 1
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
30
{
31 1
    public function __construct(private QueryBuilderInterface $queryBuilder)
32
    {
33 1
        parent::__construct($queryBuilder);
34 1
    }
35 1
36
    public function insertEx(string $table, QueryInterface|array $columns, array &$params = []): string
37
    {
38 1
        $sql = $this->insert($table, $columns, $params);
39 1
40 1
        $tableSchema = $this->schema->getTableSchema($table);
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
41 1
42
        $returnColumns = [];
43 1
        if ($tableSchema !== null) {
44
            $returnColumns = $tableSchema->getPrimaryKey();
45
        }
46 1
47
        if (!empty($returnColumns)) {
48
            $returning = [];
49
            foreach ($returnColumns as $name) {
50
                $returning[] = $this->quoter->quoteColumnName($name);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
51
            }
52
            $sql .= ' RETURNING ' . implode(', ', $returning);
53
        }
54 20
55
        return $sql;
56 20
    }
57 2
58
    /**
59
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
60
     *
61
     * @psalm-suppress MixedArrayOffset
62
     */
63 19
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
64 19
    {
65
        if (empty($rows)) {
66 19
            return '';
67 19
        }
68
69
        /**
70 19
         * @psalm-var array<array-key, ColumnSchema> $columnSchemas
71
         */
72
        $columnSchemas = [];
73
        $schema = $this->schema;
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
74
75 19
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
76 19
            $columnSchemas = $tableSchema->getColumns();
77
        }
78
79
        $values = [];
80
81 19
        /**
82 19
         * @var array $row
83
         */
84
        foreach ($rows as $row) {
85
            $vs = [];
86 16
            /**
87
             *  @var int $i
88
             *  @var mixed $value
89 19
             */
90
            foreach ($row as $i => $value) {
91 9
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
92 13
                    /**
93
                     * @var bool|ExpressionInterface|float|int|string|null $value
94 1
                     */
95 13
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
96 3
                }
97 13
98 5
                if (is_string($value)) {
99 11
                    /** @var mixed */
100 4
                    $value = $this->quoter->quoteValue($value);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
101 8
                } elseif (is_float($value)) {
102 6
                    /** ensure type cast always has . as decimal separator in all locales */
103
                    $value = NumericHelper::normalize((string) $value);
104
                } elseif ($value === true) {
105
                    $value = 'TRUE';
106 19
                } elseif ($value === false) {
107
                    $value = 'FALSE';
108 19
                } elseif ($value === null) {
109
                    $value = 'NULL';
110
                } elseif ($value instanceof ExpressionInterface) {
111 19
                    $value = $this->queryBuilder->buildExpression($value, $params);
112
                }
113
114
                /** @var bool|ExpressionInterface|float|int|string|null $value */
115
                $vs[] = $value;
116 19
            }
117 18
            $values[] = '(' . implode(', ', $vs) . ')';
118
        }
119
120
        if (empty($values)) {
121
            return '';
122
        }
123
124
        /** @var string name */
125 19
        foreach ($columns as $i => $name) {
126 19
            $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
            . $this->quoter->quoteTableName($table)
135
            . ' (' . 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 49
     * {@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 49
     * DB command later.
157
     *
158
     * @return string the INSERT SQL
159
     *
160
     * @psalm-suppress UndefinedInterfaceMethod
161
     * @psalm-suppress MixedArgument
162
     */
163
    public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
164
    {
165
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
166
    }
167
168
    /**
169
     * {@see upsert()} implementation for PostgresSQL 9.5 or higher.
170
     *
171 18
     * @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 18
     *
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 3
188
        /** @var array $uniqueNames */
189
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns(
0 ignored issues
show
Bug introduced by
The method prepareUpsertColumns() does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. ( Ignorable by Annotation )

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

189
        /** @scrutinizer ignore-call */ 
190
        [$uniqueNames, , $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...
190 15
            $table,
191
            $insertColumns,
192
            $updateColumns,
193
        );
194
195 15
        if (empty($uniqueNames)) {
196 5
            return $insertSql;
197
        }
198
199 10
        if ($updateNames === []) {
200 4
            /** there are no columns to update */
201
            $updateColumns = false;
202
        }
203 4
204 4
        if ($updateColumns === false) {
205 4
            return "$insertSql ON CONFLICT DO NOTHING";
206
        }
207
208
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
209
            $updateColumns = [];
210
211
            /** @var string $name */
212
            foreach ($updateNames as $name) {
213
                $updateColumns[$name] = new Expression(
214
                    'EXCLUDED.' . $this->quoter->quoteColumnName($name)
215
                );
216 10
            }
217
        }
218
219 10
        /**
220
         * @var array $updateColumns
221
         *
222
         * @psalm-var string[] $uniqueNames
223
         * @psalm-var string[] $updates
224
         */
225
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
The method prepareUpdateSets() does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. ( Ignorable by Annotation )

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

225
        /** @scrutinizer ignore-call */ 
226
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $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...
226
227
        return $insertSql
228
            . ' 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 2
     * @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 2
     * row's primary key will have a value 1.
240
     *
241 2
     * @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 2
     */
246 2
    public function resetSequence(string $tableName, $value = null): string
247
    {
248 2
        $table = $this->schema->getTableSchema($tableName);
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
249 2
250 2
        if ($table !== null && ($sequence = $table->getSequenceName()) !== null) {
251 2
            /**
252
             * {@see http://www.postgresql.org/docs/8.1/static/functions-sequence.html}
253 2
             */
254
            $sequence = $this->quoter->quoteTableName($sequence);
0 ignored issues
show
Bug Best Practice introduced by
The property quoter does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
255
            $tableName = $this->quoter->quoteTableName($tableName);
256 2
257
            if ($value === null) {
258
                $pk = $table->getPrimaryKey();
259
                $key = $this->quoter->quoteColumnName(reset($pk));
260
                $value = "(SELECT COALESCE(MAX($key),0) FROM $tableName)+1";
261
            } else {
262
                $value = (int) $value;
263
            }
264
265
            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 4
     * @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 4
     * DB command later.
293
     *
294 4
     * @return string the UPDATE SQL.
295
     *
296 4
     * @psalm-suppress UndefinedInterfaceMethod
297
     */
298
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
299
    {
300
        $normalizeTableRowData = $this->normalizeTableRowData($table, $columns);
301
302
        return parent::update(
303
            $table,
304
            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 18
     *
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 18
     */
348
    public function upsert(
349 18
        string $table,
350
        QueryInterface|array $insertColumns,
351 7
        $updateColumns,
352
        array &$params = []
353
    ): string {
354 18
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
355
356
        if (!is_bool($updateColumns)) {
357
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
358
        }
359
360
        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
    private function normalizeTableRowData(string $table, QueryInterface|array $columns): QueryInterface|array
374
    {
375
        if ($columns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
376
            return $columns;
377
        }
378
379
        if (($tableSchema = $this->schema->getTableSchema($table)) !== null) {
0 ignored issues
show
Bug Best Practice introduced by
The property schema does not exist on Yiisoft\Db\Pgsql\DMLQueryBuilder. Did you maybe forget to declare it?
Loading history...
380
            $columnSchemas = $tableSchema->getColumns();
381
            /** @var mixed $value */
382
            foreach ($columns as $name => $value) {
383
                if (
384
                    isset($columnSchemas[$name]) &&
385
                    $columnSchemas[$name]->getType() === Schema::TYPE_BINARY &&
386
                    is_string($value)
387
                ) {
388
                    /** explicitly setup PDO param type for binary column */
389
                    $columns[$name] = new PDOValue($value, PDO::PARAM_LOB);
390
                }
391
            }
392
        }
393
394
        return $columns;
395
    }
396
}
397