Passed
Pull Request — master (#746)
by Sergei
03:12 queued 57s
created

AbstractDMLQueryBuilder::update()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 4
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\QueryBuilder;
6
7
use JsonException;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Constraint\IndexConstraint;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\ExpressionInterface;
15
use Yiisoft\Db\Query\QueryInterface;
16
use Yiisoft\Db\Schema\QuoterInterface;
17
use Yiisoft\Db\Schema\SchemaInterface;
18
19
use function array_combine;
20
use function array_diff;
21
use function array_filter;
22
use function array_keys;
23
use function array_map;
24
use function array_merge;
25
use function array_unique;
26
use function array_values;
27
use function implode;
28
use function in_array;
29
use function is_string;
30
use function json_encode;
31
use function preg_match;
32
use function sort;
33
34
/**
35
 * It's used to manipulate data in tables.
36
 *
37
 * This manipulation involves inserting data into database tables, retrieving existing data, deleting data from existing
38
 * tables and modifying existing data.
39
 *
40
 * @link https://en.wikipedia.org/wiki/Data_manipulation_language
41
 */
42
abstract class AbstractDMLQueryBuilder implements DMLQueryBuilderInterface
43
{
44
    public function __construct(
45
        protected QueryBuilderInterface $queryBuilder,
46
        protected QuoterInterface $quoter,
47
        protected SchemaInterface $schema
48
    ) {
49
    }
50
51
    public function batchInsert(string $table, array $columns, iterable $rows, array &$params = []): string
52
    {
53
        if (empty($rows)) {
54
            return '';
55
        }
56
57
        $values = [];
58
        $columns = $this->getNormalizeColumnNames($columns);
59
        $columnSchemas = $this->schema->getTableSchema($table)?->getColumns() ?? [];
60
61
        /** @psalm-var array[] $rows */
62
        foreach ($rows as $row) {
63
            $i = 0;
64
            $placeholders = [];
65
            /** @psalm-var mixed $value */
66
            foreach ($row as $value) {
67
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
68
                    /** @psalm-var mixed $value */
69
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
70
                }
71
72
                if ($value instanceof ExpressionInterface) {
73
                    $placeholders[] = $this->queryBuilder->buildExpression($value, $params);
74
                } else {
75
                    $placeholders[] = $this->queryBuilder->bindParam($value, $params);
76
                }
77
78
                ++$i;
79
            }
80
            $values[] = '(' . implode(', ', $placeholders) . ')';
81
        }
82
83
        if (empty($values)) {
84
            return '';
85
        }
86
87
        foreach ($columns as $i => $name) {
88
            $columns[$i] = $this->quoter->quoteColumnName($name);
89
        }
90
91
        return 'INSERT INTO ' . $this->quoter->quoteTableName($table)
92
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
93
    }
94
95
    public function delete(string $table, array|string $condition, array &$params): string
96
    {
97
        $sql = 'DELETE FROM ' . $this->quoter->quoteTableName($table);
98
        $where = $this->queryBuilder->buildWhere($condition, $params);
99
100
        return $where === '' ? $sql : $sql . ' ' . $where;
101
    }
102
103
    public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
104
    {
105
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
106
107
        return 'INSERT INTO ' . $this->quoter->quoteTableName($table)
108
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
109
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' ' . $values);
110
    }
111
112
    public function insertWithReturningPks(string $table, QueryInterface|array $columns, array &$params = []): string
113
    {
114
        throw new NotSupportedException(__METHOD__ . '() is not supported by this DBMS.');
115
    }
116
117
    public function resetSequence(string $table, int|string|null $value = null): string
118
    {
119
        throw new NotSupportedException(__METHOD__ . '() is not supported by this DBMS.');
120
    }
121
122
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
123
    {
124
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
125
126
        $sql = 'UPDATE ' . $this->quoter->quoteTableName($table) . ' SET ' . implode(', ', $lines);
127
        $where = $this->queryBuilder->buildWhere($condition, $params);
128
129
        return $where === '' ? $sql : $sql . ' ' . $where;
130
    }
131
132
    public function upsert(
133
        string $table,
134
        QueryInterface|array $insertColumns,
135
        bool|array $updateColumns,
136
        array &$params
137
    ): string {
138
        throw new NotSupportedException(__METHOD__ . ' is not supported by this DBMS.');
139
    }
140
141
    /**
142
     * Prepare select-subQuery and field names for `INSERT INTO ... SELECT` SQL statement.
143
     *
144
     * @param QueryInterface $columns Object, which represents a select query.
145
     * @param array $params The parameters to bind to the generated SQL statement. These parameters will be included
146
     * in the result, with the more parameters generated during the query building process.
147
     *
148
     * @throws Exception
149
     * @throws InvalidArgumentException
150
     * @throws InvalidConfigException
151
     * @throws NotSupportedException
152
     *
153
     * @return array Array of quoted column names, values, and params.
154
     * @psalm-return array{0: string[], 1: string, 2: array}
155
     */
