Failed Conditions
Push — master ( ac0e13...24dbc4 )
by Sergei
22s queued 15s
created

OraclePlatform::getColumnDeclarationSQL()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 13
nop 2
dl 0
loc 24
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\DBALException;
8
use Doctrine\DBAL\Exception\ColumnLengthRequired;
9
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
10
use Doctrine\DBAL\Schema\Identifier;
11
use Doctrine\DBAL\Schema\Index;
12
use Doctrine\DBAL\Schema\Sequence;
13
use Doctrine\DBAL\Schema\Table;
14
use Doctrine\DBAL\Schema\TableDiff;
15
use Doctrine\DBAL\TransactionIsolationLevel;
16
use Doctrine\DBAL\Types\BinaryType;
17
use InvalidArgumentException;
18
use function array_merge;
19
use function count;
20
use function explode;
21
use function implode;
22
use function preg_match;
23
use function sprintf;
24
use function strlen;
25
use function strpos;
26
use function strtoupper;
27
use function substr;
28
29
/**
30
 * OraclePlatform.
31
 */
32
class OraclePlatform extends AbstractPlatform
33
{
34
    /**
35
     * Assertion for Oracle identifiers.
36
     *
37
     * @link http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements008.htm
38
     *
39
     * @throws DBALException
40
     */
41
    public static function assertValidIdentifier(string $identifier) : void
42
    {
43
        if (preg_match('(^(([a-zA-Z]{1}[a-zA-Z0-9_$#]{0,})|("[^"]+"))$)', $identifier) === 0) {
44
            throw new DBALException('Invalid Oracle identifier.');
45
        }
46
    }
47
48
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
49
    {
50
        if ($length === null) {
51
            return sprintf('SUBSTR(%s, %s)', $string, $start);
52
        }
53
54
        return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length);
55
    }
56
57
    public function getNowExpression(string $type = 'timestamp') : string
58
    {
59
        switch ($type) {
60
            case 'date':
61
            case 'time':
62
            case 'timestamp':
63
            default:
64
                return 'TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')';
65
        }
66
    }
67
68
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
69
    {
70
        if ($start === null) {
71
            return sprintf('INSTR(%s, %s)', $string, $substring);
72
        }
73
74
        return sprintf('INSTR(%s, %s, %s)', $string, $substring, $start);
75
    }
76
77
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
78
    {
79
        switch ($unit) {
80
            case DateIntervalUnit::MONTH:
81
            case DateIntervalUnit::QUARTER:
82
            case DateIntervalUnit::YEAR:
83
                switch ($unit) {
84
                    case DateIntervalUnit::QUARTER:
85
                        $interval = $this->multiplyInterval($interval, 3);
86
                        break;
87
88
                    case DateIntervalUnit::YEAR:
89
                        $interval = $this->multiplyInterval($interval, 12);
90
                        break;
91
                }
92
93
                return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')';
94
95
            default:
96
                $calculationClause = '';
97
98
                switch ($unit) {
99
                    case DateIntervalUnit::SECOND:
100
                        $calculationClause = '/24/60/60';
101
                        break;
102
103
                    case DateIntervalUnit::MINUTE:
104
                        $calculationClause = '/24/60';
105
                        break;
106
107
                    case DateIntervalUnit::HOUR:
108
                        $calculationClause = '/24';
109
                        break;
110
111
                    case DateIntervalUnit::WEEK:
112
                        $calculationClause = '*7';
113
                        break;
114
                }
115
116
                return '(' . $date . $operator . $interval . $calculationClause . ')';
117
        }
118
    }
119
120
    public function getDateDiffExpression(string $date1, string $date2) : string
121
    {
122
        return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2);
123
    }
124
125
    public function getBitAndComparisonExpression(string $value1, string $value2) : string
126
    {
127
        return 'BITAND(' . $value1 . ', ' . $value2 . ')';
128
    }
129
130
    public function getCurrentDatabaseExpression() : string
