Passed
Push — master ( b7dc72...de6839 )
by Edgard
21:49
created

QueryBuilder   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 668
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 88.84%

Importance

Changes 0
Metric Value
wmc 121
lcom 1
cbo 14
dl 0
loc 668
ccs 215
cts 242
cp 0.8884
rs 1.9847
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 12 2
A defaultExpressionBuilders() 0 8 1
D build() 0 40 9
B buildSelect() 0 19 6
B buildLimit() 0 18 5
A buildOrderByAndLimit() 0 15 3
B buildUnion() 0 19 5
C prepareInsertValues() 0 32 12
A prepareInsertSelectSubQuery() 0 11 2
B prepareUpdateSets() 0 19 7
C rawInsert() 0 42 11
A batchInsert() 0 20 4
C upsert() 0 68 15
A renameTable() 0 4 1
A truncateTable() 0 4 1
A dropColumn() 0 5 1
A renameColumn() 0 6 1
D alterColumn() 0 76 13
A dropIndex() 0 4 1
A addDefaultValue() 0 5 1
A dropDefaultValue() 0 5 1
C resetSequence() 0 31 7
B createTable() 0 38 6
B dropTable() 0 32 5
A selectExists() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

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
            'yii\db\conditions\LikeCondition' => 'edgardmessias\db\firebird\conditions\LikeConditionBuilder',
69 181
        ]);
70 181
    }
71 181
72
    /**
73
     * Generates a SELECT SQL statement from a [[Query]] object.
74 181
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
75 181
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
76
     * be included in the result with the additional parameters generated during the query building process.
77 181
     * @return array the generated SQL statement (the first array element) and the corresponding
78 34
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
79 34
     * include those provided in `$params`.
80 34
     */
81
    public function build($query, $params = [])
