Passed
Pull Request — master (#746)
by
unknown
02:20
created

AbstractDMLQueryBuilder::getTypecastValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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