131
    {
132
        return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')";
133
    }
134
135
    public function getBitOrComparisonExpression(string $value1, string $value2) : string
136
    {
137
        return '(' . $value1 . '-' .
138
                $this->getBitAndComparisonExpression($value1, $value2)
139
                . '+' . $value2 . ')';
140
    }
141
142
    /**
143
     * {@inheritDoc}
144
     *
145
     * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH.
146
     * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection
147
     * in {@see listSequences()}
148
     */
149
    public function getCreateSequenceSQL(Sequence $sequence) : string
150
    {
151
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
152
               ' START WITH ' . $sequence->getInitialValue() .
153
               ' MINVALUE ' . $sequence->getInitialValue() .
154
               ' INCREMENT BY ' . $sequence->getAllocationSize() .
155
               $this->getSequenceCacheSQL($sequence);
156
    }
157
158
    public function getAlterSequenceSQL(Sequence $sequence) : string
159
    {
160
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
161
               ' INCREMENT BY ' . $sequence->getAllocationSize()
162
               . $this->getSequenceCacheSQL($sequence);
163
    }
164
165
    /**
166
     * Cache definition for sequences
167
     */
168
    private function getSequenceCacheSQL(Sequence $sequence) : string
169
    {
170
        if ($sequence->getCache() === 0) {
171
            return ' NOCACHE';
172
        }
173
174
        if ($sequence->getCache() === 1) {
175
            return ' NOCACHE';
176
        }
177
178
        if ($sequence->getCache() > 1) {
179
            return ' CACHE ' . $sequence->getCache();
180
        }
181
182
        return '';
183
    }
184
185
    public function getSequenceNextValSQL(string $sequenceName) : string
186
    {
187
        return 'SELECT ' . $sequenceName . '.nextval FROM DUAL';
188
    }
189
190
    public function getSetTransactionIsolationSQL(int $level) : string
191
    {
192
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
193
    }
194
195
    protected function _getTransactionIsolationLevelSQL(int $level) : string
196
    {
197
        switch ($level) {
198
            case TransactionIsolationLevel::READ_UNCOMMITTED:
199
                return 'READ UNCOMMITTED';
200
201
            case TransactionIsolationLevel::READ_COMMITTED:
202
                return 'READ COMMITTED';
203
204
            case TransactionIsolationLevel::REPEATABLE_READ:
205
            case TransactionIsolationLevel::SERIALIZABLE:
206
                return 'SERIALIZABLE';
207
208
            default:
209
                return parent::_getTransactionIsolationLevelSQL($level);
210
        }
211
    }
212
213
    /**
214
     * {@inheritDoc}
215
     */
