Passed
Push — master ( b7f728...dee1f4 )
by Edgard
18:46
created

QueryBuilder::prepareUpdateSets()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 27.6718

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 1
cts 4
cp 0.25
rs 8.2222
cc 7
eloc 12
nc 10
nop 3
crap 27.6718
1
<?php
2
3
/**
4
 * @link http://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license http://www.yiiframework.com/license/
7
 */
8
9
namespace edgardmessias\db\firebird;
10
11
use yii\base\InvalidParamException;
12
use yii\base\NotSupportedException;
13
use yii\db\Expression;
14
use yii\db\Query;
15
16
/**
17
 *
18
 * @author Edgard Lorraine Messias <[email protected]>
19
 * @since 2.0
20
 */
21
class QueryBuilder extends \yii\db\QueryBuilder
22
{
23
24
    /**
25
     * @var array mapping from abstract column types (keys) to physical column types (values).
26
     */
27
    public $typeMap = [
28
        Schema::TYPE_PK        => 'integer NOT NULL PRIMARY KEY',
29
        Schema::TYPE_UPK       => 'integer NOT NULL PRIMARY KEY',
30
        Schema::TYPE_BIGPK     => 'bigint NOT NULL PRIMARY KEY',
31
        Schema::TYPE_UBIGPK    => 'bigint NOT NULL PRIMARY KEY',
32
        Schema::TYPE_CHAR      => 'char(1)',
33
        Schema::TYPE_STRING    => 'varchar(255)',
34
        Schema::TYPE_TEXT      => 'blob sub_type text',
35
        Schema::TYPE_SMALLINT  => 'smallint',
36
        Schema::TYPE_INTEGER   => 'integer',
37
        Schema::TYPE_BIGINT    => 'bigint',
38
        Schema::TYPE_FLOAT     => 'float',
39
        Schema::TYPE_DOUBLE    => 'double precision',
40
        Schema::TYPE_DECIMAL   => 'numeric(10,0)',
41
        Schema::TYPE_DATETIME  => 'timestamp',
42
        Schema::TYPE_TIMESTAMP => 'timestamp',
43
        Schema::TYPE_TIME      => 'time',
44
        Schema::TYPE_DATE      => 'date',
45
        Schema::TYPE_BINARY    => 'blob',
46
        Schema::TYPE_BOOLEAN   => 'smallint',
47
        Schema::TYPE_MONEY     => 'numeric(18,4)',
48
    ];
49
    
50
    public function init()
51
    {
52
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
53
            $this->typeMap[Schema::TYPE_PK] = 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
54
            $this->typeMap[Schema::TYPE_UPK] = 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
55
            $this->typeMap[Schema::TYPE_BIGPK] = 'bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
56
            $this->typeMap[Schema::TYPE_UBIGPK] = 'bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
57
            $this->typeMap[Schema::TYPE_BOOLEAN] = 'boolean';
58
        }
59 181
        
60
        parent::init();
61 181
    }
62
63 181
    protected function defaultExpressionBuilders()
64
    {
65
        return array_merge(parent::defaultExpressionBuilders(), [
66 181
            'yii\db\Expression' => 'edgardmessias\db\firebird\ExpressionBuilder',
67 181
            'yii\db\conditions\InCondition' => 'edgardmessias\db\firebird\conditions\InConditionBuilder',
68 181
        ]);
69 181
    }
70 181
71 181
    /**
72
     * Generates a SELECT SQL statement from a [[Query]] object.
73
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
74 181
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
75 181
     * be included in the result with the additional parameters generated during the query building process.
76
     * @return array the generated SQL statement (the first array element) and the corresponding
77 181
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
78 34
     * include those provided in `$params`.
79 34
     */
80 34
    public function build($query, $params = [])