82
    {
83
        $query = $query->prepare($this);
84 181
85 2
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
86 2
87 2
        $clauses = [
88
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
89
            $this->buildFrom($query->from, $params),
90
            $this->buildJoin($query->join, $params),
91
            $this->buildWhere($query->where, $params),
92 181
            $this->buildGroupBy($query->groupBy),
93 181
            $this->buildHaving($query->having, $params),
94 2
        ];
95
96
        $sql = implode($this->separator, array_filter($clauses));
97 181
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
98
99
        if (!empty($query->orderBy)) {
100
            foreach ($query->orderBy as $expression) {
101
                if ($expression instanceof Expression) {
102
                    $params = array_merge($params, $expression->params);
103 181
                }
104
            }
105 181
        }
106 53
        if (!empty($query->groupBy)) {
107 53
            foreach ($query->groupBy as $expression) {
108 3
                if ($expression instanceof Expression) {
109
                    $params = array_merge($params, $expression->params);
110 52
                }
111 52
            }
112 21
        }
113 21
114
        $union = $this->buildUnion($query->union, $params);
115 52
        if ($union !== '') {
116
            $sql = "$sql{$this->separator}$union";
117
        }
118
119
        return [$sql, $params];
120 181
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
126 5
    {
127
        if (is_array($columns)) {
128 5
            foreach ($columns as $i => $column) {
129 5
                if (!is_string($column)) {
130 5
                    continue;
131
                }
132 5
                $matches = [];
133 5
                if (preg_match('/^(COUNT|SUM|AVG|MIN|MAX)\([\{\[]{0,2}(\w+|\*)[\}\]]{0,2}\)$/i', $column, $matches)) {
134 5
                    $function = $matches[1];
135 5
                    $alias = $matches[2] != '*' ? $matches[2] : 'ALL';
136 5
137 5
                    $columns[$i] = "{$column} AS {$function}_{$alias}";
138 5
                }
139 5
            }
140
        }
141 5
142
        return parent::buildSelect($columns, $params, $distinct, $selectOption);
143
    }
144 5
    
145
    public function buildLimit($limit, $offset)
146 5
    {
147
        $sql = '';
148
        if ($this->hasLimit($limit)) {
149
            if ($limit instanceof \yii\db\ExpressionInterface) {
150
                $limit = "($limit)";
151
            }
152 181
            $sql = 'FIRST ' . $limit;
153
        }
154
        if ($this->hasOffset($offset)) {
155 181
            if ($offset instanceof \yii\db\ExpressionInterface) {
156 181
                $offset = "($offset)";
157 34
            }
158
            $sql .= ' SKIP ' . $offset;
159
        }
160 181
161 181
        return ltrim($sql);
162
    }
163 181
164 180
    /**
165
     * @inheritdoc
166
     */
167
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
168 3
    {
169 2
170 2
        $orderBy = $this->buildOrderBy($orderBy);
171 2
        if ($orderBy !== '') {
172
            $sql .= $this->separator . $orderBy;
173
        }
174
        
175 2
        $limit = $this->buildLimit($limit, $offset);
176 2
        if ($limit !== '') {
177 2
            $sql = preg_replace('/^SELECT /i', 'SELECT ' . $limit . ' ', $sql, 1);
178 2
        }
179
        
180
        return $sql;
181 1
    }
182 1
183 1
    /**
184 1
     * @param array $unions
185
     * @param array $params the binding parameters to be populated
186
     * @return string the UNION clause built from [[Query::$union]].
187
     */
188
    public function buildUnion($unions, &$params)
189
    {
190
        if (empty($unions)) {
191
            return '';
192
        }
193
194
        $result = '';
195
196 181
        foreach ($unions as $i => $union) {
197
            $query = $union['query'];
198 181
            if ($query instanceof Query) {
199 181
                list($unions[$i]['query'], $params) = $this->build($query, $params);
200
            }
201
202 2
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . $unions[$i]['query'] . ' ';
203
        }
204 2
205 2
        return trim($result);
206 2
    }
207 2
208
    /**
209
     * @inheritdoc
210 2
     */
211
    public function prepareInsertValues($table, $columns, $params = [])
212
    {
213 2
        $schema = $this->db->getSchema();
214
        $tableSchema = $schema->getTableSchema($table);
215
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
216
        
217
        //Empty insert
218
        if (empty($columns) && !empty($columnSchemas)) {
219
            $columns = [];
220
            foreach ($columnSchemas as $columnSchema) {
221 3
                if (!$columnSchema->autoIncrement) {
222
                    $columns[$columnSchema->name] = $columnSchema->defaultValue;
223 3
                }
224
            }
225
        }
226
227
        if (is_array($columns)) {
228
            foreach ($columns as $name => $value) {
229 3
                if ($value instanceof \yii\db\ExpressionInterface) {
230
                    continue;
231 3
                }
232
                if ($value instanceof \yii\db\PdoValue) {
233
                    continue;
234 3
                }
235
                if (isset($columnSchemas[$name]) && in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) {
236
                    $columns[$name] = [$value, \PDO::PARAM_LOB];
237
                }
238
            }
239
        }
240 23
241
        return parent::prepareInsertValues($table, $columns, $params);
242 23
    }
243 23
    
244 23
    /**
245
     * @inheritdoc
246
     */
247
    protected function prepareInsertSelectSubQuery($columns, $schema, $params = [])
248
    {
249
        /**
250 23
         * @see https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable
251 1
         */
252 1
        if (version_compare($this->db->firebird_version, '3.0.0', '<')) {
253 1
            throw new NotSupportedException('Firebird < 3.0.0 has the "Unstable Cursor" problem');
254 1
        }
255
256
        return parent::prepareInsertSelectSubQuery($columns, $schema, $params);
257
    }
258
259 23
    /**
260 23
     * @inheritdoc
261 23
     */
262 1
    public function prepareUpdateSets($table, $columns, $params = [])
263 23
    {
264 23
        $schema = $this->db->getSchema();
265
        $tableSchema = $schema->getTableSchema($table);
266
        $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
267
268
        foreach ($columns as $name => $value) {
269 23
            if ($value instanceof \yii\db\ExpressionInterface) {
270
                continue;
271
            }
272
            if ($value instanceof \yii\db\PdoValue) {
273
                continue;
274
            }
275
            if (isset($columnSchemas[$name]) && in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) {
276
                $columns[$name] = [$value, \PDO::PARAM_LOB];
277
            }
278
        }
279
        return parent::prepareUpdateSets($table, $columns, $params);
280
    }
281
    
282
    public function rawInsert($table, $columns, $values, &$params)
283
    {
284
        $schema = $this->db->getSchema();
285
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
286
            $columnSchemas = $tableSchema->columns;
287
        } else {
288
            $columnSchemas = [];
289
        }
290 13
291
        $cs = [];
292 13
        $vs = [];
293 13
        foreach ($values as $i => $value) {
294 13
            if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
295
                $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
296
            }
297
            if (is_string($value)) {
298 13
                // Quote the {{table}} to {{@table@}} for insert
299 13
                $value = preg_replace('/(\{\{)(%?[\w\-\. ]+%?)(\}\})/', '\1@\2@\3', $value);
300 2
                $value = $schema->quoteValue($value);
301 11
            } elseif (is_float($value)) {
302 13
                // ensure type cast always has . as decimal separator in all locales
303
                $value = \yii\helpers\StringHelper::floatToString($value);
304
            } elseif ($value === false) {
305 13
                $value = 0;
306
            } elseif ($value === null) {
307
                $value = 'NULL';
308
            } 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...
309
                $value = $this->buildExpression($value, $params);
310
            }
