Test Failed
Pull Request — dev (#123)
by Def
06:45 queued 04:20
created

DMLQueryBuilder   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Test Coverage

Coverage 94.05%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 100
c 1
b 0
f 0
dl 0
loc 366
ccs 79
cts 84
cp 0.9405
rs 9.0399
wmc 42

9 Methods

Rating   Name   Duplication   Size   Complexity  
A newUpsert() 0 49 6
B normalizeTableRowData() 0 22 7
A upsert() 0 13 2
A resetSequence() 0 27 5
A update() 0 9 2
C batchInsert() 0 73 14
A insert() 0 3 1
A insertEx() 0 20 4
A __construct() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DMLQueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DMLQueryBuilder, and based on these observations, apply Extract Interface, too.

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\Db\Schema\SchemaInterface;
23
use Yiisoft\Strings\NumericHelper;
24 414
25
use function implode;
26
use function is_array;
27 1
use function is_string;
28
use function reset;
29 1
30
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
31 1
{
32
    public function __construct(private QueryBuilderInterface $queryBuilder)
33 1
    {
34 1
        parent::__construct($queryBuilder);
35 1
    }
36
37
    public function insertEx(string $table, QueryInterface|array $columns, array &$params = []): string
38 1
    {
39 1
        $sql = $this->insert($table, $columns, $params);
40 1
41 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...
42
43 1
        $returnColumns = [];
44
        if ($tableSchema !== null) {
45
            $returnColumns = $tableSchema->getPrimaryKey();
46 1
        }
47
48
        if (!empty($returnColumns)) {
49
            $returning = [];
50
            foreach ($returnColumns as $name) {
51
                $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...
52
            }
53
            $sql .= ' RETURNING ' . implode(', ', $returning);
54 20
        }
55
56 20
        return $sql;
57 2
    }
58
59
    /**
60
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
61
     *
62
     * @psalm-suppress MixedArrayOffset
63 19
     */
64 19
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
65
    {
66 19
        if (empty($rows)) {
67 19
            return '';
68
        }
69
70 19
        /**
71
         * @psalm-var array<array-key, ColumnSchema> $columnSchemas
72
         */
73
        $columnSchemas = [];
74
        $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...
75 19
76 19
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
77
            $columnSchemas = $tableSchema->getColumns();
78
        }
79
80
        $values = [];
81 19
82 19
        /**
83
         * @var array $row
84
         */
85
        foreach ($rows as $row) {
86 16
            $vs = [];
87
            /**
88
             *  @var int $i
89 19
             *  @var mixed $value
90
             */
91 9
            foreach ($row as $i => $value) {
92 13
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
93
                    /**
94 1
                     * @var bool|ExpressionInterface|float|int|string|null $value
95 13
                     */
96 3
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
97 13
                }
98 5
99 11
                if (is_string($value)) {
100 4
                    /** @var mixed */
101 8
                    $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...
102 6
                } elseif (is_float($value)) {
103
                    /** ensure type cast always has . as decimal separator in all locales */
104
                    $value = NumericHelper::normalize((string) $value);
105
                } elseif ($value === true) {
106 19
                    $value = 'TRUE';
107
                } elseif ($value === false) {
108 19
                    $value = 'FALSE';
109
                } elseif ($value === null) {
110
                    $value = 'NULL';
111 19
                } elseif ($value instanceof ExpressionInterface) {
112
                    $value = $this->queryBuilder->buildExpression($value, $params);
113
                }
114
115
                /** @var bool|ExpressionInterface|float|int|string|null $value */
116 19
                $vs[] = $value;
117 18
            }
118
            $values[] = '(' . implode(', ', $vs) . ')';
119
        }
120
121
        if (empty($values)) {
122
            return '';
123
        }
124
125 19
        /** @var string name */
126 19
        foreach ($columns as $i => $name) {
127
            $columns[$i] = $this->quoter->quoteColumnName($name);
128
        }
129
130
        /**
131
         * @psalm-var string[] $columns
132
         * @psalm-var string[] $values
133
         */
134
        return 'INSERT INTO '
135
            . $this->quoter->quoteTableName($table)
136
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
137
    }
138
139
    /**
140
     * Creates an INSERT SQL statement.
141
     *
142
     * For example,
143
     *
144
     * ```php
145
     * $sql = $queryBuilder->insert('user', [
146
     *     'name' => 'Sam',
147
     *     'age' => 30,
148
     * ], $params);
149
     * ```
150
     *
151
     * The method will properly escape the table and column names.
152
     *
153
     * @param string $table the table that new rows will be inserted into.
154 49
     * @param array|QueryInterface $columns the column data (name => value) to be inserted into the table or instance of
155
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
156 49
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
157
     * DB command later.
158
     *
159
     * @return string the INSERT SQL
160
     *
161
     * @psalm-suppress UndefinedInterfaceMethod
162
     * @psalm-suppress MixedArgument
163
     */
164
    public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
165
    {
166
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
167
    }
168
169
    /**
170
     * {@see upsert()} implementation for PostgresSQL 9.5 or higher.
171 18
     *
172
     * @param string $table
173
     * @param array|QueryInterface $insertColumns
174
     * @param array|bool|QueryInterface $updateColumns
175
     * @param array $params
176
     *
177 18
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
178
     *
179
     * @return string
180 18
     */
