Passed
Push — dev ( 85c32d...c7af07 )
by Def
16:37 queued 08:07
created

QueryBuilderPDOOracle::addForeignKey()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 7
dl 0
loc 24
ccs 10
cts 11
cp 0.9091
crap 3.0067
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle\PDO;
6
7
use Generator;
8
use Throwable;
9
use Yiisoft\Db\Command\CommandInterface;
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\Oracle\Builder\InConditionBuilder;
16
use Yiisoft\Db\Oracle\Builder\LikeConditionBuilder;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Oracle\Builder\LikeConditionBuilder 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\Oracle\DDLQueryBuilder;
18
use Yiisoft\Db\Oracle\DMLQueryBuilder;
19
use Yiisoft\Db\Query\Conditions\InCondition;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\Conditions\InCondition 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...
20
use Yiisoft\Db\Query\Conditions\LikeCondition;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\Conditions\LikeCondition 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...
21
use Yiisoft\Db\Query\Query;
22
use Yiisoft\Db\Query\QueryBuilder;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\QueryBuilder 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...
23
use Yiisoft\Db\Schema\QuoterInterface;
24
use Yiisoft\Db\Schema\Schema;
25
use Yiisoft\Db\Schema\SchemaInterface;
26
use Yiisoft\Strings\NumericHelper;
27
28
use function array_merge;
29
use function implode;
30
use function is_float;
31
use function is_string;
32
use function reset;
33
34
/**
35
 * QueryBuilder is the query builder for Oracle databases.
36
 */
37
final class QueryBuilderPDOOracle extends QueryBuilder
38
{
39
    /**
40
     * @psalm-var string[] $typeMap Mapping from abstract column types (keys) to physical column types (values).
41
     */
42
    protected array $typeMap = [
43
        Schema::TYPE_PK => 'NUMBER(10) NOT NULL PRIMARY KEY',
44
        Schema::TYPE_UPK => 'NUMBER(10) UNSIGNED NOT NULL PRIMARY KEY',
45
        Schema::TYPE_BIGPK => 'NUMBER(20) NOT NULL PRIMARY KEY',
46
        Schema::TYPE_UBIGPK => 'NUMBER(20) UNSIGNED NOT NULL PRIMARY KEY',
47
        Schema::TYPE_CHAR => 'CHAR(1)',
48
        Schema::TYPE_STRING => 'VARCHAR2(255)',
49
        Schema::TYPE_TEXT => 'CLOB',
50
        Schema::TYPE_TINYINT => 'NUMBER(3)',
51
        Schema::TYPE_SMALLINT => 'NUMBER(5)',
52
        Schema::TYPE_INTEGER => 'NUMBER(10)',
53
        Schema::TYPE_BIGINT => 'NUMBER(20)',
54
        Schema::TYPE_FLOAT => 'NUMBER',
55
        Schema::TYPE_DOUBLE => 'NUMBER',
56
        Schema::TYPE_DECIMAL => 'NUMBER',
57
        Schema::TYPE_DATETIME => 'TIMESTAMP',
58
        Schema::TYPE_TIMESTAMP => 'TIMESTAMP',
59
        Schema::TYPE_TIME => 'TIMESTAMP',
60
        Schema::TYPE_DATE => 'DATE',
61
        Schema::TYPE_BINARY => 'BLOB',
62
        Schema::TYPE_BOOLEAN => 'NUMBER(1)',
63
        Schema::TYPE_MONEY => 'NUMBER(19,4)',
64
    ];
65
    private DDLQueryBuilder $ddlBuilder;
66
    private DMLQueryBuilder $dmlBuilder;
67
68 336
    public function __construct(
69
        private CommandInterface $command,
70
        private QuoterInterface $quoter,
71
        private SchemaInterface $schema
72
    ) {
73 336
        $this->ddlBuilder = new DDLQueryBuilder($this);
74 336
        $this->dmlBuilder = new DMLQueryBuilder($this);
75 336
        parent::__construct($quoter, $schema, $this->ddlBuilder, $this->dmlBuilder);
76
    }
77
78 4
    public function addForeignKey(
79
        string $name,
80
        string $table,
81
        $columns,
82
        string $refTable,
83
        $refColumns,
84
        ?string $delete = null,
85
        ?string $update = null
86
    ): string {
87 4
        $sql = 'ALTER TABLE ' . $this->quoter->quoteTableName($table)
88 4
            . ' ADD CONSTRAINT ' . $this->quoter->quoteColumnName($name)
89 4
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
90 4
            . ' REFERENCES ' . $this->quoter->quoteTableName($refTable)
91 4
            . ' (' . $this->buildColumns($refColumns) . ')';
92
93 4
        if ($delete !== null) {
94 2
            $sql .= ' ON DELETE ' . $delete;
95
        }
96
97 4
        if ($update !== null) {
98
            throw new Exception('Oracle does not support ON UPDATE clause.');
99
        }
100
101 4
        return $sql;
102
    }
103
104
    /**
105
     * Builds a SQL statement for changing the definition of a column.
106
     *
107
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
108
     * method.
109
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
110
     * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column
111
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
112
     * generated SQL.
113
     *
114
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
115
     * 'varchar(255) not null'.
116
     *
117
     * @return string the SQL statement for changing the definition of a column.
118
     */
119
    public function alterColumn(string $table, string $column, string $type): string
120
    {
121
        $type = $this->getColumnType($type);
122
123
        return 'ALTER TABLE '
124
            . $this->quoter->quoteTableName($table)
125
            . ' MODIFY '
126
            . $this->quoter->quoteColumnName($column)
127
            . ' ' . $this->getColumnType($type);
128
    }
129
130
    /**
131
     * Generates a batch INSERT SQL statement.
132
     *
133
     * For example,
134
     *
135
     * ```php
136
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
137
     *     ['Tom', 30],
138
     *     ['Jane', 20],
139
     *     ['Linda', 25],
140
     * ]);
141
     * ```
142
     *
143
     * Note that the values in each row must match the corresponding column names.
144
     *
145
     * @param string $table the table that new rows will be inserted into.
146
     * @param array $columns the column names.
147
     * @param Generator|iterable $rows the rows to be batched inserted into the table.
148
     * @param array $params
149
     *
150
     * @throws \Exception|InvalidArgumentException
151
     *
152
     * @return string the batch INSERT SQL statement.
153
     *
154
     * @psalm-suppress MixedArrayOffset
155
     */
156 14
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
157
    {
158 14
        if (empty($rows)) {
159 2
            return '';
160
        }
161
162 13
        $schema = $this->schema;
163
164 13
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
165 9
            $columnSchemas = $tableSchema->getColumns();
166
        } else {
167 4
            $columnSchemas = [];
168
        }
169
170 13
        $values = [];
171
172
        /** @psalm-var string[][] $rows */
173 13
        foreach ($rows as $row) {
174 12
            $vs = [];
175 12
            foreach ($row as $i => $value) {
176 12
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
177
                    /** @var mixed $value */
178 5
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
179
                }
180
181 12
                if (is_string($value)) {
182
                    /** @var mixed $value */
183 8
                    $value = $this->quoter->quoteValue($value);
184 7
                } elseif (is_float($value)) {
185
                    /* ensure type cast always has . as decimal separator in all locales */
186 1
                    $value = NumericHelper::normalize($value);
187 7
                } elseif ($value === false) {
188 3
                    $value = 0;
189 7
                } elseif ($value === null) {
190 4
                    $value = 'NULL';
191 4
                } elseif ($value instanceof ExpressionInterface) {
192 3
                    $value = $this->buildExpression($value, $params);
193
                }
194
195
                /** @var mixed */
196 12
                $vs[] = $value;
197
            }
198
199
            /** @psalm-var string[] $vs */
200 12
            $values[] = '(' . implode(', ', $vs) . ')';
201
        }
