Passed
Branch dev (d14b82)
by Wilmer
12:57
created

DMLQueryBuilder::insertEx()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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