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