81
    {
82
        $query = $query->prepare($this);
83
84 181
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
85 2
86 2
        $clauses = [
87 2
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
88
            $this->buildFrom($query->from, $params),
89
            $this->buildJoin($query->join, $params),
90
            $this->buildWhere($query->where, $params),
91
            $this->buildGroupBy($query->groupBy),
92 181
            $this->buildHaving($query->having, $params),
93 181
        ];
94 2
95
        $sql = implode($this->separator, array_filter($clauses));
96
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
97 181
98
        if (!empty($query->orderBy)) {
99
            foreach ($query->orderBy as $expression) {
100
                if ($expression instanceof Expression) {
101
                    $params = array_merge($params, $expression->params);
102
                }
103 181
            }
104
        }
105 181
        if (!empty($query->groupBy)) {
106 53
            foreach ($query->groupBy as $expression) {
107 53
                if ($expression instanceof Expression) {
108 3
                    $params = array_merge($params, $expression->params);
109
                }
110 52
            }
111 52
        }
112 21
113 21
        $union = $this->buildUnion($query->union, $params);
114
        if ($union !== '') {
115 52
            $sql = "$sql{$this->separator}$union";
116
        }
117
118
        return [$sql, $params];
119
    }
120 181
121
    /**
122
     * @inheritdoc
123
     */
124
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
125
    {
126 5
        if (is_array($columns)) {
127
            foreach ($columns as $i => $column) {
128 5
                if (!is_string($column)) {
129 5
                    continue;
130 5
                }
131
                $matches = [];
132 5
                if (preg_match('/^(COUNT|SUM|AVG|MIN|MAX)\([\{\[]{0,2}(\w+|\*)[\}\]]{0,2}\)$/i', $column, $matches)) {
133 5
                    $function = $matches[1];
134 5
                    $alias = $matches[2] != '*' ? $matches[2] : 'ALL';
135 5
136 5
                    $columns[$i] = "{$column} AS {$function}_{$alias}";
137 5
                }
138 5
            }
139 5
        }
140
141 5
        return parent::buildSelect($columns, $params, $distinct, $selectOption);
142
    }
143
144 5
    /**
145
     * @inheritdoc
146 5
     */
147
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
148
    {
149
150
        $orderBy = $this->buildOrderBy($orderBy);
151
        if ($orderBy !== '') {
152 181
            $sql .= $this->separator . $orderBy;
153
        }
154
155 181
        $limit = $limit !== null ? intval($limit) : -1;
156 181
        $offset = $offset !== null ? intval($offset) : -1;
157 34
        // If ignoring both params then do nothing
158
        if ($offset < 0 && $limit < 0) {
159
            return $sql;
160 181
        }
161 181
        // If we are ignoring limit then return full result set starting
162
        // from $offset. In Firebird this can only be done with SKIP
163 181
        if ($offset >= 0 && $limit < 0) {
164 180
            $count = 1; //Only do it once
165
            $sql = preg_replace('/^SELECT /i', 'SELECT SKIP ' . (int) $offset . ' ', $sql, $count);
166
            return $sql;
167
        }
168 3
        // If we are ignoring $offset then return $limit rows.
169 2
        // ie, return the first $limit rows in the set.
170 2
        if ($offset < 0 && $limit >= 0) {
171 2
            $count = 1; //Only do it once
172
            $sql = preg_replace('/^SELECT /i', 'SELECT FIRST ' . (int) $limit . ' ', $sql, $count);
173
            return $sql;
174
        }
175 2
        // Otherwise apply the params and return the amended sql.
176 2
        if ($offset >= 0 && $limit >= 0) {
177 2
            $count = 1; //Only do it once
178 2
            $sql = preg_replace('/^SELECT /i', 'SELECT FIRST ' . (int) $limit . ' SKIP ' . (int) $offset . ' ', $sql, $count);
179
            return $sql;
180
        }
181 1
        // If we have fallen through the cracks then just pass
182 1
        // the sql back.
183 1
        return $sql;
184 1
    }
185
186
    /**
187
     * @param array $unions
188
     * @param array $params the binding parameters to be populated
189
     * @return string the UNION clause built from [[Query::$union]].
190
     */
191
    public function buildUnion($unions, &$params)
192
    {
193
        if (empty($unions)) {
194
            return '';
195
        }
196 181
197
        $result = '';
198 181
199 181
        foreach ($unions as $i => $union) {
200
            $query = $union['query'];
201
            if ($query instanceof Query) {
202 2
                list($unions[$i]['query'], $params) = $this->build($query, $params);
203
            }
204 2
205 2
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . $unions[$i]['query'] . ' ';
206 2
        }
207 2
208
        return trim($result);
209
    }
210 2
211
    /**
212
     * @inheritdoc
213 2
     */
214
    public function prepareInsertValues($table, $columns, $params = [])
