Test Failed
Pull Request — master (#44)
by Wilmer
13:17 queued 09:47
created

QueryBuilderPDOOracle::addForeignKey()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 7
dl 0
loc 24
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;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Command\CommandInterface 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...
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\Conditions\InConditionBuilder;
16
use Yiisoft\Db\Oracle\Conditions\LikeConditionBuilder;
17
use Yiisoft\Db\Oracle\DDLQueryBuilder;
18
use Yiisoft\Db\Oracle\DMLQueryBuilder;
19
use Yiisoft\Db\Query\Conditions\InCondition;
20
use Yiisoft\Db\Query\Conditions\LikeCondition;
21
use Yiisoft\Db\Query\Query;
22
use Yiisoft\Db\Query\QueryBuilder;
23
use Yiisoft\Db\Schema\QuoterInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\QuoterInterface 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...
24
use Yiisoft\Db\Schema\Schema;
25
use Yiisoft\Db\Schema\SchemaInterface;
26
use Yiisoft\Strings\NumericHelper;
27
28
/**
29
 * QueryBuilder is the query builder for Oracle databases.
30
 */
31
final class QueryBuilderPDOOracle extends QueryBuilder
32
{
33
    /**
34
     * @var array mapping from abstract column types (keys) to physical column types (values).
35
     */
36
    protected array $typeMap = [
37
        Schema::TYPE_PK => 'NUMBER(10) NOT NULL PRIMARY KEY',
38
        Schema::TYPE_UPK => 'NUMBER(10) UNSIGNED NOT NULL PRIMARY KEY',
39
        Schema::TYPE_BIGPK => 'NUMBER(20) NOT NULL PRIMARY KEY',
40
        Schema::TYPE_UBIGPK => 'NUMBER(20) UNSIGNED NOT NULL PRIMARY KEY',
41
        Schema::TYPE_CHAR => 'CHAR(1)',
42
        Schema::TYPE_STRING => 'VARCHAR2(255)',
43
        Schema::TYPE_TEXT => 'CLOB',
44
        Schema::TYPE_TINYINT => 'NUMBER(3)',
45
        Schema::TYPE_SMALLINT => 'NUMBER(5)',
46
        Schema::TYPE_INTEGER => 'NUMBER(10)',
47
        Schema::TYPE_BIGINT => 'NUMBER(20)',
48
        Schema::TYPE_FLOAT => 'NUMBER',
49
        Schema::TYPE_DOUBLE => 'NUMBER',
50
        Schema::TYPE_DECIMAL => 'NUMBER',
51
        Schema::TYPE_DATETIME => 'TIMESTAMP',
52
        Schema::TYPE_TIMESTAMP => 'TIMESTAMP',
53
        Schema::TYPE_TIME => 'TIMESTAMP',
54
        Schema::TYPE_DATE => 'DATE',
55
        Schema::TYPE_BINARY => 'BLOB',
56
        Schema::TYPE_BOOLEAN => 'NUMBER(1)',
57
        Schema::TYPE_MONEY => 'NUMBER(19,4)',
58
    ];
59
60
    public function __construct(
61
        private CommandInterface $command,
62
        private Query $query,
63
        private QuoterInterface $quoter,
64
        private SchemaInterface $schema
65
    ) {
66
        $this->ddlBuilder = new DDLQueryBuilder($this);
0 ignored issues
show
Bug Best Practice introduced by
The property ddlBuilder does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
67
        $this->dmlBuilder = new DMLQueryBuilder($this);
0 ignored issues
show
Bug Best Practice introduced by
The property dmlBuilder does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
68
        parent::__construct($quoter, $schema);
0 ignored issues
show
Unused Code introduced by
The call to Yiisoft\Db\Query\QueryBuilder::__construct() has too many arguments starting with $schema. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

68
        parent::/** @scrutinizer ignore-call */ 
69
                __construct($quoter, $schema);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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