311 8
312
            $cs[] = $schema->quoteColumnName($columns[$i]);
313 8
            $vs[] = $value;
314 2
        }
315
316
        if (empty($vs)) {
317 7
            return 'INSERT INTO ' . $schema->quoteTableName($table)
318 7
                    . ' DEFAULT VALUES';
319 7
        }
320
321
        return 'INSERT INTO ' . $schema->quoteTableName($table)
322
                . ' (' . implode(', ', $cs) . ') VALUES (' . implode(', ', $vs) . ')';
323
    }
324 7
325 7
    /**
326 7
     * @inheritdoc
327 7
     */
328 7
    public function batchInsert($table, $columns, $rows, &$params = [])
329 4
    {
330
        if (empty($rows)) {
331 7
            return '';
332 4
        }
333 4
334 1
        $schema = $this->db->getSchema();
335 4
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
336 3
            $columnSchemas = $tableSchema->columns;
0 ignored issues
show
Unused Code introduced by
$columnSchemas is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
337
        } else {
338 7
            $columnSchemas = [];
0 ignored issues
show
Unused Code introduced by
$columnSchemas is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
339
        }
340 7
341 7
        $values = [];
342
        foreach ($rows as $row) {
343
            $values[] = $this->rawInsert($table, $columns, $row, $params) . ';';
344 7
        }
345 6
346
        return 'EXECUTE block AS BEGIN ' . implode(' ', $values) . ' END;';
347
    }
348 7
    
349
    /**
350
     * {@inheritdoc}
351
     * @see https://www.firebirdsql.org/refdocs/langrefupd21-update-or-insert.html
352
     * @see https://www.firebirdsql.org/refdocs/langrefupd21-merge.html
353
     */
354 1
    public function upsert($table, $insertColumns, $updateColumns, &$params)
355
    {
356 1
        /** @var Constraint[] $constraints */
357
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
358
        if (empty($uniqueNames)) {
359
            return 'UPDATE OR ' . $this->insert($table, $insertColumns, $params);
360
        }
361
362 2
        $onCondition = ['or'];
363
        $quotedTableName = $this->db->quoteTableName($table);
364 2
        foreach ($constraints as $constraint) {
365
            $constraintCondition = ['and'];
366
            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...
367
                $quotedName = $this->db->quoteColumnName($name);
368
                $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
369
            }
370 1
            $onCondition[] = $constraintCondition;
371
        }
