Completed
Pull Request — master (#3769)
by Benjamin
65:11 queued 11s
created

ityColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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