216
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
217
    {
218
        return 'NUMBER(1)';
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
225
    {
226
        return 'NUMBER(10)';
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
233
    {
234
        return 'NUMBER(20)';
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
241
    {
242
        return 'NUMBER(5)';
243
    }
244
245
    /**
246
     * {@inheritDoc}
247
     */
248
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
249
    {
250
        return 'TIMESTAMP(0)';
251
    }
252
253
    /**
254
     * {@inheritDoc}
255
     */
256
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
257
    {
258
        return 'TIMESTAMP(0) WITH TIME ZONE';
259
    }
260
261
    /**
262
     * {@inheritDoc}
263
     */
264
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
265
    {
266
        return 'DATE';
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
273
    {
274
        return 'DATE';
275
    }
276
277
    /**
278
     * {@inheritDoc}
279
     */
280
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
281
    {
282
        return '';
283
    }
284
285
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
286
    {
287
        if ($length === null) {
288
            throw ColumnLengthRequired::new($this, 'VARCHAR2');
289
        }
290
291
        return sprintf('VARCHAR2(%d)', $length);
292
    }
293
294
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length) : string
295
    {
296
        if ($length === null) {
297
            throw ColumnLengthRequired::new($this, 'RAW');
298
        }
299
300
        return sprintf('RAW(%d)', $length);
301
    }
302
303
    protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length) : string
304
    {
305
        return $this->getBinaryTypeDeclarationSQLSnippet($length);
306
    }
307
308
    /**
309
     * {@inheritDoc}
310
     */
311
    public function getClobTypeDeclarationSQL(array $field) : string
312
    {
313
        return 'CLOB';
314
    }
315
316
    public function getListDatabasesSQL() : string
317
    {
318
        return 'SELECT username FROM all_users';
319
    }
320
321
    public function getListSequencesSQL(string $database) : string
322
    {
323
        return 'SELECT SEQUENCE_NAME, MIN_VALUE, INCREMENT_BY FROM SYS.ALL_SEQUENCES WHERE SEQUENCE_OWNER = '
324
            . $this->quoteStringLiteral(
325
                $this->normalizeIdentifier($database)->getName()
326
            );
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
333
    {
334
        $indexes            = $options['indexes'] ?? [];
335
        $options['indexes'] = [];
336
        $sql                = parent::_getCreateTableSQL($tableName, $columns, $options);
337
338
        foreach ($columns as $column) {
339
            if (isset($column['sequence'])) {
340
                $sql[] = $this->getCreateSequenceSQL($column['sequence']);
341
            }
342
343
            if (empty($column['autoincrement'])) {
344
                continue;
345
            }
346
347
            $sql = array_merge($sql, $this->getCreateAutoincrementSql($column['name'], $tableName));
348
        }
349
350
        if (isset($indexes) && ! empty($indexes)) {
351
            foreach ($indexes as $index) {
352
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
353
            }
354
        }
355
356
        return $sql;
357
    }
358
359
    /**
360
     * {@inheritDoc}
361
     *
362
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaOracleReader.html
363
     */
364
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
365
    {
366
        $table = $this->normalizeIdentifier($table);
367
        $table = $this->quoteStringLiteral($table->getName());
368
369
        return "SELECT uind_col.index_name AS name,
370
                       (
371
                           SELECT uind.index_type
372
                           FROM   user_indexes uind
373
                           WHERE  uind.index_name = uind_col.index_name
374
                       ) AS type,
375
                       decode(
376
                           (
377
                               SELECT uind.uniqueness
378
                               FROM   user_indexes uind
379
                               WHERE  uind.index_name = uind_col.index_name
380
                           ),
381
                           'NONUNIQUE',
382
                           0,
383
                           'UNIQUE',
384
                           1
385
                       ) AS is_unique,
386
                       uind_col.column_name AS column_name,
387
                       uind_col.column_position AS column_pos,
388
                       (
389
                           SELECT ucon.constraint_type
390
                           FROM   user_constraints ucon
391
                           WHERE  ucon.index_name = uind_col.index_name
392
                       ) AS is_primary
393
             FROM      user_ind_columns uind_col
394
             WHERE     uind_col.table_name = " . $table . '
395
             ORDER BY  uind_col.column_position ASC';
396
    }
397
398
    public function getListTablesSQL() : string
399
    {
400
        return 'SELECT * FROM sys.user_tables';
401
    }
402
403
    public function getListViewsSQL(string $database) : string
404
    {
405
        return 'SELECT view_name, text FROM sys.user_views';
406
    }
407
408
    public function getCreateViewSQL(string $name, string $sql) : string
409
    {
410
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
411
    }
412
413
    public function getDropViewSQL(string $name) : string
414
    {
415
        return 'DROP VIEW ' . $name;
416
    }
417
418
    /**
419
     * @return array<int, string>
420
     */
421
    public function getCreateAutoincrementSql(string $name, string $table, int $start = 1) : array
422
    {
423
        $tableIdentifier   = $this->normalizeIdentifier($table);
424
        $quotedTableName   = $tableIdentifier->getQuotedName($this);
425
        $unquotedTableName = $tableIdentifier->getName();
426
427
        $nameIdentifier = $this->normalizeIdentifier($name);
428
        $quotedName     = $nameIdentifier->getQuotedName($this);
429
        $unquotedName   = $nameIdentifier->getName();
430
431
        $sql = [];
432
433
        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier);
434
435
        $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true);
436
437
        $sql[] = 'DECLARE
438
  constraints_Count NUMBER;
439
BEGIN
440
  SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count FROM USER_CONSTRAINTS WHERE TABLE_NAME = \'' . $unquotedTableName . '\' AND CONSTRAINT_TYPE = \'P\';
441
  IF constraints_Count = 0 OR constraints_Count = \'\' THEN
442
    EXECUTE IMMEDIATE \'' . $this->getCreateConstraintSQL($idx, $quotedTableName) . '\';
443
  END IF;
444
END;';
445
446
        $sequenceName = $this->getIdentitySequenceName(
447
            $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName,
448
            $nameIdentifier->isQuoted() ? $quotedName : $unquotedName
449
        );
450
        $sequence     = new Sequence($sequenceName, $start);
451
        $sql[]        = $this->getCreateSequenceSQL($sequence);
452
453
        $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . '
454
   BEFORE INSERT
455
   ON ' . $quotedTableName . '
456
   FOR EACH ROW
457
DECLARE
458
   last_Sequence NUMBER;
459
   last_InsertID NUMBER;
460
BEGIN
461
   SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
462
   IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN
463
      SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
464
   ELSE
465
      SELECT NVL(Last_Number, 0) INTO last_Sequence
466
        FROM User_Sequences
467
       WHERE Sequence_Name = \'' . $sequence->getName() . '\';
468
      SELECT :NEW.' . $quotedName . ' INTO last_InsertID FROM DUAL;
469
      WHILE (last_InsertID > last_Sequence) LOOP
470
         SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL;
471
      END LOOP;
472
   END IF;
473
END;';
474
475
        return $sql;
476
    }
477
478
    /**
479
     * Returns the SQL statements to drop the autoincrement for the given table name.
480
     *
481
     * @param string $table The table name to drop the autoincrement for.
482
     *
483
     * @return string[]
484
     */
485
    public function getDropAutoincrementSql(string $table) : array
486
    {
487
        $table                       = $this->normalizeIdentifier($table);
488
        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table);
489
        $identitySequenceName        = $this->getIdentitySequenceName(
490
            $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(),
491
            ''
492
        );
493
494
        return [
495
            'DROP TRIGGER ' . $autoincrementIdentifierName,
496
            $this->getDropSequenceSQL($identitySequenceName),
497
            $this->getDropConstraintSQL($autoincrementIdentifierName, $table->getQuotedName($this)),
498
        ];
499
    }