372 1
        $on = $this->buildCondition($onCondition, $params);
373 1
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
374
        if (!empty($placeholders)) {
375
            /** @var Schema $schema */
376
            $schema = $this->db->getSchema();
377
            $tableSchema = $schema->getTableSchema($table);
378
            $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
379 1
            
380
            $usingSelectValues = [];
381 1
            foreach ($insertNames as $index => $name) {
382 1
                if (isset($columnSchemas[$name])) {
383 1
                    $usingSelectValues[$name] = new Expression("CAST({$placeholders[$index]} AS {$columnSchemas[$name]->dbType})");
384
                }
385
            }
386
            $usingSubQuery = (new Query())
387
                ->select($usingSelectValues)
388
                ->from('RDB$DATABASE');
389 3
            list($usingValues, $params) = $this->build($usingSubQuery, $params);
390
        }
391 3
        $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
392 3
            . 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" '
393 3
            . "ON ($on)";
394
        $insertValues = [];
395 3
        foreach ($insertNames as $name) {
396
            $quotedName = $this->db->quoteColumnName($name);
397 3
            if (strrpos($quotedName, '.') === false) {
398
                $quotedName = '"EXCLUDED".' . $quotedName;
399 3
            }
400
            $insertValues[] = $quotedName;
401 3
        }
402 3
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
403 2
            . ' VALUES (' . implode(', ', $insertValues) . ')';
404 2
        if ($updateColumns === false) {
405 2
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
406 2
        }
407
408
        if ($updateColumns === true) {
409
            $updateColumns = [];
410 3
            foreach ($updateNames as $name) {
411 3
                $quotedName = $this->db->quoteColumnName($name);
412 3
                if (strrpos($quotedName, '.') === false) {
413
                    $quotedName = '"EXCLUDED".' . $quotedName;
414 3
                }
415
                $updateColumns[$name] = new Expression($quotedName);
416
            }
417
        }
418
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
419
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
420
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
421
    }
422
423
    /**
424
     * @inheritdoc
425
     */
426
    public function renameTable($oldName, $newName)
427
    {
428
        throw new \yii\base\NotSupportedException($this->db->getDriverName() . ' does not support rename table.');
429
    }
430
    
431
    /**
432
     * @inheritdoc
433
     */
434
    public function truncateTable($table)
435
    {
436
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
437
    }
438
    
439
    /**
440
     * @inheritdoc
441
     */
442
    public function dropColumn($table, $column)
443
    {
444
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
445 3
            . ' DROP ' . $this->db->quoteColumnName($column);
446 2
    }
447
    
448
    /**
449 2
     * @inheritdoc
450 2
     */
451 2
    public function renameColumn($table, $oldName, $newName)
452
    {
453
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
454
            . ' ALTER ' . $this->db->quoteColumnName($oldName)
455
            . ' TO ' . $this->db->quoteColumnName($newName);
456
    }
457 2
    
458 2
    /**
459 2
     * @inheritdoc
460
     */
461 2
    public function alterColumn($table, $column, $type)