215
    {
216
        $schema = $this->db->getSchema();
217
        $tableSchema = $schema->getTableSchema($table);
218
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
219
        
220
        //Empty insert
221 3
        if (empty($columns) && !empty($columnSchemas)) {
222
            $columns = [];
223 3
            foreach ($columnSchemas as $columnSchema) {
224
                if (!$columnSchema->autoIncrement) {
225
                    $columns[$columnSchema->name] = $columnSchema->defaultValue;
226
                }
227
            }
228
        }
229 3
230
        if (is_array($columns)) {
231 3
            foreach ($columns as $name => $value) {
232
                if ($value instanceof \yii\db\ExpressionInterface) {
233
                    continue;
234 3
                }
235
                if ($value instanceof \yii\db\PdoValue) {
236
                    continue;
237
                }
238
                if (isset($columnSchemas[$name]) && in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) {
239
                    $columns[$name] = new \yii\db\PdoValue($value, \PDO::PARAM_LOB);
240 23
                }
241
            }
242 23
        }
243 23
244 23
        return parent::prepareInsertValues($table, $columns, $params);
245
    }
246
    
247
    /**
248
     * @inheritdoc
249
     */
250 23
    protected function prepareInsertSelectSubQuery($columns, $schema, $params = [])
251 1
    {
252 1
        /**
253 1
         * @see https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable
254 1
         */
255
        if (version_compare($this->db->firebird_version, '3.0.0', '<')) {
256
            throw new NotSupportedException('Firebird < 3.0.0 has the "Unstable Cursor" problem');
257
        }
258
259 23
        return parent::prepareInsertSelectSubQuery($columns, $schema, $params);
260 23
    }
261 23
262 1
    /**
263 23
     * @inheritdoc
264 23
     */
265
    public function prepareUpdateSets($table, $columns, $params = [])
266
    {
267
        $schema = $this->db->getSchema();
268
        $tableSchema = $schema->getTableSchema($table);
269 23
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
270
271
        foreach ($columns as $name => $value) {
272
            if ($value instanceof \yii\db\ExpressionInterface) {
273
                continue;
274
            }
275
            if ($value instanceof \yii\db\PdoValue) {
276
                continue;
277
            }
278
            if (isset($columnSchemas[$name]) && in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) {
279
                $columns[$name] = new \yii\db\PdoValue($value, \PDO::PARAM_LOB);
280
            }
281
        }
282
        return parent::prepareUpdateSets($table, $columns, $condition, $params);
0 ignored issues
show
Bug introduced by
The variable $condition does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Unused Code introduced by
The call to QueryBuilder::prepareUpdateSets() has too many arguments starting with $params.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
283
    }
284
285
    /**
286
     * @inheritdoc
287
     */
288
    public function batchInsert($table, $columns, $rows, &$params = [])
289
    {
290 13
        if (empty($rows)) {
291
            return '';
292 13
        }
293 13
294 13
        $schema = $this->db->getSchema();
295
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
296
            $columnSchemas = $tableSchema->columns;
297
        } else {
298 13
            $columnSchemas = [];
299 13
        }
300 2
301 11
        $values = [];
302 13
        foreach ($rows as $row) {
303
            $vs = [];
304
            foreach ($row as $i => $value) {
305 13
                if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
306
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
307
                }
308
                if (is_string($value)) {
309
                    $value = $schema->quoteValue($value);
310
                } elseif ($value === false) {
311 8
                    $value = 0;
312
                } elseif ($value === null) {
313 8
                    $value = 'NULL';
314 2
                } elseif ($value instanceof ExpressionInterface) {
0 ignored issues
show
Bug introduced by
The class edgardmessias\db\firebird\ExpressionInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
315
                    $value = $this->buildExpression($value, $params);
316
                }
317 7
                $vs[] = $value;
318 7
            }
319 7
            $values[] = 'INSERT INTO ' . $schema->quoteTableName($table)
320
                    . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $vs) . ');';
321
        }
322
323
        foreach ($columns as $i => $name) {
324 7
            $columns[$i] = $schema->quoteColumnName($name);
325 7
        }
326 7
327 7
        return 'EXECUTE block AS BEGIN ' . implode(' ', $values) . ' END;';
328 7
    }
329 4
    
330
    /**
331 7
     * {@inheritdoc}
332 4
     * @see https://www.firebirdsql.org/refdocs/langrefupd21-update-or-insert.html
333 4
     * @see https://www.firebirdsql.org/refdocs/langrefupd21-merge.html
334 1
     */