202
203 13
        if (empty($values)) {
204 1
            return '';
205
        }
206
207
        /** @psalm-var string[] $columns */
208 12
        foreach ($columns as $i => $name) {
209 11
            $columns[$i] = $this->quoter->quoteColumnName($name);
210
        }
211
212 12
        $tableAndColumns = ' INTO ' . $this->quoter->quoteTableName($table)
213 12
            . ' (' . implode(', ', $columns) . ') VALUES ';
214
215 12
        return 'INSERT ALL ' . $tableAndColumns . implode($tableAndColumns, $values) . ' SELECT 1 FROM SYS.DUAL';
216
    }
217
218 166
    public function buildOrderByAndLimit(string $sql, array $orderBy, $limit, $offset, array &$params = []): string
219
    {
220 166
        $orderBy = $this->buildOrderBy($orderBy, $params);
221
222 166
        if ($orderBy !== '') {
223 6
            $sql .= $this->separator . $orderBy;
224
        }
225
226 166
        $filters = [];
227
228 166
        if ($this->hasOffset($offset)) {
229 1
            $filters[] = 'rowNumId > ' . (string) $offset;
230
        }
231
232 166
        if ($this->hasLimit($limit)) {
233 9
            $filters[] = 'rownum <= ' . (string) $limit;
234
        }
235
236 166
        if (empty($filters)) {
237 160
            return $sql;
238
        }
239
240 9
        $filter = implode(' AND ', $filters);
241
        return <<<SQL
242
        WITH USER_SQL AS ($sql), PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL)
243
        SELECT * FROM PAGINATION WHERE $filter
