Completed
Pull Request — dev (#53)
by Def
09:50 queued 09:50
created

QueryBuilderPDOOracle::renameTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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