156
    protected function prepareInsertSelectSubQuery(QueryInterface $columns, array $params = []): array
157
    {
158
        /** @psalm-var string[] $select */
159
        $select = $columns->getSelect();
160
161
        if (empty($select) || in_array('*', $select, true)) {
162
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
163
        }
164
165
        [$values, $params] = $this->queryBuilder->build($columns, $params);
166
167
        $names = [];
168
169
        foreach ($select as $title => $field) {
170
            if (is_string($title)) {
171
                $names[] = $this->quoter->quoteColumnName($title);
172
            } else {
173
                if ($field instanceof ExpressionInterface) {
174
                    $field = $this->queryBuilder->buildExpression($field, $params);
175
                }
176
177
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $field, $matches)) {
178
                    $names[] = $this->quoter->quoteColumnName($matches[2]);
179
                } else {
180
                    $names[] = $this->quoter->quoteColumnName($field);
181
                }
182
            }
183
        }
184
185
        return [$names, $values, $params];
186
    }
187
188
    /**
189
     * Prepare column names and placeholders for `INSERT` SQL statement.
190
     *
191
     * @throws Exception
192
     * @throws InvalidConfigException
193
     * @throws InvalidArgumentException
194
     * @throws NotSupportedException
195
     *
196
     * @return array Array of quoted column names, placeholders, values, and params.
197
     * @psalm-return array{0: string[], 1: string[], 2: string, 3: array}
198
     */
199
    protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array
200
    {
201
        if ($columns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
202
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $params);
203
            return [$names, [], $values, $params];
204
        }
205
206
        if (empty($columns)) {
207
            return [[], [], 'DEFAULT VALUES', []];
208
        }
209
210
        $names = [];
211
        $placeholders = [];
212
        $columns = $this->normalizeColumnNames($columns);
213
        $columnSchemas = $this->schema->getTableSchema($table)?->getColumns() ?? [];
214
215
        /**
216
         * @psalm-var mixed $value
217
         * @psalm-var array<string, mixed> $columns
218
         */
219
        foreach ($columns as $name => $value) {
220
            $names[] = $this->quoter->quoteColumnName($name);
221
222
            if (isset($columnSchemas[$name])) {
223
                /** @var mixed $value */
224
                $value = $columnSchemas[$name]->dbTypecast($value);
225
            }
226
227
            if ($value instanceof ExpressionInterface) {
228
                $placeholders[] = $this->queryBuilder->buildExpression($value, $params);
229
            } else {
230
                $placeholders[] = $this->queryBuilder->bindParam($value, $params);
231
            }
232
        }
233
234
        return [$names, $placeholders, '', $params];
235
    }
236
237
    /**
238
     * Prepare column names and placeholders for `UPDATE` SQL statement.
239
     *
240
     * @throws Exception
241
     * @throws InvalidConfigException
242
     * @throws InvalidArgumentException
243
     * @throws NotSupportedException
244
     *
245
     * @psalm-return array{0: string[], 1: array}
246
     */
247
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
248
    {
249
        $sets = [];
250
        $columns = $this->normalizeColumnNames($columns);
251
        $columnSchemas = $this->schema->getTableSchema($table)?->getColumns() ?? [];
252
253
        /**
254
         * @psalm-var array<string, mixed> $columns
255
         * @psalm-var mixed $value
256
         */
257
        foreach ($columns as $name => $value) {
258
            if (isset($columnSchemas[$name])) {
259
                /** @psalm-var mixed $value */
260
                $value = $columnSchemas[$name]->dbTypecast($value);
261
            }
262
263
            if ($value instanceof ExpressionInterface) {
264
                $placeholder = $this->queryBuilder->buildExpression($value, $params);
265
            } else {
266
                $placeholder = $this->queryBuilder->bindParam($value, $params);
267
            }
268
269
            $sets[] = $this->quoter->quoteColumnName($name) . '=' . $placeholder;
270
        }
271
272
        return [$sets, $params];
273
    }
274
275
    /**
276
     * Prepare column names and constraints for "upsert" operation.
277
     *
278
     * @throws Exception
279
     * @throws InvalidArgumentException
280
     * @throws InvalidConfigException
281
     * @throws JsonException
282
     * @throws NotSupportedException
283
     *
284
     * @psalm-param Constraint[] $constraints
285
     *
286
     * @return array Array of unique, insert and update quoted column names.
287
     * @psalm-return array{0: string[], 1: string[], 2: string[]|null}
288
     */
