Passed
Pull Request — dev (#109)
by Wilmer
03:08
created

DMLQueryBuilder::newUpsert()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 49
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087

Importance

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