500
501
    /**
502
     * Normalizes the given identifier.
503
     *
504
     * Uppercases the given identifier if it is not quoted by intention
505
     * to reflect Oracle's internal auto uppercasing strategy of unquoted identifiers.
506
     *
507
     * @param string $name The identifier to normalize.
508
     *
509
     * @return Identifier The normalized identifier.
510
     */
511
    private function normalizeIdentifier(string $name) : Identifier
512
    {
513
        $identifier = new Identifier($name);
514
515
        return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name));
516
    }
517
518
    /**
519
     * Returns the autoincrement primary key identifier name for the given table identifier.
520
     *
521
     * Quotes the autoincrement primary key identifier name
522
     * if the given table name is quoted by intention.
523
     *
524
     * @param Identifier $table The table identifier to return the autoincrement primary key identifier name for.
525
     */
526
    private function getAutoincrementIdentifierName(Identifier $table) : string
527
    {
528
        $identifierName = $table->getName() . '_AI_PK';
529
530
        return $table->isQuoted()
531
            ? $this->quoteSingleIdentifier($identifierName)
532
            : $identifierName;
533
    }
534
535
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
536
    {
537
        $table = $this->normalizeIdentifier($table);
538
        $table = $this->quoteStringLiteral($table->getName());
539
540
        return "SELECT alc.constraint_name,
541
          alc.DELETE_RULE,
542
          cols.column_name \"local_column\",
543
          cols.position,
544
          (
545
              SELECT r_cols.table_name
546
              FROM   user_cons_columns r_cols
547
              WHERE  alc.r_constraint_name = r_cols.constraint_name
548
              AND    r_cols.position = cols.position
549
          ) AS \"references_table\",
550
          (
551
              SELECT r_cols.column_name
552
              FROM   user_cons_columns r_cols
553
              WHERE  alc.r_constraint_name = r_cols.constraint_name
554
              AND    r_cols.position = cols.position
555
          ) AS \"foreign_column\"
556
     FROM user_cons_columns cols
557
     JOIN user_constraints alc
558
       ON alc.constraint_name = cols.constraint_name
559
      AND alc.constraint_type = 'R'
560
      AND alc.table_name = " . $table . '
561
    ORDER BY cols.constraint_name ASC, cols.position ASC';
562
    }