244
        SQL;
245
    }
246
247
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
248
    {
249
        throw new NotSupportedException('Oracle does not support enabling/disabling integrity check.');
250
    }
251
252
    public function command(): CommandInterface
253
    {
254
        return $this->command;
255
    }
256
257
    /**
258
     * Builds a SQL statement for dropping an index.
259
     *
260
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
261
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
262
     *
263
     * @return string the SQL statement for dropping an index.
264
     */
265 2
    public function dropIndex(string $name, string $table): string
266
    {
267 2
        return 'DROP INDEX ' . $this->quoter->quoteTableName($name);
268
    }
269
270
    /**
271
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
272
     *
273
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
274
     * or 1.
275
     *
276
     * @param string $tableName the name of the table whose primary key sequence will be reset.
277
     * @param array|int|string|null $value the value for the primary key of the next new row inserted. If this is not
278
     * set, the next new row's primary key will have a value 1.
279
     *
280
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
281
     */
282 1
    public function executeResetSequence(string $tableName, array|int|string $value = null): void
283
    {
284 1
        $tableSchema = $this->schema->getTableSchema($tableName);
285
286 1
        if ($tableSchema === null) {
287
            throw new InvalidArgumentException("Unknown table: $tableName");
288
        }
289
290 1
        if ($tableSchema->getSequenceName() === null) {
291
            throw new InvalidArgumentException("There is no sequence associated with table: $tableName");
292
        }
293
294 1
        if ($value !== null) {
295 1
            $value = (int) $value;
296
        } else {
297 1
            if (count($tableSchema->getPrimaryKey()) > 1) {
298
                throw new InvalidArgumentException(
299
                    "Can't reset sequence for composite primary key in table: $tableName"
300
                );
301
            }
302 1
            $value = (int) $this->command->setSql(
303 1
                'SELECT MAX("' . $tableSchema->getPrimaryKey()[0] . '") FROM "' . $tableSchema->getName() . '"'
304 1
            )->queryScalar() + 1;
305
        }
306
307
        /**
308
         *  Oracle needs at least two queries to reset sequence (see adding transactions and/or use alter method to
309
         *  avoid grants' issue?)
310
         */
311 1
        $this->command->setSQl('DROP SEQUENCE "' . (string) $tableSchema->getSequenceName() . '"')->execute();
312 1
        $this->command->setSql(
313
            'CREATE SEQUENCE "' .
314 1
            (string) $tableSchema->getSequenceName() .
315
            '" START WITH ' .
316
            $value .
317
            ' INCREMENT BY 1 NOMAXVALUE NOCACHE'
318 1
        )->execute();
319
    }
320
321 42
    public function prepareInsertValues(string $table, $columns, array $params = []): array
322
    {
323
        /**
324
         * @var array $names
325
         * @var array $placeholders
326
         */
327 42
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
328
329 39
        if (!$columns instanceof Query && empty($names)) {
330
            $tableSchema = $this->schema->getTableSchema($table);
331
332
            if ($tableSchema !== null) {
333
                $tableColumns = $tableSchema->getColumns();
334
                $columns = !empty($tableSchema->getPrimaryKey())
335
                    ? $tableSchema->getPrimaryKey() : [reset($tableColumns)->getName()];
336
                foreach ($columns as $name) {
337
                    /** @var mixed */
338
                    $names[] = $this->quoter->quoteColumnName($name);
339
                    $placeholders[] = 'DEFAULT';
340
                }
341
            }
342
        }
343
344 39
        return [$names, $placeholders, $values, $params];
345
    }
346
347 278
    public function quoter(): QuoterInterface
348
    {
349 278
        return $this->quoter;
350
    }
351
352
    /**
353
     * Builds a SQL statement for renaming a DB table.
354
     *
355
     * @param string $oldName
356
     * @param string $newName the new table name. The name will be properly quoted by the method.
357
     *
358
     * @return string the SQL statement for renaming a DB table.
359
     */
360 2
    public function renameTable(string $oldName, string $newName): string
361
    {
362 2
        return 'ALTER TABLE ' . $this->quoter->quoteTableName($oldName) . ' RENAME TO ' .
363 2
            $this->quoter->quoteTableName($newName);
364
    }
365
366 1
    public function selectExists(string $rawSql): string
367
    {
368 1
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END FROM DUAL';
369
    }
370
371 147
    public function schema(): SchemaInterface
372
    {
373 147
        return $this->schema;
374
    }
375
376 336
    protected function defaultExpressionBuilders(): array
377
    {
378 336
        return array_merge(parent::defaultExpressionBuilders(), [
379
            InCondition::class => InConditionBuilder::class,
380
            LikeCondition::class => LikeConditionBuilder::class,
381
        ]);
382
    }
383
}
384