335 4
    public function upsert($table, $insertColumns, $updateColumns, &$params)
336 3
    {
337
        /** @var Constraint[] $constraints */
338 7
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
339
        if (empty($uniqueNames)) {
340 7
            return 'UPDATE OR ' . $this->insert($table, $insertColumns, $params);
341 7
        }
342
343
        $onCondition = ['or'];
344 7
        $quotedTableName = $this->db->quoteTableName($table);
345 6
        foreach ($constraints as $constraint) {
346
            $constraintCondition = ['and'];
347
            foreach ($constraint->columnNames as $name) {
0 ignored issues
show
Bug introduced by
The expression $constraint->columnNames of type array<integer,string>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
348 7
                $quotedName = $this->db->quoteColumnName($name);
349
                $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
350
            }
351
            $onCondition[] = $constraintCondition;
352
        }
353
        $on = $this->buildCondition($onCondition, $params);
354 1
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
355
        if (!empty($placeholders)) {
356 1
            $usingSelectValues = [];
357
            foreach ($insertNames as $index => $name) {
358
                $usingSelectValues[$name] = new Expression($placeholders[$index]);
359
            }
360
            $usingSubQuery = (new Query())
361
                ->select($usingSelectValues)
362 2
                ->from('RDB$DATABASE');
363
            list($usingValues, $params) = $this->build($usingSubQuery, $params);
364 2
        }
365
        $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
366
            . 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" '
367
            . "ON ($on)";
368
        $insertValues = [];
369
        foreach ($insertNames as $name) {
370 1
            $quotedName = $this->db->quoteColumnName($name);
371
            if (strrpos($quotedName, '.') === false) {
372 1
                $quotedName = '"EXCLUDED".' . $quotedName;
373 1
            }
374
            $insertValues[] = $quotedName;
375
        }
376
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
377
            . ' VALUES (' . implode(', ', $insertValues) . ')';
378
        if ($updateColumns === false) {
379 1
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
380
        }
381 1
382 1
        if ($updateColumns === true) {
383 1
            $updateColumns = [];
384
            foreach ($updateNames as $name) {
385
                $quotedName = $this->db->quoteColumnName($name);
386
                if (strrpos($quotedName, '.') === false) {
387
                    $quotedName = '"EXCLUDED".' . $quotedName;
388
                }
389 3
                $updateColumns[$name] = new Expression($quotedName);
390
            }
391 3
        }
392 3
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
393 3
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
394
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
395 3
    }
396
397 3
    /**
398
     * @inheritdoc
399 3
     */
400
    public function renameTable($oldName, $newName)
401 3
    {
402 3
        throw new \yii\base\NotSupportedException($this->db->getDriverName() . ' does not support rename table.');
403 2
    }
404 2
    
405 2
    /**
406 2
     * @inheritdoc
407
     */
408
    public function truncateTable($table)
409
    {
410 3
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
411 3
    }
412 3
    
413
    /**
414 3
     * @inheritdoc
415
     */
416
    public function dropColumn($table, $column)
417
    {
418
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
419
            . ' DROP ' . $this->db->quoteColumnName($column);
420
    }
421
    
422
    /**
423
     * @inheritdoc
424
     */
425
    public function renameColumn($table, $oldName, $newName)
426
    {
427
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
428
            . ' ALTER ' . $this->db->quoteColumnName($oldName)
429
            . ' TO ' . $this->db->quoteColumnName($newName);
430
    }
431
    
432
    /**
433
     * @inheritdoc
434
     */
435
    public function alterColumn($table, $column, $type)
