Passed
Push — dev ( a54f58...65e869 )
by Def
09:39 queued 06:21
created

DMLQueryBuilder   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Test Coverage

Coverage 94.32%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 90
c 1
b 0
f 0
dl 0
loc 335
ccs 83
cts 88
cp 0.9432
rs 9.6
wmc 35

8 Methods

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