289
    protected function prepareUpsertColumns(
290
        string $table,
291
        QueryInterface|array $insertColumns,
292
        QueryInterface|bool|array $updateColumns,
293
        array &$constraints = []
294
    ): array {
295
        if ($insertColumns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$insertColumns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
296
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns);
297
        } else {
298
            /** @psalm-var array<string, mixed> $insertColumns */
299
            $insertNames = $this->getNormalizeColumnNames(array_keys($insertColumns));
300
301
            foreach ($insertNames as $i => $name) {
302
                $insertNames[$i] = $this->quoter->quoteColumnName($name);
303
            }
304
        }
305
306
        /** @psalm-var string[] $uniqueNames */
307
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
308
309
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
310
            return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
311
        }
312
313
        return [$uniqueNames, $insertNames, null];
314
    }
315
316
    /**
317
     * Returns all quoted column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
318
     * for the named table removing constraints which didn't cover the specified column list.
319
     *
320
     * The column list will be unique by column names.
321
     *
322
     * @param string $name The table name, may contain schema name if any. Don't quote the table name.
323
     * @param string[] $columns Source column list.
324
     * @param array $constraints This parameter optionally receives a matched constraint list. The constraints
325
     * will be unique by their column names.
326
     *
327
     * @throws JsonException
328
     *
329
     * @return array The quoted column names.
330
     *
331
     * @psalm-param Constraint[] $constraints
332
    */
333
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
334
    {
335
        $primaryKey = $this->schema->getTablePrimaryKey($name);
336
337
        if ($primaryKey !== null) {
338
            $constraints[] = $primaryKey;
339
        }
340
341
        /** @psalm-var IndexConstraint[] $tableIndexes */
342
        $tableIndexes = $this->schema->getTableIndexes($name);
343
344
        foreach ($tableIndexes as $constraint) {
345
            if ($constraint->isUnique()) {
346
                $constraints[] = $constraint;
347
            }
348
        }
349
350
        $constraints = array_merge($constraints, $this->schema->getTableUniques($name));
351
352
        /**
353
         * Remove duplicates
354
         *
355
         * @psalm-var Constraint[] $constraints
356
         */
357
        $constraints = array_combine(
358
            array_map(
359
                static function (Constraint $constraint): string {
360
                    $columns = (array) $constraint->getColumnNames();
361
                    sort($columns, SORT_STRING);
362
                    return json_encode($columns, JSON_THROW_ON_ERROR);
363
                },
364
                $constraints
365
            ),
366
            $constraints
367
        );
368
369
        $columnNames = [];
370
        $quoter = $this->quoter;
371
372
        // Remove all constraints which don't cover the specified column list.
373
        $constraints = array_values(
374
            array_filter(
375
                $constraints,
376
                static function (Constraint $constraint) use ($quoter, $columns, &$columnNames): bool {
377
                    /** @psalm-var string[] $getColumnNames */
378
                    $getColumnNames = (array) $constraint->getColumnNames();
379
                    $constraintColumnNames = [];
380
381
                    foreach ($getColumnNames as $columnName) {
382
                        $constraintColumnNames[] = $quoter->quoteColumnName($columnName);
383
                    }
384
385
                    $result = empty(array_diff($constraintColumnNames, $columns));
386
387
                    if ($result) {
388
                        $columnNames = array_merge((array) $columnNames, $constraintColumnNames);
389
                    }
390
391
                    return $result;
392
                }
393
            )
394
        );
395
396
        /** @psalm-var string[] $columnNames */
397
        return array_unique($columnNames);
398
    }
399
400
    /**
401
     * Normalizes the column names.
402
     *
403
     * @param array $columns The column data (name => value).
404
     *
405
     * @return array The normalized column names (name => value).
406
     */
407
    protected function normalizeColumnNames(array $columns): array
408
    {
409
        /** @var string[] $columnList */
410
        $columnList = array_keys($columns);
411
        $normalizedNames = $this->getNormalizeColumnNames($columnList);
412
413
        return array_combine($normalizedNames, $columns);
414
    }
415
416
    /**
417
     * Get normalized column names
418
     *
419
     * @param string[] $columns The column names.
420
     *
421
     * @return string[] Normalized column names.
422
     */
423
    protected function getNormalizeColumnNames(array $columns): array
424
    {
425
        $normalizedNames = [];
426
427
        foreach ($columns as $name) {
428
            $normalizedName = $this->quoter->ensureColumnName($name);
429
            $normalizedNames[] = $this->quoter->unquoteSimpleColumnName($normalizedName);
430
        }
431
432
        return $normalizedNames;
433
    }
434
}
435