563
564
    public function getListTableConstraintsSQL(string $table) : string
565
    {
566
        $table = $this->normalizeIdentifier($table);
567
        $table = $this->quoteStringLiteral($table->getName());
568
569
        return 'SELECT * FROM user_constraints WHERE table_name = ' . $table;
570
    }
571
572
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
573
    {
574
        $table = $this->normalizeIdentifier($table);
575
        $table = $this->quoteStringLiteral($table->getName());
576
577
        $tabColumnsTableName       = 'user_tab_columns';
578
        $colCommentsTableName      = 'user_col_comments';
579
        $tabColumnsOwnerCondition  = '';
580
        $colCommentsOwnerCondition = '';
581
582
        if ($database !== null && $database !== '/') {
583
            $database                  = $this->normalizeIdentifier($database);
584
            $database                  = $this->quoteStringLiteral($database->getName());
585
            $tabColumnsTableName       = 'all_tab_columns';
586
            $colCommentsTableName      = 'all_col_comments';
587
            $tabColumnsOwnerCondition  = ' AND c.owner = ' . $database;
588
            $colCommentsOwnerCondition = ' AND d.OWNER = c.OWNER';
589
        }
590
591
        return sprintf(
592
            <<<'SQL'
593
SELECT   c.*,
594
         (
595
             SELECT d.comments
596
             FROM   %s d
597
             WHERE  d.TABLE_NAME = c.TABLE_NAME%s
598
             AND    d.COLUMN_NAME = c.COLUMN_NAME
599
         ) AS comments
600
FROM     %s c
601
WHERE    c.table_name = %s%s
602
ORDER BY c.column_id
603
SQL
604
            ,
605
            $colCommentsTableName,
606
            $colCommentsOwnerCondition,
607
            $tabColumnsTableName,
608
            $table,
609
            $tabColumnsOwnerCondition
610
        );
611
    }
612
613
    /**
614
     * {@inheritDoc}
615
     */
616
    public function getDropSequenceSQL($sequence) : string
617
    {
618
        if ($sequence instanceof Sequence) {
619
            $sequence = $sequence->getQuotedName($this);
620
        }
621
622
        return 'DROP SEQUENCE ' . $sequence;
623
    }
624
625
    /**
626
     * {@inheritDoc}
627
     */
628
    public function getDropForeignKeySQL($foreignKey, $table) : string
629
    {
630
        if (! $foreignKey instanceof ForeignKeyConstraint) {
631
            $foreignKey = new Identifier($foreignKey);
632
        }
633
634
        if (! $table instanceof Table) {
635
            $table = new Identifier($table);
636
        }
637
638
        $foreignKey = $foreignKey->getQuotedName($this);
639
        $table      = $table->getQuotedName($this);
640
641
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
642
    }
643
644
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
645
    {
646
        $referentialAction = '';
647
648
        if ($foreignKey->hasOption('onDelete')) {
649
            $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete'));
650
        }
651
652
        if ($referentialAction !== '') {
653
            return ' ON DELETE ' . $referentialAction;
654
        }
655
656
        return '';
657
    }