436
    {
437
        $schema = $this->db->getSchema();
438
        $tableSchema = $schema->getTableSchema($table);
439
        $columnSchema = $tableSchema->getColumn($column);
440
        
441
        $allowNullNewType = !preg_match('/not +null/i', $type);
442
        
443
        $type = preg_replace('/ +(not)? *null/i', '', $type);
444
        
445 3
        $hasType = false;
446 2
        
447
        $matches = [];
448
        if (isset($this->typeMap[$type])) {
449 2
            $hasType = true;
450 2
        } elseif (preg_match('/^(\w+)[\( ]/', $type, $matches)) {
451 2
            if (isset($this->typeMap[$matches[1]])) {
452
                $hasType = true;
453
            }
454
        }
455
        
456
        $baseSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
457 2
        . ' ALTER ' . $this->db->quoteColumnName($column)
458 2
        . (($hasType) ? ' TYPE ' : ' ') .  $this->getColumnType($type);
459 2
        
460
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
461 2
            $nullSql = false;
462 2
            
463
            if ($columnSchema->allowNull != $allowNullNewType) {
464
                $nullSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
465
                . ' ALTER ' . $this->db->quoteColumnName($column)
466
                . ($allowNullNewType ? ' DROP' : ' SET')
467
                . ' NOT NULL';
468
            }
469 1
470
            $sql = 'EXECUTE BLOCK AS BEGIN'
471 1
                . ' EXECUTE STATEMENT ' . $this->db->quoteValue($baseSql) . ';';
472
            
473
            /**
474
             * In any case (whichever option you choose), make sure that the column doesn't have any NULLs.
475
             * Firebird will not check it for you. Later when you backup the database, everything is fine,
476
             * but restore will fail as the NOT NULL column has NULLs in it. To be safe, each time you change from NULL to NOT NULL.
477 1
             */
478
            if (!$allowNullNewType) {
479 1
                $sql .= ' UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . $this->db->quoteColumnName($column) . ' = 0'
480 1
                    . ' WHERE ' . $this->db->quoteColumnName($column) . ' IS NULL;';
481
            }
482
483 1
            if ($nullSql) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nullSql of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
484
                $sql .= ' EXECUTE STATEMENT ' . $this->db->quoteValue($nullSql) . ';';
485
            }
486
487 1
            $sql .= ' END';
488 1
            return $sql;
489
        }
490
491 1
        if ($columnSchema->allowNull == $allowNullNewType) {
492 1
            return $baseSql;
493 1
        } else {
494 1
            $sql = 'EXECUTE BLOCK AS BEGIN'
495 1
                . ' EXECUTE STATEMENT ' . $this->db->quoteValue($baseSql) . ';'
496 1
                . ' UPDATE RDB$RELATION_FIELDS SET RDB$NULL_FLAG = ' . ($allowNullNewType ? 'NULL' : '1')
497
                . ' WHERE UPPER(RDB$FIELD_NAME) = UPPER(\'' . $column . '\') AND UPPER(RDB$RELATION_NAME) = UPPER(\'' . $table . '\');';
498
            /**
499 1
             * In any case (whichever option you choose), make sure that the column doesn't have any NULLs.
500
             * Firebird will not check it for you. Later when you backup the database, everything is fine,
501
             * but restore will fail as the NOT NULL column has NULLs in it. To be safe, each time you change from NULL to NOT NULL.
502 1
             */
503 1
            if (!$allowNullNewType) {
504
                $sql .= ' UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . $this->db->quoteColumnName($column) . ' = 0'
505
                    . ' WHERE ' . $this->db->quoteColumnName($column) . ' IS NULL;';
506 1
            }
507
            $sql .= ' END';
508
            return $sql;
509
        }
510
    }
511
    
512 5
    /**
513
     * @inheritdoc
514 5
     */
515
    public function dropIndex($name, $table)
516 5
    {
517 5
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
518
    }
519
    
520
    /**
521 5
     * {@inheritdoc}
522
     */
523 4
    public function addDefaultValue($name, $table, $column, $value)
524
    {
525
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
526
            . $this->db->quoteColumnName($column) . ' SET DEFAULT ' . $this->db->quoteValue($value);
527 4
    }
528 4
529
    /**
530
     * {@inheritdoc}
531
     */
532
    public function dropDefaultValue($name, $table)
533
    {
534 4
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
535 4
            . $this->db->quoteColumnName($name) . ' DROP DEFAULT';
536 4
    }
537 4
538
    /**
539
     * @inheritdoc
540 5
     */
541
    public function resetSequence($table, $value = null)