462 2
    {
463
        $schema = $this->db->getSchema();
464
        $tableSchema = $schema->getTableSchema($table);
465
        $columnSchema = $tableSchema->getColumn($column);
466
        
467
        $allowNullNewType = !preg_match('/not +null/i', $type);
468
        
469 1
        $type = preg_replace('/ +(not)? *null/i', '', $type);
470
        
471 1
        $hasType = false;
472
        
473
        $matches = [];
474
        if (isset($this->typeMap[$type])) {
475
            $hasType = true;
476
        } elseif (preg_match('/^(\w+)[\( ]/', $type, $matches)) {
477 1
            if (isset($this->typeMap[$matches[1]])) {
478
                $hasType = true;
479 1
            }
480 1
        }
481
        
482
        $baseSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
483 1
        . ' ALTER ' . $this->db->quoteColumnName($column)
484
        . (($hasType) ? ' TYPE ' : ' ') .  $this->getColumnType($type);
485
        
486
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
487 1
            $nullSql = false;
488 1
            
489
            if ($columnSchema->allowNull != $allowNullNewType) {
490
                $nullSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
491 1
                . ' ALTER ' . $this->db->quoteColumnName($column)
492 1
                . ($allowNullNewType ? ' DROP' : ' SET')
493 1
                . ' NOT NULL';
494 1
            }
495 1
496 1
            $sql = 'EXECUTE BLOCK AS BEGIN'
497
                . ' EXECUTE STATEMENT ' . $this->db->quoteValue($baseSql) . ';';
498
            
499 1
            /**
500
             * In any case (whichever option you choose), make sure that the column doesn't have any NULLs.
501
             * Firebird will not check it for you. Later when you backup the database, everything is fine,
502 1
             * 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.
503 1
             */
504
            if (!$allowNullNewType) {
505
                $sql .= ' UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . $this->db->quoteColumnName($column) . ' = 0'
506 1
                    . ' WHERE ' . $this->db->quoteColumnName($column) . ' IS NULL;';
507
            }
508
509
            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...
510
                $sql .= ' EXECUTE STATEMENT ' . $this->db->quoteValue($nullSql) . ';';
511
            }
512 5
513
            $sql .= ' END';
514 5
            return $sql;
515
        }
516 5
517 5
        if ($columnSchema->allowNull == $allowNullNewType) {
518
            return $baseSql;
519
        } else {
520
            $sql = 'EXECUTE BLOCK AS BEGIN'
521 5
                . ' EXECUTE STATEMENT ' . $this->db->quoteValue($baseSql) . ';'
522
                . ' UPDATE RDB$RELATION_FIELDS SET RDB$NULL_FLAG = ' . ($allowNullNewType ? 'NULL' : '1')
523 4
                . ' WHERE UPPER(RDB$FIELD_NAME) = UPPER(\'' . $column . '\') AND UPPER(RDB$RELATION_NAME) = UPPER(\'' . $table . '\');';
524
            /**
525
             * In any case (whichever option you choose), make sure that the column doesn't have any NULLs.
526
             * Firebird will not check it for you. Later when you backup the database, everything is fine,
527 4
             * 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.
528 4
             */
529
            if (!$allowNullNewType) {
530
                $sql .= ' UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . $this->db->quoteColumnName($column) . ' = 0'
531
                    . ' WHERE ' . $this->db->quoteColumnName($column) . ' IS NULL;';
532
            }
533
            $sql .= ' END';
534 4
            return $sql;
535 4
        }
536 4
    }
537 4
    
538
    /**
539
     * @inheritdoc
540 5
     */
541
    public function dropIndex($name, $table)
542
    {
543
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
544 1
    }
545
    
546
    /**
547
     * {@inheritdoc}
548
     */
549
    public function addDefaultValue($name, $table, $column, $value)
550 3
    {
551
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
552 3
            . $this->db->quoteColumnName($column) . ' SET DEFAULT ' . $this->db->quoteValue($value);
553
    }
554 3
555 3
    /**
556 1
     * {@inheritdoc}
557
     */
558
    public function dropDefaultValue($name, $table)
559
    {
560
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
561
            . $this->db->quoteColumnName($name) . ' DROP DEFAULT';
562 2
    }
563 2
564 2
    /**
565
     * @inheritdoc
566 2
     */
567
    public function resetSequence($table, $value = null)
568
    {
569
        $tableSchema = $this->db->getTableSchema($table);
570
        if ($tableSchema === null) {
571
            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...
572
        }
573
        if ($tableSchema->sequenceName === null) {
574
            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...
575
        }
576 3
577
        if ($value !== null) {
578 3
            $value = (int) $value;
579
        } else {
580
            // use master connection to get the biggest PK value
581
            $value = $this->db->useMaster(function (Connection $db) use ($tableSchema) {
582
                $key = false;
583
                foreach ($tableSchema->primaryKey as $name) {
584
                    if ($tableSchema->columns[$name]->autoIncrement) {
585
                        $key = $name;
586
                        break;
587
                    }
588
                }
589
                if ($key === false) {
590
                    return 0;
591
                }
592
                return $db->createCommand("SELECT MAX({$this->db->quoteColumnName($key)}) FROM {$this->db->quoteTableName($tableSchema->name)}")->queryScalar();
593
            }) + 1;
594
        }
595
596
        return "ALTER SEQUENCE {$this->db->quoteColumnName($tableSchema->sequenceName)} RESTART WITH $value";
597
    }