658
659
    public function getForeignKeyReferentialActionSQL(string $action) : string
660
    {
661
        $action = strtoupper($action);
662
663
        switch ($action) {
664
            case 'RESTRICT': // RESTRICT is not supported, therefore falling back to NO ACTION.
665
            case 'NO ACTION':
666
                // NO ACTION cannot be declared explicitly,
667
                // therefore returning empty string to indicate to OMIT the referential clause.
668
                return '';
669
670
            case 'CASCADE':
671
            case 'SET NULL':
672
                return $action;
673
674
            default:
675
                // SET DEFAULT is not supported, throw exception instead.
676
                throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $action));
677
        }
678
    }
679
680
    public function getDropDatabaseSQL(string $database) : string
681
    {
682
        return 'DROP USER ' . $database . ' CASCADE';
683
    }
684
685
    /**
686
     * {@inheritDoc}
687
     */
688
    public function getAlterTableSQL(TableDiff $diff) : array
689
    {
690
        $sql         = [];
691
        $commentsSQL = [];
692
        $columnSql   = [];
693
694
        $fields = [];
695
696
        foreach ($diff->addedColumns as $column) {
697
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
698
                continue;
699
            }
700
701
            $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
702
            $comment  = $this->getColumnComment($column);
703
704
            if ($comment === null || $comment === '') {
705
                continue;
706
            }
707
708
            $commentsSQL[] = $this->getCommentOnColumnSQL(
709
                $diff->getName($this)->getQuotedName($this),
710
                $column->getQuotedName($this),
711
                $comment
712
            );
713
        }
714
715
        if (count($fields) > 0) {
716
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ADD (' . implode(', ', $fields) . ')';
717
        }
718
719
        $fields = [];
720
        foreach ($diff->changedColumns as $columnDiff) {
721
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
722
                continue;
723
            }
724
725
            $column = $columnDiff->column;
726
727
            // Do not generate column alteration clause if type is binary and only fixed property has changed.
728
            // Oracle only supports binary type columns with variable length.
729
            // Avoids unnecessary table alteration statements.
730
            if ($column->getType() instanceof BinaryType &&
731
                $columnDiff->hasChanged('fixed') &&
732
                count($columnDiff->changedProperties) === 1
733
            ) {
734
                continue;
735
            }
736
737
            $columnHasChangedComment = $columnDiff->hasChanged('comment');
738
739
            /**
740
             * Do not add query part if only comment has changed
741
             */
742
            if (! ($columnHasChangedComment && count($columnDiff->changedProperties) === 1)) {
743
                $columnInfo = $column->toArray();
744
745
                if (! $columnDiff->hasChanged('notnull')) {
746
                    unset($columnInfo['notnull']);
747
                }
748
749
                $fields[] = $column->getQuotedName($this) . $this->getColumnDeclarationSQL('', $columnInfo);
750
            }
751
752
            if (! $columnHasChangedComment) {
753
                continue;
754
            }
755
756
            $commentsSQL[] = $this->getCommentOnColumnSQL(
757
                $diff->getName($this)->getQuotedName($this),
758
                $column->getQuotedName($this),
759
                $this->getColumnComment($column)
760
            );
761
        }
762
763
        if (count($fields) > 0) {
764
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' MODIFY (' . implode(', ', $fields) . ')';
765
        }
766
767
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
768
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
769
                continue;
770
            }
771
772
            $oldColumnName = new Identifier($oldColumnName);
773
774
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
775
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
776
        }
777
778
        $fields = [];
779
        foreach ($diff->removedColumns as $column) {
780
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
781
                continue;
782
            }
783
784
            $fields[] = $column->getQuotedName($this);
785
        }
786
787
        if (count($fields) > 0) {
788
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' DROP (' . implode(', ', $fields) . ')';
789
        }