181
    public function newUpsert(
182
        string $table,
183
        QueryInterface|array $insertColumns,
184
        bool|array|QueryInterface $updateColumns,
185
        array &$params = []
186 18
    ): string {
187 3
        $insertSql = $this->insert($table, $insertColumns, $params);
188
189
        /** @var array $uniqueNames */
190 15
        [$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

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

226
        /** @scrutinizer ignore-call */ 
227
        [$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...
227
228
        return $insertSql
229
            . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates);
230
    }
231
232
    /**
233
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
234
     *
235
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
236
     * or 1.
237 2
     *
238
     * @param string $tableName the name of the table whose primary key sequence will be reset.
239 2
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
240
     * row's primary key will have a value 1.
241 2
     *
242
     * @throws Exception|InvalidArgumentException if the table does not exist or there is no sequence
243
     * associated with the table.
244
     *
245 2
     * @return string the SQL statement for resetting sequence.
246 2
     */
247
    public function resetSequence(string $tableName, $value = null): string
248 2
    {
249 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...
250 2
251 2
        if ($table !== null && ($sequence = $table->getSequenceName()) !== null) {
252
            /**
253 2
             * {@see http://www.postgresql.org/docs/8.1/static/functions-sequence.html}
254
             */
255
            $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...
256 2
            $tableName = $this->quoter->quoteTableName($tableName);
257
258
            if ($value === null) {
259
                $pk = $table->getPrimaryKey();
260
                $key = $this->quoter->quoteColumnName(reset($pk));
261
                $value = "(SELECT COALESCE(MAX($key),0) FROM $tableName)+1";
262
            } else {
263
                $value = (int) $value;
264
            }
265
266
            return "SELECT SETVAL('$sequence',$value,false)";
267
        }
268
269
        if ($table === null) {
270
            throw new InvalidArgumentException("Table not found: $tableName");
271
        }
272
273
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
274
    }
275
276
    /**
277
     * Creates an UPDATE SQL statement.
278
     *
279
     * For example,
280
     *
281
     * ```php
282
     * $params = [];
283
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
284
     * ```
285
     *
286
     * The method will properly escape the table and column names.
287
     *
288
     * @param string $table the table to be updated.
289 4
     * @param array $columns the column data (name => value) to be updated.
290
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
291
     * {@see Query::where()} on how to specify condition.
292 4
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
293
     * DB command later.
294 4
     *
295
     * @return string the UPDATE SQL.
296 4
     *
297
     * @psalm-suppress UndefinedInterfaceMethod
298
     */
299
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
300
    {
301
        $normalizeTableRowData = $this->normalizeTableRowData($table, $columns);
302
303
        return parent::update(
304
            $table,
305
            is_array($normalizeTableRowData) ? $normalizeTableRowData : [],
0 ignored issues
show
introduced by
The condition is_array($normalizeTableRowData) is always true.
Loading history...
306
            $condition,
307
            $params,
308
        );
309
    }
310
311
    /**
312
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
313
     * constraints), or update them if they do.
314
     *
315
     * For example,
316
     *
317
     * ```php
318
     * $sql = $queryBuilder->upsert('pages', [
319
     *     'name' => 'Front page',
320
     *     'url' => 'http://example.com/', // url is unique
321
     *     'visits' => 0,
322
     * ], [
323
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
324
     * ], $params);
325
     * ```
326
     *
327
     * The method will properly escape the table and column names.
328
     *
329
     * @param string $table the table that new rows will be inserted into/updated in.
330
     * @param array|QueryInterface $insertColumns the column data (name => value) to be inserted into the table or
331
     * instance of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
332
     * @param array|bool|QueryInterface $updateColumns the column data (name => value) to be updated if they already
333
     * exist.
334
     * If `true` is passed, the column data will be updated to match the insert column data.
335
     * If `false` is passed, no update will be performed if the column data already exists.
336
     * @param array $params the binding parameters that will be generated by this method.
337
     * They should be bound to the DB command later.
338
     *
339
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
340 18
     * underlying DBMS.
341
     *
342
     * @return string the resulting SQL.
343
     *
344
     * @link https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
345
     * @link https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
346
     *
347 18
     * @psalm-suppress UndefinedInterfaceMethod
348
     */
349 18
    public function upsert(
350
        string $table,
351 7
        QueryInterface|array $insertColumns,
352
        $updateColumns,
353
        array &$params = []
354 18
    ): string {
355
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
356
357
        if (!is_bool($updateColumns)) {
358
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
359
        }
360
361
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
362
    }
363
364
    /**
365
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
366
     *
367
     * @param string $table the table that data will be saved into.
368
     * @param array|QueryInterface $columns the column data (name => value) to be saved into the table or instance of
369
     * {@see QueryInterface} to perform INSERT INTO ... SELECT SQL statement. Passing of
370
     * {@see QueryInterface}.
371
     *
372
     * @return array|QueryInterface normalized columns.
373
     */
374
    private function normalizeTableRowData(string $table, QueryInterface|array $columns): QueryInterface|array
375
    {
376
        if ($columns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
377
            return $columns;
378
        }
379
380
        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...
381
            $columnSchemas = $tableSchema->getColumns();
382
            /** @var mixed $value */
383
            foreach ($columns as $name => $value) {
384
                if (
385
                    isset($columnSchemas[$name]) &&
386
                    $columnSchemas[$name]->getType() === Schema::TYPE_BINARY &&
387
                    is_string($value)
388
                ) {
389
                    /** explicitly setup PDO param type for binary column */
390
                    $columns[$name] = new PDOValue($value, PDO::PARAM_LOB);
391
                }
392
            }
393
        }
394
395
        return $columns;
396
    }
397
}
398