Schema::createColumnSchemaBuilder()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 1
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\InvalidCallException;
12
use yii\db\CheckConstraint;
13
use yii\db\Constraint;
14
use yii\db\ConstraintFinderInterface;
15
use yii\db\ConstraintFinderTrait;
16
use yii\db\Exception;
17
use yii\db\Expression;
18
use yii\db\ForeignKeyConstraint;
19
use yii\db\IndexConstraint;
20
use yii\db\Schema as BaseSchema;
21
use yii\db\TableSchema;
22
use yii\db\Transaction;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, edgardmessias\db\firebird\Transaction.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
23
use yii\db\ViewFinderTrait;
24
use yii\helpers\ArrayHelper;
25
26
/**
27
 * Schema represents the Firebird schema information.
28
 *
29
 * @property string[] $indexNames All index names in the Firebird. This property is read-only.
30
 * @property IndexSchema[] $indexSchemas The metadata for all indexes in the Firebird. Each array element is an
31
 * instance of [[IndexSchema]] or its child class. This property is read-only.
32
 * @property array $indexTypes All index types in the Firebird in format: index name => index type. This
33
 * property is read-only.
34
 * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
35
 *
36
 * @author Edgard Lorraine Messias <[email protected]>
37
 * @since 2.0
38
 */
39
class Schema extends BaseSchema implements ConstraintFinderInterface
40
{
41
    use ViewFinderTrait;
42
    use ConstraintFinderTrait;
43
44
    private $_lastInsertID = null;
45
46
    /**
47
     * @var array map of DB errors and corresponding exceptions
48
     * If left part is found in DB error message exception class from the right part is used.
49
     */
50
    public $exceptionMap = [
51
        'SQLSTATE[23'                                               => 'yii\db\IntegrityException',
52
        'SQLSTATE[HY000]: General error: -803 violation of PRIMARY' => 'yii\db\IntegrityException',
53
    ];
54
    public $reservedWords = [
55
        'ADD',
56
        'ADMIN',
57
        'ALL',
58
        'ALTER',
59
        'AND',
60
        'ANY',
61
        'AS',
62
        'AT',
63
        'AVG',
64
        'BEGIN',
65
        'BETWEEN',
66
        'BIGINT',
67
        'BIT_LENGTH',
68
        'BLOB',
69
        'BOTH',
70
        'BOOLEAN',
71 99
        'BY',
72
        'CASE',
73 99
        'CAST',
74
        'CHAR',
75
        'CHAR_LENGTH',
76
        'CHARACTER',
77
        'CHARACTER_LENGTH',
78
        'CHECK',
79 2
        'CLOSE',
80
        'COLLATE',
81 2
        'COLUMN',
82
        'COMMIT',
83
        'CONNECT',
84 157
        'CONSTRAINT',
85
        'CORR',
86 157
        'COUNT',
87
        'COVAR_POP',
88
        'CREATE',
89
        'CROSS',
90 157
        'CURRENT',
91 157
        'CURRENT_CONNECTION',
92 43
        'CURRENT_DATE',
93
        'CURRENT_ROLE',
94
        'CURRENT_TIME',
95 156
        'CURRENT_TIMESTAMP',
96
        'CURRENT_TRANSACTION',
97
        'CURRENT_USER',
98 173
        'CURSOR',
99
        'DATE',
100 173
        'DAY',
101 6
        'DEC',
102
        'DECIMAL',
103 173
        'DECLARE',
104
        'DEFAULT',
105
        'DELETE',
106 106
        'DELETING',
107
        'DETERMINISTIC',
108 106
        'DISCONNECT',
109 106
        'DISTINCT',
110 106
        'DOUBLE',
111 104
        'DROP',
112 104
        'ELSE',
113
        'END',
114 7
        'ESCAPE',
115
        'EXECUTE',
116
        'EXISTS',
117 93
        'EXTERNAL',
118
        'EXRACT',
119 93
        'FALSE',
120
        'FETCH',
121
        'FILTER',
122
        'FLOAT',
123
        'FOR',
124
        'FOREIGN',
125
        'FROM',
126
        'FULL',
127 93
        'FUNCTION',
128
        'GDSCODE',
129 93
        'GLOBAL',
130
        'GRANT',
131
        'GROUP',
132
        'HAVING',
133
        'HOUR',
134
        'IN',
135
        'INDEX',
136
        'INNER',
137 106
        'INSENSITIVE',
138
        'INSERT',
139 106
        'INSERTING',
140 106
        'INT',
141
        'INTEGER',
142
        'INTO',
143
        'IS',
144
        'JOIN',
145 106
        'LEADING',
146 106
        'LEFT',
147
        'LIKE',
148 106
        'LONG',
149
        'LOWER',
150
        'MAX',
151
        'MAXIMUM_SEGMENT',
152
        'MERGE',
153
        'MIN',
154
        'MINUTE',
155
        'MONTH',
156 106
        'NATIONAL',
157
        'NATURAL',
158
        'NCHAR',
159
        'NO',
160
        'NOT',
161
        'NULL',
162
        'NUMERIC',
163
        'OCTET_LENGTH',
164
        'OF',
165
        'OFFSET',
166
        'ON',
167
        'OPEN',
168
        'OR',
169
        'ORDER',
170
        'OUTER',
171
        'OVER',
172
        'PARAMETER',
173
        'PASSWORD',
174 106
        'PLAN',
175
        'POSITION',
176
        'POST_EVENT',
177
        'PRECISION',
178
        'PRIMARY',
179
        'PROCEDURE',
180
        'RDB$DB_KEY',
181
        'RDB$RECORD_VERSION',
182
        'REAL',
183
        'RECORD_VERSION',
184 106
        'RECREATE',
185
        'RECURSIVE',
186 106
        'REFERENCES',
187
        'REGR_AVGX',
188 106
        'REGR_AVGY',
189 106
        'REGR_COUNT',
190 106
        'REGR_INTERCEPT',
191
        'REGR_R2',
192
        'REGR_SLOPE',
193
        'REGR_SXX',
194
        'REGR_SXY',
195
        'REGR_SYY',
196
        'RELEASE',
197
        'RETURN',
198
        'RETURNING_VALUES',
199
        'RETURNS',
200
        'REVOKE',
201 104
        'RIGHT',
202
        'ROLLBACK',
203 104
        'ROW',
204
        'ROWS',
205
        'ROW_COUNT',
206
        'SAVEPOINT',
207 104
        'SCROLL',
208 104
        'SECOND',
209 104
        'SELECT',
210 104
        'SENSITIVE',
211 104
        'SET',
212
        'SIMILAR',
213 104
        'SOME',
214 104
        'SQLCODE',
215 104
        'SQLSTATE',
216 87
        'START',
217 87
        'STDDEV_POP',
218 79
        'STDDEV_SAMP',
219 13
        'SUM',
220 13
        'TABLE',
221
        'THEN',
222
        'TIME',
223 104
        'TIMESTAMP',
224 104
        'TO',
225 104
        'TRAILING',
226
        'TRIGGER',
227
        'TRIM',
228 104
        'TRUE',
229
        'UNION',
230
        'UNIQUE',
231
        'UNKNOWN',
232
        'UPDATE',
233
        'UPDATING',
234
        'UPPER',
235 104
        'USER',
236
        'USING',
237 104
        'VALUE',
238
        'VALUES',
239
        'VARCHAR',
240
        'VARIABLE',
241
        'VARYING',
242
        'VAR_POP',
243
        'VAR_SAMP',
244
        'VIEW',
245
        'WHEN',
246 104
        'WHERE',
247
        'WHILE',
248 104
        'WITH',
249 104
        'YEAR',
250 104
    ];
251 104
252 104
    /**
253 104
     * @var array mapping from physical column types (keys) to abstract column types (values)
254
     */
255 104
    public $typeMap = [
256
        'bigint'             => self::TYPE_BIGINT,
257 104
        'char'               => self::TYPE_CHAR,
258 104
        'varchar'            => self::TYPE_STRING,
259
        'timestamp'          => self::TYPE_TIMESTAMP,
260 63
        'decimal'            => self::TYPE_DECIMAL,
261 63
        'float'              => self::TYPE_FLOAT,
262
        'blob'               => self::TYPE_BINARY,
263
        'integer'            => self::TYPE_INTEGER,
264 63
        'blob sub_type text' => self::TYPE_TEXT,
265
        'numeric'            => self::TYPE_DECIMAL,
266 104
        'double precision'   => self::TYPE_DOUBLE,
267 103
        'smallint'           => self::TYPE_SMALLINT,
268
    ];
269 104
270
    /**
271 104
     * {@inheritdoc}
272
     */
273
    protected function resolveTableName($name)
274
    {
275
        $resolvedName = new TableSchema();
276
        $this->resolveTableNames($resolvedName, $name);
277
        return $resolvedName;
278
    }
279
280
    /**
281
     * Creates a query builder for the database.
282
     * This method may be overridden by child classes to create a DBMS-specific query builder.
283
     * @return QueryBuilder query builder instance
284
     */
285
    public function createQueryBuilder()
286
    {
287 104
        return new QueryBuilder($this->db);
288
    }
289
290 104
    /**
291 104
     * @inheritdoc
292 88
     */
293 88
    public function createColumnSchemaBuilder($type, $length = null)
294 88
    {
295 88
        return new ColumnSchemaBuilder($type, $length);
296
    }
297 104
298 104
    public function quoteSimpleTableName($name)
299 104
    {
300 104
        if ($this->db->tablePrefix !== '') {
301 104
            return $name;
302
        }
303
304
        $word = strtoupper(str_replace('%', '', $name));
305
        if (in_array($word, $this->reservedWords)) {
306
            return strpos($name, '"') !== false ? $name : '"' . $name . '"';
307 104
        }
308 16
309 16
        return $name;
310 16
    }
311 16
312 16
    public function quoteSimpleColumnName($name)
313
    {
314 104
        if (in_array(strtoupper($name), $this->reservedWords)) {
315 101
            return parent::quoteSimpleColumnName($name);
316 47
        }
317 47
        return $name;
318 3
    }
319 3
320 3
    protected function loadTableSchema($name)
321 3
    {
322 3
        $table = $this->resolveTableName($name);
323 47
        if ($this->findColumns($table)) {
324 46
            $this->findConstraints($table);
325 46
            return $table;
326 46
        }
327 46
        return null;
328 46
    }
329
330 4
    public function getPdoType($data)
331 4
    {
332
        static $typeMap = [
333 47
            // php type => PDO type
334 89
            'boolean'  => \PDO::PARAM_INT,
335 15
            'integer'  => \PDO::PARAM_INT,
336 15
            'string'   => \PDO::PARAM_STR,
337 15
            'resource' => \PDO::PARAM_LOB,
338 15
            'NULL'     => \PDO::PARAM_NULL,
339 15
        ];
340
        $type = gettype($data);
341 15
342
        return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR;
343
    }
344 104
345
    /**
346 104
     *
347 104
     * @param TableSchema $table
348 104
     * @param string $name
349 104
     */
350 104
    protected function resolveTableNames($table, $name)
351
    {
352
        $parts = explode('.', str_replace('"', '', $name));
353
        if (isset($parts[1])) {
354
            $table->schemaName = $parts[0];
355 104
            $table->name = strtolower($parts[1]);
356
            $table->fullName = $this->quoteTableName($table->schemaName) . '.' . $this->quoteTableName($table->name);
357 104
        } else {
358 104
            $table->name = strtolower($parts[0]);
359 63
            $table->fullName = $this->quoteTableName($table->name);
360 63
        }
361 15
    }
362
363 63
    /**
364
     * Collects the table column metadata.
365
     *
366
     * @param TableSchema $table the table metadata
367 104
     * @return boolean whether the table exists in the database
368
     */
369
    protected function findColumns($table)
370
    {
371
        // Zoggo - Converted sql to use join syntax
372
        // robregonm - Added isAutoInc
373
        $sql = 'SELECT
374
                    rel.rdb$field_name AS fname,
375 104
                    rel.rdb$default_source AS fdefault,
376
                    fld.rdb$field_type AS fcodtype,
377
                    fld.rdb$field_sub_type AS fcodsubtype,
378
                    fld.rdb$field_length AS flength,
379
                    fld.rdb$character_length AS fcharlength,
380
                    fld.rdb$field_scale AS fscale,
381
                    fld.rdb$field_precision AS fprecision,
382
                    rel.rdb$null_flag AS fnull,
383
                    rel.rdb$description AS fcomment,
384
                    fld.rdb$default_value AS fdefault_value,';
385
386
        if ($this->db->supportColumnIdentity) {
387
            $sql .= '
388
                    rel.rdb$generator_name AS fgenerator_name,';
389
        }
390
391 104
        $sql .= '
392
                    (SELECT RDB$TRIGGER_SOURCE FROM RDB$TRIGGERS
393 104
                        WHERE RDB$SYSTEM_FLAG = 0
394
                        AND UPPER(RDB$RELATION_NAME)=UPPER(\'' . $table->name . '\')
395
                        AND RDB$TRIGGER_TYPE = 1
396
                        AND RDB$TRIGGER_INACTIVE = 0
397
                        AND (UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=GEN_ID%\'
398 104
                            OR UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=NEXTVALUEFOR%\'))
399 104
                    AS fautoinc
400
                FROM
401
                    rdb$relation_fields rel
402 29
                    JOIN rdb$fields fld ON rel.rdb$field_source=fld.rdb$field_name
403 29
                WHERE
404
                    UPPER(rel.rdb$relation_name)=UPPER(\'' . $table->name . '\')
405 29
                ORDER BY
406 29
                    rel.rdb$field_position;';
407 29
        try {
408
            $columns = $this->db->createCommand($sql)->queryAll();
409
            if (empty($columns)) {
410 29
                return false;
411
            }
412 104
        } catch (Exception $e) {
413 104
            return false;
414
        }
415 5
        $sql = 'SELECT
416
                    idx.rdb$field_name AS fname
417
                FROM
418
                    rdb$relation_constraints rc
419
                    JOIN rdb$index_segments idx ON idx.rdb$index_name=rc.rdb$index_name
420
                WHERE rc.rdb$constraint_type=\'PRIMARY KEY\'
421
					AND UPPER(rc.rdb$relation_name)=UPPER(\'' . $table->name . '\')';
422 5
        try {
423
            $pkeys = $this->db->createCommand($sql)->queryColumn();
424 5
        } catch (Exception $e) {
425
            return false;
426
        }
427
        $pkeys = array_map('rtrim', $pkeys);
428
        $pkeys = array_map('strtolower', $pkeys);
429 5
        foreach ($columns as $key => $column) {
430 5
            $column = array_map('strtolower', $column);
431
            $columns[$key]['fprimary'] = in_array(rtrim($column['fname']), $pkeys);
432 5
        }
433
        foreach ($columns as $column) {
434
            $c = $this->loadColumnSchema($column);
435
            if ($table->sequenceName === null && $c->autoIncrement) {
436
                $matches = [];
437
438
                if (isset($column['fgenerator_name']) && $column['fgenerator_name']) {
439
                    $table->sequenceName = $column['fgenerator_name'];
440
                } elseif (preg_match("/NEW.{$c->name}\s*=\s*GEN_ID\((\w+)/i", $column['fautoinc'], $matches)) {
441
                    $table->sequenceName = $matches[1];
442
                } elseif (preg_match("/NEW.{$c->name}\s*=\s*NEXT\s+VALUE\s+FOR\s+(\w+)/i", $column['fautoinc'], $matches)) {
443
                    $table->sequenceName = $matches[1];
444
                }
445
            }
446
            $table->columns[$c->name] = $c;
447
            if ($c->isPrimaryKey) {
448
                $table->primaryKey[] = $c->name;
449
            }
450 1
        }
451
        return (count($table->columns) > 0);
452
    }
453
454
    /**
455
     * @return ColumnSchema
456
     * @throws \yii\base\InvalidConfigException
457
     */
458 1
    protected function createColumnSchema()
459 1
    {
460 1
        return \Yii::createObject('\edgardmessias\db\firebird\ColumnSchema');
461 1
    }
462 1
463 1
    /**
464
     * Creates a table column.
465 1
     *
466
     * @param array $column column metadata
467
     * @return ColumnSchema normalized column metadata
468
     */
469
    protected function loadColumnSchema($column)
470
    {
471
        $c = $this->createColumnSchema();
472
        $c->name = strtolower(rtrim($column['fname']));
473
        $c->allowNull = (int) $column['fnull'] !== 1;
474
        $c->isPrimaryKey = $column['fprimary'];
475
        $c->autoIncrement = (isset($column['fgenerator_name']) && $column['fgenerator_name']) || (boolean) $column['fautoinc'];
476 2
        $c->comment = $column['fcomment'] === null ? '' : $column['fcomment'];
477
478 2
        $c->type = self::TYPE_STRING;
479 2
480 1
        $defaultValue = null;
481 1
        if (!empty($column['fdefault'])) {
482 1
            // remove whitespace, 'DEFAULT ' prefix and surrounding single quotes; all optional
483 1
            if (preg_match("/\s*(DEFAULT\s+){0,1}('(.*)'|(.*))\s*/i", $column['fdefault'], $parts)) {
484
                $defaultValue = array_pop($parts);
485 1
            }
486
            // handle escaped single quotes like in "funny''quoted''string"
487 2
            $defaultValue = str_replace('\'\'', '\'', $defaultValue);
488
        }
489
        if ($defaultValue === null) {
490
            $defaultValue = $column['fdefault_value'];
491
        }
492 13
        $dbType = '';
493
        $baseTypes = [
494 13
            7   => 'SMALLINT',
495 13
            8   => 'INTEGER',
496 13
            16  => 'INT64',
497 13
            9   => 'QUAD',
498 13
            10  => 'FLOAT',
499 12
            11  => 'D_FLOAT',
500 12
            17  => 'BOOLEAN',
501 12
            27  => 'DOUBLE PRECISION',
502
            12  => 'DATE',
503 12
            13  => 'TIME',
504
            35  => 'TIMESTAMP',
505
            261 => 'BLOB',
506 13
            40  => 'CSTRING',
507 13
            45  => 'BLOB_ID',
508 13
        ];
509
        $baseCharTypes = [
510 13
            37 => 'VARCHAR',
511
            14 => 'CHAR',
512
        ];
513 13
        if (array_key_exists((int) $column['fcodtype'], $baseTypes)) {
514 12
            $dbType = $baseTypes[(int) $column['fcodtype']];
515 12
        } elseif (array_key_exists((int) $column['fcodtype'], $baseCharTypes)) {
516 10
            $c->size = (int) $column['fcharlength'];
517 12
            $c->precision = $c->size;
518
            $dbType = $baseCharTypes[(int) $column['fcodtype']] . "($c->size)";
519
        }
520
        switch ((int) $column['fcodtype']) {
521 13
            case 7:
522
            case 8:
523
                switch ((int) $column['fcodsubtype']) {
524
                    case 1:
525
                        $c->precision = (int) $column['fprecision'];
526
                        $c->size = $c->precision;
527
                        $c->scale = abs((int) $column['fscale']);
0 ignored issues
show
Documentation Bug introduced by
It seems like abs((int) $column['fscale']) can also be of type double. However, the property $scale is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
528 2
                        $dbType = "NUMERIC({$c->precision},{$c->scale})";
529
                        break;
530 2
                    case 2:
531
                        $c->precision = (int) $column['fprecision'];
532
                        $c->size = $c->precision;
533
                        $c->scale = abs((int) $column['fscale']);
534 2
                        $dbType = "DECIMAL({$c->precision},{$c->scale})";
535 2
                        break;
536
                }
537
                break;
538 1
            case 16:
539 1
                switch ((int) $column['fcodsubtype']) {
540
                    case 1:
541
                        $c->precision = (int) $column['fprecision'];
542
                        $c->size = $c->precision;
543
                        $c->scale = abs((int) $column['fscale']);
544
                        $dbType = "NUMERIC({$c->precision},{$c->scale})";
545
                        break;
546
                    case 2:
547
                        $c->precision = (int) $column['fprecision'];
548
                        $c->size = $c->precision;
549
                        $c->scale = abs((int) $column['fscale']);
550
                        $dbType = "DECIMAL({$c->precision},{$c->scale})";
551
                        break;
552
                    default:
553
                        $dbType = 'BIGINT';
554
                        break;
555
                }
556
                break;
557
            case 261:
558
                switch ((int) $column['fcodsubtype']) {
559
                    case 1:
560
                        $dbType = 'BLOB SUB_TYPE TEXT';
561
                        $c->size = null;
562
                        break;
563
                }
564
                break;
565
        }
566
567
        $c->dbType = strtolower($dbType);
568
569
        $c->type = self::TYPE_STRING;
570
        if (preg_match('/^([\w\ ]+)(?:\(([^\)]+)\))?/', $c->dbType, $matches)) {
571
            $type = strtolower($matches[1]);
572
            if (isset($this->typeMap[$type])) {
573
                $c->type = $this->typeMap[$type];
574
            }
575
        }
576
577
578
        $c->phpType = $this->getColumnPhpType($c);
579
580
        $c->defaultValue = null;
581
        if ($defaultValue !== null) {
582
            if (in_array($c->type, [self::TYPE_DATE, self::TYPE_DATETIME, self::TYPE_TIME, self::TYPE_TIMESTAMP])
583
                    && preg_match('/(CURRENT_|NOW|NULL|TODAY|TOMORROW|YESTERDAY)/i', $defaultValue)) {
584
                $c->defaultValue = new Expression(trim($defaultValue));
585
            } else {
586
                $c->defaultValue = $c->phpTypecast($defaultValue);
587
            }
588
        }
589
590
        return $c;
591
    }
592
593
    /**
594
     * Collects the foreign key column details for the given table.
595
     *
596
     * @param TableSchema $table the table metadata
597
     */
598
    protected function findConstraints($table)
599
    {
600
        // Zoggo - Converted sql to use join syntax
601
        $sql = 'SELECT
602
                    a.rdb$constraint_name as fconstraint,
603
                    c.rdb$relation_name AS ftable,
604
                    d.rdb$field_name AS pfield,
605
                    e.rdb$field_name AS ffield
606
                FROM
607
                    rdb$ref_constraints b
608
                    JOIN rdb$relation_constraints a ON a.rdb$constraint_name=b.rdb$constraint_name
609
                    JOIN rdb$relation_constraints c ON b.rdb$const_name_uq=c.rdb$constraint_name
610
                    JOIN rdb$index_segments d ON c.rdb$index_name=d.rdb$index_name
611
                    JOIN rdb$index_segments e ON a.rdb$index_name=e.rdb$index_name AND e.rdb$field_position = d.rdb$field_position
612
                WHERE
613
                    a.rdb$constraint_type=\'FOREIGN KEY\' AND
614
                    UPPER(a.rdb$relation_name)=UPPER(\'' . $table->name . '\') ';
615
        try {
616
            $fkeys = $this->db->createCommand($sql)->queryAll();
617
        } catch (Exception $e) {
618
            return false;
619
        }
620
621
        $constraints = [];
622
        foreach ($fkeys as $fkey) {
623
            // Zoggo - Added strtolower here to guarantee that values are
624
            // returned lower case. Otherwise gii generates wrong code.
625
            $fkey = array_map('rtrim', $fkey);
626
            $fkey = array_map('strtolower', $fkey);
627
628
            if (!isset($constraints[$fkey['fconstraint']])) {
629
                $constraints[$fkey['fconstraint']] = [
630
                    $fkey['ftable']
631
                ];
632
            }
633
            $constraints[$fkey['fconstraint']][$fkey['ffield']] = $fkey['pfield'];
634
        }
635
        $table->foreignKeys = $constraints;
636
    }
637
638
    protected function findTableNames($schema = '')
639
    {
640
        $sql = 'SELECT
641
                    rdb$relation_name
642
                FROM
643
                    rdb$relations
644
                WHERE
645
                    (rdb$system_flag is null OR rdb$system_flag=0)';
646
        try {
647
            $tables = $this->db->createCommand($sql)->queryColumn();
648
        } catch (Exception $e) {
649
            return false;
650
        }
651
652
        $tables = array_map('rtrim', $tables);
653
        $tables = array_map('strtolower', $tables);
654
655
        return $tables;
656
    }
657
658
    /**
659
     * Returns all unique indexes for the given table.
660
     * Each array element is of the following structure:
661
     *
662
     * ~~~
663
     * [
664
     *  'IndexName1' => ['col1' [, ...]],
665
     *  'IndexName2' => ['col2' [, ...]],
666
     * ]
667
     * ~~~
668
     *
669
     * @param TableSchema $table the table metadata
670
     * @return array all unique indexes for the given table.
671
     * @since 2.0.4
672
     */
673
    public function findUniqueIndexes($table)
674
    {
675
        $query = '
676
SELECT id.RDB$INDEX_NAME as index_name, ids.RDB$FIELD_NAME as column_name
677
FROM RDB$INDICES id
678
INNER JOIN RDB$INDEX_SEGMENTS ids ON ids.RDB$INDEX_NAME = id.RDB$INDEX_NAME
679
WHERE id.RDB$UNIQUE_FLAG = 1
680
AND   id.RDB$SYSTEM_FLAG = 0
681
AND UPPER(id.RDB$RELATION_NAME) = UPPER(\'' . $table->name . '\')
682
ORDER BY id.RDB$RELATION_NAME, id.RDB$INDEX_NAME, ids.RDB$FIELD_POSITION';
683
        $result = [];
684
        $command = $this->db->createCommand($query);
685
        foreach ($command->queryAll() as $row) {
686
            $result[strtolower(rtrim($row['index_name']))][] = strtolower(rtrim($row['column_name']));
687
        }
688
        return $result;
689
    }
690
691
    /**
692
     * Sets the isolation level of the current transaction.
693
     * @param string $level The transaction isolation level to use for this transaction.
694
     * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]]
695
     * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used
696
     * after `SET TRANSACTION ISOLATION LEVEL`.
697
     * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
698
     */
699
    public function setTransactionIsolationLevel($level)
700
    {
701
        if ($level == Transaction::READ_UNCOMMITTED) {
702
            parent::setTransactionIsolationLevel('READ COMMITTED RECORD_VERSION');
703
        } elseif ($level == Transaction::REPEATABLE_READ) {
704
            parent::setTransactionIsolationLevel('SNAPSHOT');
705
        } elseif ($level == Transaction::SERIALIZABLE) {
706
            parent::setTransactionIsolationLevel('SNAPSHOT TABLE STABILITY');
707
        } else {
708
            parent::setTransactionIsolationLevel($level);
709
        }
710
    }
711
712
    /**
713
     * @inheritdoc
714
     */
715
    public function insert($table, $columns)
716
    {
717
        $this->_lastInsertID = false;
718
        $params = [];
719
        $sql = "";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
720
        $returnColumns = $this->getTableSchema($table)->primaryKey;
721
        if (!empty($returnColumns)) {
722
            if (!$this->db->supportReturningInsert) {
723
                $returs = [];
724
                $returning = [];
725
                $columnSchemas = $this->getTableSchema($table)->columns;
726
                foreach ((array) $returnColumns as $name) {
727
                    $returs[] = $this->quoteColumnName($name) . ' ' . $columnSchemas[$name]->dbType;
728
                    $returning[] = $this->quoteColumnName($name);
729
                }
730
731
                $sql = $this->db->getQueryBuilder()->rawInsert($table, array_keys($columns), array_values($columns), $params);
0 ignored issues
show
Bug introduced by
The method rawInsert() does not exist on yii\db\QueryBuilder. Did you maybe mean insert()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
732
733
                $sql = "EXECUTE block RETURNS (" 
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal EXECUTE block RETURNS ( does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
734
                        . implode(', ',$returs) 
735
                        . ") AS BEGIN\n" 
736
                        . $sql . ' RETURNING ' . implode(', ', $returning)
737
                        . ' INTO ' . implode(', ', $returning)
738
                        . ";\nSUSPEND;\nEND;";
739
            } else {
740
                $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
741
                $returning = [];
742
                foreach ((array) $returnColumns as $name) {
743
                    $returning[] = $this->quoteColumnName($name);
744
                }
745
                $sql .= ' RETURNING ' . implode(', ', $returning);
746
            }
747
        }
748
749
        $command = $this->db->createCommand($sql, $params);
750
        $command->prepare(false);
751
        $result = $command->queryOne();
752
753
        if (!$command->pdoStatement->rowCount()) {
754
            return false;
755
        } else {
756
            if (!empty($returnColumns)) {
757
                foreach ((array) $returnColumns as $name) {
758
                    if ($this->getTableSchema($table)->getColumn($name)->autoIncrement) {
759
                        $this->_lastInsertID = $result[$name];
760
                        break;
761
                    }
762
                }
763
            }
764
            return $result;
765
        }
766
    }
767
768
    /**
769
     * @inheritdoc
770
     */
771
    public function getLastInsertID($sequenceName = '')
772
    {
773
        if (!$this->db->isActive) {
774
            throw new InvalidCallException('DB Connection is not active.');
775
        }
776
777
        if ($sequenceName !== '') {
778
            return $this->db->createCommand('SELECT GEN_ID(' . $this->db->quoteTableName($sequenceName) . ', 0 ) FROM RDB$DATABASE;')->queryScalar();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->db->createCommand...BASE;')->queryScalar(); of type string|null|false adds false to the return on line 778 which is incompatible with the return type of the parent method yii\db\Schema::getLastInsertID of type string. It seems like you forgot to handle an error condition.
Loading history...
779
        }
780
781
        if ($this->_lastInsertID !== false) {
782
            return $this->_lastInsertID;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->_lastInsertID; (false) is incompatible with the return type of the parent method yii\db\Schema::getLastInsertID of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
783
        }
784
        return null;
785
    }
786
787
    protected function loadTablePrimaryKey($tableName)
788
    {
789
        static $sql = <<<'SQL'
790
SELECT RC.RDB$CONSTRAINT_NAME AS NAME, IDX.RDB$FIELD_NAME AS COLUMN_NAME
791
FROM RDB$RELATION_CONSTRAINTS RC
792
  JOIN RDB$INDEX_SEGMENTS IDX
793
    ON IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
794
WHERE RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
795
AND   UPPER(RC.RDB$RELATION_NAME) = UPPER(:tableName)
796
ORDER BY IDX.RDB$FIELD_POSITION
797
SQL;
798
799
        $resolvedName = $this->resolveTableName($tableName);
800
        $constraints = $this->db->createCommand($sql, [
801
            ':tableName' => $resolvedName->name,
802
        ])->queryAll();
803
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
804
        $constraints = ArrayHelper::index($constraints, null, ['name']);
805
806
        foreach ($constraints as $name => $constraint) {
807
            $columns = ArrayHelper::getColumn($constraint, 'column_name');
808
            $columns = array_map('trim', $columns);
809
            $columns = array_map('strtolower', $columns);
810
            return new Constraint([
811
                'name' => strtolower(trim($name)),
812
                'columnNames' => $columns,
813
            ]);
814
        }
815
816
        return null;
817
    }
818
819
    protected function loadTableUniques($tableName)
820
    {
821
        static $sql = <<<'SQL'
822
SELECT RC.RDB$CONSTRAINT_NAME AS NAME, IDX.RDB$FIELD_NAME AS COLUMN_NAME
823
FROM RDB$RELATION_CONSTRAINTS RC
824
  JOIN RDB$INDEX_SEGMENTS IDX
825
    ON IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
826
WHERE RC.RDB$CONSTRAINT_TYPE = 'UNIQUE'
827
AND   UPPER(RC.RDB$RELATION_NAME) = UPPER(:tableName)
828
ORDER BY IDX.RDB$FIELD_POSITION
829
SQL;
830
831
        $resolvedName = $this->resolveTableName($tableName);
832
        $constraints = $this->db->createCommand($sql, [
833
            ':tableName' => $resolvedName->name,
834
        ])->queryAll();
835
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
836
        $constraints = ArrayHelper::index($constraints, null, ['name']);
837
838
        $result = [];
839
        foreach ($constraints as $name => $rows) {
840
            $columns = ArrayHelper::getColumn($rows, 'column_name');
841
            $columns = array_map('trim', $columns);
842
            $columns = array_map('strtolower', $columns);
843
            $result[] = new Constraint([
844
                'name' => strtolower(trim($name)),
845
                'columnNames' => $columns,
846
            ]);
847
        }
848
849
        return $result;
850
    }
851
852
    protected function loadTableChecks($tableName)
853
    {
854
        // DISTINCT not work on blob, need cast to varchar
855
        // 8191 Is max for UTF-8
856
        static $sql = <<<'SQL'
857
SELECT DISTINCT RC.RDB$CONSTRAINT_NAME AS NAME,
858
       DEP.RDB$FIELD_NAME AS COLUMN_NAME,
859
       CAST(TRIG.RDB$TRIGGER_SOURCE AS VARCHAR(8191)) AS CHECK_EXPR
860
       FROM RDB$RELATION_CONSTRAINTS RC
861
  JOIN RDB$CHECK_CONSTRAINTS CH_CONST
862
    ON CH_CONST.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME
863
  JOIN RDB$TRIGGERS TRIG
864
    ON TRIG.RDB$TRIGGER_NAME = CH_CONST.RDB$TRIGGER_NAME
865
  JOIN RDB$DEPENDENCIES DEP
866
    ON DEP.RDB$DEPENDENT_NAME = TRIG.RDB$TRIGGER_NAME
867
   AND DEP.RDB$DEPENDED_ON_NAME = TRIG.RDB$RELATION_NAME
868
WHERE RC.RDB$CONSTRAINT_TYPE = 'CHECK'
869
AND   UPPER(RC.RDB$RELATION_NAME) = UPPER(:tableName)
870
SQL;
871
872
        $resolvedName = $this->resolveTableName($tableName);
873
        $constraints = $this->db->createCommand($sql, [
874
            ':tableName' => $resolvedName->name,
875
        ])->queryAll();
876
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
877
        $constraints = ArrayHelper::index($constraints, null, ['name']);
878
879
        $result = [];
880
        foreach ($constraints as $name => $constraint) {
881
            $columns = ArrayHelper::getColumn($constraint, 'column_name');
882
            $columns = array_map('trim', $columns);
883
            $columns = array_map('strtolower', $columns);
884
885
            $check_expr = $constraint[0]['check_expr'];
886
            $check_expr = preg_replace('/^\s*CHECK\s*/i', '', $check_expr); // remove "CHECK " at begin
887
            $check_expr = preg_replace('/^\((.*)\)$/i', '\1', $check_expr); // remove bracket () at begin and end
888
889
            $result[] = new CheckConstraint([
890
                'name' => strtolower(trim($name)),
891
                'columnNames' => $columns,
892
                'expression' => $check_expr,
893
            ]);
894
        }
895
896
        return $result;
897
    }
898
899
    protected function loadTableIndexes($tableName)
900
    {
901
        static $sql = <<<'SQL'
902
SELECT IDX.RDB$INDEX_NAME AS NAME,
903
       SEG.RDB$FIELD_NAME AS COLUMN_NAME,
904
       IDX.RDB$UNIQUE_FLAG AS INDEX_IS_UNIQUE,
905
       CASE WHEN CONST.RDB$CONSTRAINT_NAME IS NOT NULL THEN 1 ELSE 0 END AS INDEX_IS_PRIMARY
906
       FROM RDB$INDICES IDX
907
  JOIN RDB$INDEX_SEGMENTS SEG
908
  LEFT JOIN RDB$RELATION_CONSTRAINTS CONST ON CONST.RDB$INDEX_NAME = SEG.RDB$INDEX_NAME AND CONST.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
909
    ON SEG.RDB$INDEX_NAME = IDX.RDB$INDEX_NAME
910
WHERE UPPER(IDX.RDB$RELATION_NAME) = UPPER(:tableName)
911
ORDER BY SEG.RDB$FIELD_POSITION
912
SQL;
913
914
        $resolvedName = $this->resolveTableName($tableName);
915
        $indexes = $this->db->createCommand($sql, [
916
            ':tableName' => $resolvedName->name,
917
        ])->queryAll();
918
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
919
        $indexes = ArrayHelper::index($indexes, null, 'name');
920
        $result = [];
921
        foreach ($indexes as $name => $index) {
922
            $columns = ArrayHelper::getColumn($index, 'column_name');
923
            $columns = array_map('trim', $columns);
924
            $columns = array_map('strtolower', $columns);
925
926
            $result[] = new IndexConstraint([
927
                'isPrimary' => (bool) $index[0]['index_is_primary'],
928
                'isUnique' => (bool) $index[0]['index_is_unique'],
929
                'name' => strtolower(trim($name)),
930
                'columnNames' => $columns,
931
            ]);
932
        }
933
934
        return $result;
935
    }
936
937
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
938
    {
939
        throw new \yii\base\NotSupportedException('FirebirdSQL does not support default value constraints.');
940
    }
941
942
    protected function loadTableForeignKeys($tableName)
943
    {
944
        static $sql = <<<'SQL'
945
SELECT A.RDB$CONSTRAINT_NAME AS NAME,
946
       E.RDB$FIELD_NAME AS COLUMN_NAME,
947
       C.RDB$RELATION_NAME AS FOREIGN_TABLE_NAME,
948
       D.RDB$FIELD_NAME AS FOREIGN_COLUMN_NAME,
949
       B.RDB$UPDATE_RULE AS ON_UPDATE,
950
       B.RDB$DELETE_RULE AS ON_DELETE
951
FROM RDB$REF_CONSTRAINTS B
952
  JOIN RDB$RELATION_CONSTRAINTS A
953
    ON A.RDB$CONSTRAINT_NAME = B.RDB$CONSTRAINT_NAME
954
  JOIN RDB$RELATION_CONSTRAINTS C
955
    ON B.RDB$CONST_NAME_UQ = C.RDB$CONSTRAINT_NAME
956
  JOIN RDB$INDEX_SEGMENTS D
957
    ON C.RDB$INDEX_NAME = D.RDB$INDEX_NAME
958
  JOIN RDB$INDEX_SEGMENTS E
959
    ON A.RDB$INDEX_NAME = E.RDB$INDEX_NAME
960
   AND E.RDB$FIELD_POSITION = D.RDB$FIELD_POSITION
961
WHERE A.RDB$CONSTRAINT_TYPE = 'FOREIGN KEY'
962
AND   UPPER(A.RDB$RELATION_NAME) = UPPER(:tableName)
963
ORDER BY E.RDB$FIELD_POSITION
964
SQL;
965
966
        $resolvedName = $this->resolveTableName($tableName);
967
        $constraints = $this->db->createCommand($sql, [
968
            ':tableName' => $resolvedName->name,
969
        ])->queryAll();
970
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
971
        $constraints = ArrayHelper::index($constraints, null, ['name']);
972
973
        $result = [];
974
        foreach ($constraints as $name => $constraint) {
975
            $columnNames = ArrayHelper::getColumn($constraint, 'column_name');
976
            $columnNames = array_map('trim', $columnNames);
977
            $columnNames = array_map('strtolower', $columnNames);
978
            
979
            $foreignColumnNames = ArrayHelper::getColumn($constraint, 'foreign_column_name');
980
            $foreignColumnNames = array_map('trim', $foreignColumnNames);
981
            $foreignColumnNames = array_map('strtolower', $foreignColumnNames);
982
            
983
            $result[] = new ForeignKeyConstraint([
984
                'name' => strtolower(trim($name)),
985
                'columnNames' => $columnNames,
986
                'foreignTableName' => strtolower(trim($constraint[0]['foreign_table_name'])),
987
                'foreignColumnNames' => $foreignColumnNames,
988
                'onDelete' => trim($constraint[0]['on_delete']),
989
                'onUpdate' => trim($constraint[0]['on_update']),
990
            ]);
991
        }
992
993
        return $result;
994
    }
995
996
    protected function findViewNames($schema = '')
0 ignored issues
show
Unused Code introduced by
The parameter $schema is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
997
    {
998
        $sql = <<<'SQL'
999
SELECT RDB$RELATION_NAME
1000
FROM RDB$RELATIONS
1001
WHERE RDB$VIEW_BLR IS NOT NULL
1002
AND   (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0)
1003
SQL;
1004
1005
        $views = $this->db->createCommand($sql)->queryColumn();
1006
        $views = array_map('trim', $views);
1007
        $views = array_map('strtolower', $views);
1008
1009
        return $views;
1010
    }
1011
}
1012