790
791
        $tableSql = [];
792
793
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
794
            $sql = array_merge($sql, $commentsSQL);
795
796
            $newName = $diff->getNewName();
797
798
            if ($newName !== null) {
799
                $sql[] = sprintf(
800
                    'ALTER TABLE %s RENAME TO %s',
801
                    $diff->getName($this)->getQuotedName($this),
802
                    $newName->getQuotedName($this)
803
                );
804
            }
805
806
            $sql = array_merge(
807
                $this->getPreAlterTableIndexForeignKeySQL($diff),
808
                $sql,
809
                $this->getPostAlterTableIndexForeignKeySQL($diff)
810
            );
811
        }
812
813
        return array_merge($sql, $tableSql, $columnSql);
814
    }
815
816
    /**
817
     * {@inheritdoc}
818
     */
819
    public function getColumnDeclarationSQL(string $name, array $field) : string
820
    {
821
        if (isset($field['columnDefinition'])) {
822
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
823
        } else {
824
            $default = $this->getDefaultValueDeclarationSQL($field);
825
826
            $notnull = '';
827
828
            if (isset($field['notnull'])) {
829
                $notnull = $field['notnull'] ? ' NOT NULL' : ' NULL';
830
            }
831
832
            $unique = ! empty($field['unique']) ?
833
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
834
835
            $check = ! empty($field['check']) ?
836
                ' ' . $field['check'] : '';
837
838
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
839
            $columnDef = $typeDecl . $default . $notnull . $unique . $check;
840
        }
841
842
        return $name . ' ' . $columnDef;
843
    }
844
845
    /**
846
     * {@inheritdoc}
847
     */
848
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
849
    {
850
        if (strpos($tableName, '.') !== false) {
851
            [$schema]     = explode('.', $tableName);
852
            $oldIndexName = $schema . '.' . $oldIndexName;
853
        }
854
855
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
856
    }
857
858
    public function prefersSequences() : bool
859
    {
860
        return true;
861
    }
862
863
    public function usesSequenceEmulatedIdentityColumns() : bool
864
    {
865
        return true;
866
    }
867
868
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
869
    {
870
        $table = new Identifier($tableName);
871
872
        // No usage of column name to preserve BC compatibility with <2.5
873
        $identitySequenceName = $table->getName() . '_SEQ';
874
875
        if ($table->isQuoted()) {
876
            $identitySequenceName = '"' . $identitySequenceName . '"';
877
        }
878
879
        $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName);
880
881
        return $identitySequenceIdentifier->getQuotedName($this);
882
    }
883
884
    public function supportsCommentOnStatement() : bool
885
    {
886
        return true;
887
    }
888
889
    public function getName() : string
890
    {
891
        return 'oracle';
892
    }
893
894
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
895
    {
896
        if ($limit === null && $offset <= 0) {
897
            return $query;
898
        }
899
900
        if (preg_match('/^\s*SELECT/i', $query) === 1) {
901
            if (preg_match('/\sFROM\s/i', $query) === 0) {
902
                $query .= ' FROM dual';
903
            }
904
905
            $columns = ['a.*'];
906
907
            if ($offset > 0) {
908
                $columns[] = 'ROWNUM AS doctrine_rownum';
909
            }
910
911
            $query = sprintf('SELECT %s FROM (%s) a', implode(', ', $columns), $query);
912
913
            if ($limit !== null) {
914
                $query .= sprintf(' WHERE ROWNUM <= %d', $offset + $limit);
915
            }
916
917
            if ($offset > 0) {
918
                $query = sprintf('SELECT * FROM (%s) WHERE doctrine_rownum >= %d', $query, $offset + 1);
919
            }
920
        }
921
922
        return $query;
923
    }
924
925
    /**
926
     * {@inheritDoc}
927
     *
928
     * Oracle returns all column names in SQL result sets in uppercase.
929
     */
930
    public function getSQLResultCasing(string $column) : string