542
    {
543
        $tableSchema = $this->db->getTableSchema($table);
544 1
        if ($tableSchema === null) {
545
            throw new InvalidParamException("Table not found: $table");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated with message: since 2.0.14. Use [[InvalidArgumentException]] instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
546
        }
547
        if ($tableSchema->sequenceName === null) {
548
            throw new InvalidParamException("There is not sequence associated with table '$table'.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated with message: since 2.0.14. Use [[InvalidArgumentException]] instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
549
        }
550 3
551
        if ($value !== null) {
552 3
            $value = (int) $value;
553
        } else {
554 3
            // use master connection to get the biggest PK value
555 3
            $value = $this->db->useMaster(function (Connection $db) use ($tableSchema) {
556 1
                $key = false;
557
                foreach ($tableSchema->primaryKey as $name) {
558
                    if ($tableSchema->columns[$name]->autoIncrement) {
559
                        $key = $name;
560
                        break;
561
                    }
562 2
                }
563 2
                if ($key === false) {
564 2
                    return 0;
565
                }
566 2
                return $db->createCommand("SELECT MAX({$this->db->quoteColumnName($key)}) FROM {$this->db->quoteTableName($tableSchema->name)}")->queryScalar();
567
            }) + 1;
568
        }
569
570
        return "ALTER SEQUENCE {$this->db->quoteColumnName($tableSchema->sequenceName)} RESTART WITH $value";
571
    }
572
    
573
    /**
574
     * @inheritdoc
575
     */
576 3
    public function createTable($table, $columns, $options = null)
577
    {
578 3
        $sql = parent::createTable($table, $columns, $options);
579
        
580
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
581
            return $sql;
582
        }
583
584
        foreach ($columns as $name => $type) {
585
            if (!is_string($name)) {
586
                continue;
587
            }
588
            
589
            if (strpos($type, Schema::TYPE_PK) === 0 || strpos($type, Schema::TYPE_BIGPK) === 0) {
590
                $sqlTrigger = <<<SQLTRIGGER
591
CREATE TRIGGER tr_{$table}_{$name} FOR {$this->db->quoteTableName($table)}
592
ACTIVE BEFORE INSERT POSITION 0
593
AS
594
BEGIN
595
    if (NEW.{$this->db->quoteColumnName($name)} is NULL) then NEW.{$this->db->quoteColumnName($name)} = NEXT VALUE FOR seq_{$table}_{$name};
596
END
597
SQLTRIGGER;
598
                
599
                $sqlBlock = <<<SQL
600
EXECUTE block AS
601
BEGIN
602
    EXECUTE STATEMENT {$this->db->quoteValue($sql)};
603
    EXECUTE STATEMENT {$this->db->quoteValue("CREATE SEQUENCE seq_{$table}_{$name}")};
604
    EXECUTE STATEMENT {$this->db->quoteValue($sqlTrigger)};
605
END;
606
SQL;
607
608
                return $sqlBlock;
609
            }
610
        }
611
        
612
        return $sql;
613
    }
614
    
615
    /**
616
     * @inheritdoc
617
     */
618
    public function dropTable($table)
619
    {
620
        $sql = parent::dropTable($table);
621
        
622
        $tableSchema = $this->db->getTableSchema($table);
623
        if ($tableSchema === null || $tableSchema->sequenceName === null) {
624
            return $sql;
625
        }
626
        
627
        /**
628
         * Not drop sequence for sequence "GENERATED BY DEFAULT AS IDENTITY"
629
         */
630
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
631
            $sqlUserSquence = 'SELECT 1 FROM RDB$GENERATORS
632
                WHERE RDB$SYSTEM_FLAG = 0 AND RDB$GENERATOR_NAME = :name';
633
            
634
            $is_user_sequence = $this->db->createCommand($sqlUserSquence, [':name' => $tableSchema->sequenceName])->queryScalar();
635
            
636
            if (!$is_user_sequence) {
637
                return $sql;
638
            }
639
        }
640
        
641
        $sqlBlock = <<<SQL
642
EXECUTE block AS
643
BEGIN
644
    EXECUTE STATEMENT {$this->db->quoteValue($sql)};
645
    EXECUTE STATEMENT {$this->db->quoteValue("DROP SEQUENCE {$tableSchema->sequenceName}")};
646
END;
647
SQL;
648
                return $sqlBlock;
649
    }
650
651
    /**
652
     * Creates a SELECT EXISTS() SQL statement.
653
     * @param string $rawSql the subquery in a raw form to select from.
654
     * @return string the SELECT EXISTS() SQL statement.
655
     *
656
     * @since 2.0.8
657
     */
658
    public function selectExists($rawSql)
659
    {
660
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END FROM RDB$DATABASE';
661
    }
662
}
663