QueryBuilder::rawInsert()   C
last analyzed

Complexity

Conditions 12
Paths 100

Size

Total Lines 44
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 12.4202

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 18
cts 21
cp 0.8571
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 30
nc 100
nop 4
crap 12.4202

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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