931
    {
932
        return strtoupper($column);
933
    }
934
935
    public function getCreateTemporaryTableSnippetSQL() : string
936
    {
937
        return 'CREATE GLOBAL TEMPORARY TABLE';
938
    }
939
940
    public function getDateTimeTzFormatString() : string
941
    {
942
        return 'Y-m-d H:i:sP';
943
    }
944
945
    public function getDateFormatString() : string
946
    {
947
        return 'Y-m-d 00:00:00';
948
    }
949
950
    public function getTimeFormatString() : string
951
    {
952
        return '1900-01-01 H:i:s';
953
    }
954
955
    public function fixSchemaElementName(string $schemaElementName) : string
956
    {
957
        if (strlen($schemaElementName) > 30) {
958
            // Trim it
959
            return substr($schemaElementName, 0, 30);
960
        }
961
962
        return $schemaElementName;
963
    }
964
965
    public function getMaxIdentifierLength() : int
966
    {
967
        return 30;
968
    }
969
970
    public function supportsSequences() : bool
971
    {
972
        return true;
973
    }
974
975
    public function supportsForeignKeyOnUpdate() : bool
976
    {
977
        return false;
978
    }
979
980
    public function supportsReleaseSavepoints() : bool
981
    {
982
        return false;
983
    }
984
985
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
986
    {
987
        $tableIdentifier = new Identifier($tableName);
988
989
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
990
    }
991
992
    public function getDummySelectSQL(string $expression = '1') : string
993
    {
994
        return sprintf('SELECT %s FROM DUAL', $expression);
995
    }
996
997
    protected function initializeDoctrineTypeMappings() : void
998
    {
999
        $this->doctrineTypeMapping = [
1000
            'binary_double'  => 'float',
1001
            'binary_float'   => 'float',
1002
            'binary_integer' => 'boolean',
1003
            'blob'           => 'blob',
1004
            'char'           => 'string',
1005
            'clob'           => 'text',
1006
            'date'           => 'date',
1007
            'float'          => 'float',
1008
            'integer'        => 'integer',
1009
            'long'           => 'string',
1010
            'long raw'       => 'blob',
1011
            'nchar'          => 'string',
1012
            'nclob'          => 'text',
1013
            'number'         => 'integer',
1014
            'nvarchar2'      => 'string',
1015
            'pls_integer'    => 'boolean',
1016
            'raw'            => 'binary',
1017
            'rowid'          => 'string',
1018
            'timestamp'      => 'datetime',
1019
            'timestamptz'    => 'datetimetz',
1020
            'urowid'         => 'string',
1021
            'varchar'        => 'string',
1022
            'varchar2'       => 'string',
1023
        ];
1024
    }
1025
1026
    public function releaseSavePoint(string $savepoint) : string
1027
    {
1028
        return '';
1029
    }
1030
1031
    protected function getReservedKeywordsClass() : string
1032
    {
1033
        return Keywords\OracleKeywords::class;
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039
    public function getBlobTypeDeclarationSQL(array $field) : string
1040
    {
1041
        return 'BLOB';
1042
    }
1043
1044
    public function getListTableCommentsSQL(string $table, ?string $database = null) : string
1045
    {
1046
        $tableCommentsName = 'user_tab_comments';
1047
        $ownerCondition    = '';
1048
1049
        if ($database !== null && $database !== '/') {
1050
            $tableCommentsName = 'all_tab_comments';
1051
            $ownerCondition    = ' AND owner = ' . $this->quoteStringLiteral($this->normalizeIdentifier($database)->getName());
1052
        }
1053
1054
        return sprintf(
1055
            <<<'SQL'
1056
SELECT comments FROM %s WHERE table_name = %s%s
1057
SQL
1058
            ,
1059
            $tableCommentsName,
1060
            $this->quoteStringLiteral($this->normalizeIdentifier($table)->getName()),
1061
            $ownerCondition
1062
        );
1063
    }
1064
}
1065