598
    
599
    /**
600
     * @inheritdoc
601
     */
602
    public function createTable($table, $columns, $options = null)
603
    {
604
        $sql = parent::createTable($table, $columns, $options);
605
        
606
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
607
            return $sql;
608
        }
609
610
        foreach ($columns as $name => $type) {
611
            if (!is_string($name)) {
612
                continue;
613
            }
614
            
615
            if (strpos($type, Schema::TYPE_PK) === 0 || strpos($type, Schema::TYPE_BIGPK) === 0) {
616
                $sqlTrigger = <<<SQLTRIGGER
617
CREATE TRIGGER tr_{$table}_{$name} FOR {$this->db->quoteTableName($table)}
618
ACTIVE BEFORE INSERT POSITION 0
619
AS
620
BEGIN
621
    if (NEW.{$this->db->quoteColumnName($name)} is NULL) then NEW.{$this->db->quoteColumnName($name)} = NEXT VALUE FOR seq_{$table}_{$name};
622
END
623
SQLTRIGGER;
624
                
625
                $sqlBlock = <<<SQL
626
EXECUTE block AS
627
BEGIN
628
    EXECUTE STATEMENT {$this->db->quoteValue($sql)};
629
    EXECUTE STATEMENT {$this->db->quoteValue("CREATE SEQUENCE seq_{$table}_{$name}")};
630
    EXECUTE STATEMENT {$this->db->quoteValue($sqlTrigger)};
631
END;
632
SQL;
633
634
                return $sqlBlock;
635
            }
636
        }
637
        
638
        return $sql;
639
    }
640
    
641
    /**
642
     * @inheritdoc
643
     */
644
    public function dropTable($table)
645
    {
646
        $sql = parent::dropTable($table);
647
        
648
        $tableSchema = $this->db->getTableSchema($table);
649
        if ($tableSchema === null || $tableSchema->sequenceName === null) {
650
            return $sql;
651
        }
652
        
653
        /**
654
         * Not drop sequence for sequence "GENERATED BY DEFAULT AS IDENTITY"
655
         */
656
        if (version_compare($this->db->firebird_version, '3.0.0', '>=')) {
657
            $sqlUserSquence = 'SELECT 1 FROM RDB$GENERATORS
658
                WHERE RDB$SYSTEM_FLAG = 0 AND RDB$GENERATOR_NAME = :name';
659
            
660
            $is_user_sequence = $this->db->createCommand($sqlUserSquence, [':name' => $tableSchema->sequenceName])->queryScalar();
661
            
662
            if (!$is_user_sequence) {
663
                return $sql;
664
            }
665
        }
666
        
667
        $sqlBlock = <<<SQL
668
EXECUTE block AS
669
BEGIN
670
    EXECUTE STATEMENT {$this->db->quoteValue($sql)};
671
    EXECUTE STATEMENT {$this->db->quoteValue("DROP SEQUENCE {$tableSchema->sequenceName}")};
672
END;
673
SQL;
674
                return $sqlBlock;
675
    }
676
677
    /**
678
     * Creates a SELECT EXISTS() SQL statement.
679
     * @param string $rawSql the subquery in a raw form to select from.
680
     * @return string the SELECT EXISTS() SQL statement.
681
     *
682
     * @since 2.0.8
683
     */
684
    public function selectExists($rawSql)
685
    {
686
        return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END FROM RDB$DATABASE';
687
    }
688
}
689