OraclePlatform::getDateTimeTzFormatString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 1
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 308
    public static function assertValidIdentifier(string $identifier) : void
42
    {
43 308
        if (preg_match('(^(([a-zA-Z]{1}[a-zA-Z0-9_$#]{0,})|("[^"]+"))$)', $identifier) === 0) {
44 110
            throw new DBALException('Invalid Oracle identifier.');
45
        }
46 198
    }
47
48 3
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
49
    {
50 3
        if ($length === null) {
51 1
            return sprintf('SUBSTR(%s, %s)', $string, $start);
52
        }
53
54 2
        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 1
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
69
    {
70 1
        if ($start === null) {
71 1
            return sprintf('INSTR(%s, %s)', $string, $substring);
72
        }
73
74 1
        return sprintf('INSTR(%s, %s, %s)', $string, $substring, $start);
75
    }
76
77 48
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
78
    {
79
        switch ($unit) {
80 48
            case DateIntervalUnit::MONTH:
81 42
            case DateIntervalUnit::QUARTER:
82 36
            case DateIntervalUnit::YEAR:
83
                switch ($unit) {
84 18
                    case DateIntervalUnit::QUARTER:
85 6
                        $interval = $this->multiplyInterval($interval, 3);
86 6
                        break;
87
88 12
                    case DateIntervalUnit::YEAR:
89 6
                        $interval = $this->multiplyInterval($interval, 12);
90 6
                        break;
91
                }
92
93 18
                return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')';
94
95
            default:
96 30
                $calculationClause = '';
97
98
                switch ($unit) {
99 30
                    case DateIntervalUnit::SECOND:
100 6
                        $calculationClause = '/24/60/60';
101 6
                        break;
102
103 24
                    case DateIntervalUnit::MINUTE:
104 6
                        $calculationClause = '/24/60';
105 6
                        break;
106
107 18
                    case DateIntervalUnit::HOUR:
108 6
                        $calculationClause = '/24';
109 6
                        break;
110
111 12
                    case DateIntervalUnit::WEEK:
112 6
                        $calculationClause = '*7';
113 6
                        break;
114
                }
115
116 30
                return '(' . $date . $operator . $interval . $calculationClause . ')';
117
        }
118
    }
119
120 3
    public function getDateDiffExpression(string $date1, string $date2) : string
121
    {
122 3
        return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2);
123
    }
124
125 45
    public function getBitAndComparisonExpression(string $value1, string $value2) : string
126
    {
127 45
        return 'BITAND(' . $value1 . ', ' . $value2 . ')';
128
    }
129
130 68
    public function getCurrentDatabaseExpression() : string
131
    {
132 68
        return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')";
133
    }
134
135 23
    public function getBitOrComparisonExpression(string $value1, string $value2) : string
136
    {
137 23
        return '(' . $value1 . '-' .
138 23
                $this->getBitAndComparisonExpression($value1, $value2)
139 23
                . '+' . $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 158
    public function getCreateSequenceSQL(Sequence $sequence) : string
150
    {
151 158
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
152 158
               ' START WITH ' . $sequence->getInitialValue() .
153 158
               ' MINVALUE ' . $sequence->getInitialValue() .
154 158
               ' INCREMENT BY ' . $sequence->getAllocationSize() .
155 158
               $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 158
    private function getSequenceCacheSQL(Sequence $sequence) : string
169
    {
170 158
        if ($sequence->getCache() === 0) {
171 22
            return ' NOCACHE';
172
        }
173
174 136
        if ($sequence->getCache() === 1) {
175 22
            return ' NOCACHE';
176
        }
177
178 114
        if ($sequence->getCache() > 1) {
179 22
            return ' CACHE ' . $sequence->getCache();
180
        }
181
182 92
        return '';
183
    }
184
185 1
    public function getSequenceNextValSQL(string $sequenceName) : string
186
    {
187 1
        return 'SELECT ' . $sequenceName . '.nextval FROM DUAL';
188
    }
189
190 22
    public function getSetTransactionIsolationSQL(int $level) : string
191
    {
192 22
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
193
    }
194
195 22
    protected function _getTransactionIsolationLevelSQL(int $level) : string
196
    {
197
        switch ($level) {
198 22
            case TransactionIsolationLevel::READ_UNCOMMITTED:
199 22
                return 'READ UNCOMMITTED';
200
201 22
            case TransactionIsolationLevel::READ_COMMITTED:
202 22
                return 'READ COMMITTED';
203
204 22
            case TransactionIsolationLevel::REPEATABLE_READ:
205 22
            case TransactionIsolationLevel::SERIALIZABLE:
206 22
                return 'SERIALIZABLE';
207
208
            default:
209
                return parent::_getTransactionIsolationLevelSQL($level);
210
        }
211
    }
212
213
    /**
214
     * {@inheritDoc}
215
     */
216 38
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
217
    {
218 38
        return 'NUMBER(1)';
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224 348
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
225
    {
226 348
        return 'NUMBER(10)';
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232 15
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
233
    {
234 15
        return 'NUMBER(20)';
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240 1
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
241
    {
242 1
        return 'NUMBER(5)';
243
    }
244
245
    /**
246
     * {@inheritDoc}
247
     */
248 24
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
249
    {
250 24
        return 'TIMESTAMP(0)';
251
    }
252
253
    /**
254
     * {@inheritDoc}
255
     */
256 16
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
257
    {
258 16
        return 'TIMESTAMP(0) WITH TIME ZONE';
259
    }
260
261
    /**
262
     * {@inheritDoc}
263
     */
264 20
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
265
    {
266 20
        return 'DATE';
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272 18
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
273
    {
274 18
        return 'DATE';
275
    }
276
277
    /**
278
     * {@inheritDoc}
279
     */
280
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
281
    {
282
        return '';
283
    }
284
285 360
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
286
    {
287 360
        if ($length === null) {
288 22
            throw ColumnLengthRequired::new($this, 'VARCHAR2');
289
        }
290
291 338
        return sprintf('VARCHAR2(%d)', $length);
292
    }
293
294 90
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length) : string
295
    {
296 90
        if ($length === null) {
297 44
            throw ColumnLengthRequired::new($this, 'RAW');
298
        }
299
300 46
        return sprintf('RAW(%d)', $length);
301
    }
302
303 46
    protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length) : string
304
    {
305 46
        return $this->getBinaryTypeDeclarationSQLSnippet($length);
306
    }
307
308
    /**
309
     * {@inheritDoc}
310
     */
311 85
    public function getClobTypeDeclarationSQL(array $field) : string
312
    {
313 85
        return 'CLOB';
314
    }
315
316 2
    public function getListDatabasesSQL() : string
317
    {
318 2
        return 'SELECT username FROM all_users';
319
    }
320
321 27
    public function getListSequencesSQL(string $database) : string
322
    {
323
        return 'SELECT SEQUENCE_NAME, MIN_VALUE, INCREMENT_BY FROM SYS.ALL_SEQUENCES WHERE SEQUENCE_OWNER = '
324 27
            . $this->quoteStringLiteral(
325 27
                $this->normalizeIdentifier($database)->getName()
326
            );
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332 379
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
333
    {
334 379
        $indexes            = $options['indexes'] ?? [];
335 379
        $options['indexes'] = [];
336 379
        $sql                = parent::_getCreateTableSQL($tableName, $columns, $options);
337
338 379
        foreach ($columns as $column) {
339 379
            if (isset($column['sequence'])) {
340
                $sql[] = $this->getCreateSequenceSQL($column['sequence']);
341
            }
342
343 379
            if (empty($column['autoincrement'])) {
344 335
                continue;
345
            }
346
347 89
            $sql = array_merge($sql, $this->getCreateAutoincrementSql($column['name'], $tableName));
348
        }
349
350 379
        if (isset($indexes) && ! empty($indexes)) {
351 82
            foreach ($indexes as $index) {
352 82
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
353
            }
354
        }
355
356 379
        return $sql;
357
    }
358
359
    /**
360
     * {@inheritDoc}
361
     *
362
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaOracleReader.html
363
     */
364 73
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
365
    {
366 73
        $table = $this->normalizeIdentifier($table);
367 73
        $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 73
             WHERE     uind_col.table_name = " . $table . '
395
             ORDER BY  uind_col.column_position ASC';
396
    }
397
398 13
    public function getListTablesSQL() : string
399
    {
400 13
        return 'SELECT * FROM sys.user_tables';
401
    }
402
403 1
    public function getListViewsSQL(string $database) : string
404
    {
405 1
        return 'SELECT view_name, text FROM sys.user_views';
406
    }
407
408 1
    public function getCreateViewSQL(string $name, string $sql) : string
409
    {
410 1
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
411
    }
412
413 1
    public function getDropViewSQL(string $name) : string
414
    {
415 1
        return 'DROP VIEW ' . $name;
416
    }
417
418
    /**
419
     * @return array<int, string>
420
     */
421 89
    public function getCreateAutoincrementSql(string $name, string $table, int $start = 1) : array
422
    {
423 89
        $tableIdentifier   = $this->normalizeIdentifier($table);
424 89
        $quotedTableName   = $tableIdentifier->getQuotedName($this);
425 89
        $unquotedTableName = $tableIdentifier->getName();
426
427 89
        $nameIdentifier = $this->normalizeIdentifier($name);
428 89
        $quotedName     = $nameIdentifier->getQuotedName($this);
429 89
        $unquotedName   = $nameIdentifier->getName();
430
431 89
        $sql = [];
432
433 89
        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier);
434
435 89
        $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true);
436
437 89
        $sql[] = 'DECLARE
438
  constraints_Count NUMBER;
439
BEGIN
440 89
  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 89
    EXECUTE IMMEDIATE \'' . $this->getCreateConstraintSQL($idx, $quotedTableName) . '\';
443
  END IF;
444
END;';
445
446 89
        $sequenceName = $this->getIdentitySequenceName(
447 89
            $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName,
448 89
            $nameIdentifier->isQuoted() ? $quotedName : $unquotedName
449
        );
450 89
        $sequence     = new Sequence($sequenceName, $start);
451 89
        $sql[]        = $this->getCreateSequenceSQL($sequence);
452
453 89
        $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . '
454
   BEFORE INSERT
455 89
   ON ' . $quotedTableName . '
456
   FOR EACH ROW
457
DECLARE
458
   last_Sequence NUMBER;
459
   last_InsertID NUMBER;
460
BEGIN
461 89
   SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
462 89
   IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN
463 89
      SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
464
   ELSE
465
      SELECT NVL(Last_Number, 0) INTO last_Sequence
466
        FROM User_Sequences
467 89
       WHERE Sequence_Name = \'' . $sequence->getName() . '\';
468 89
      SELECT :NEW.' . $quotedName . ' INTO last_InsertID FROM DUAL;
469
      WHILE (last_InsertID > last_Sequence) LOOP
470 89
         SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL;
471
      END LOOP;
472
   END IF;
473
END;';
474
475 89
        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 197
    public function getDropAutoincrementSql(string $table) : array
486
    {
487 197
        $table                       = $this->normalizeIdentifier($table);
488 197
        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table);
489 197
        $identitySequenceName        = $this->getIdentitySequenceName(
490 197
            $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(),
491 197
            ''
492
        );
493
494
        return [
495 197
            'DROP TRIGGER ' . $autoincrementIdentifierName,
496 197
            $this->getDropSequenceSQL($identitySequenceName),
497 197
            $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 510
    private function normalizeIdentifier(string $name) : Identifier
512
    {
513 510
        $identifier = new Identifier($name);
514
515 510
        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 264
    private function getAutoincrementIdentifierName(Identifier $table) : string
527
    {
528 264
        $identifierName = $table->getName() . '_AI_PK';
529
530 264
        return $table->isQuoted()
531 45
            ? $this->quoteSingleIdentifier($identifierName)
532 264
            : $identifierName;
533
    }
534
535 71
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
536
    {
537 71
        $table = $this->normalizeIdentifier($table);
538 71
        $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 71
      AND alc.table_name = " . $table . '
561
    ORDER BY cols.constraint_name ASC, cols.position ASC';
562
    }
563
564 22
    public function getListTableConstraintsSQL(string $table) : string
565
    {
566 22
        $table = $this->normalizeIdentifier($table);
567 22
        $table = $this->quoteStringLiteral($table->getName());
568
569 22
        return 'SELECT * FROM user_constraints WHERE table_name = ' . $table;
570
    }
571
572 164
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
573
    {
574 164
        $table = $this->normalizeIdentifier($table);
575 164
        $table = $this->quoteStringLiteral($table->getName());
576
577 164
        $tabColumnsTableName       = 'user_tab_columns';
578 164
        $colCommentsTableName      = 'user_col_comments';
579 164
        $tabColumnsOwnerCondition  = '';
580 164
        $colCommentsOwnerCondition = '';
581
582 164
        if ($database !== null && $database !== '/') {
583 98
            $database                  = $this->normalizeIdentifier($database);
584 98
            $database                  = $this->quoteStringLiteral($database->getName());
585 98
            $tabColumnsTableName       = 'all_tab_columns';
586 98
            $colCommentsTableName      = 'all_col_comments';
587 98
            $tabColumnsOwnerCondition  = ' AND c.owner = ' . $database;
588 98
            $colCommentsOwnerCondition = ' AND d.OWNER = c.OWNER';
589
        }
590
591 164
        return sprintf(
592
            <<<'SQL'
593 59
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 164
            $colCommentsTableName,
606 164
            $colCommentsOwnerCondition,
607 164
            $tabColumnsTableName,
608 164
            $table,
609 164
            $tabColumnsOwnerCondition
610
        );
611
    }
612
613
    /**
614
     * {@inheritDoc}
615
     */
616 197
    public function getDropSequenceSQL($sequence) : string
617
    {
618 197
        if ($sequence instanceof Sequence) {
619
            $sequence = $sequence->getQuotedName($this);
620
        }
621
622 197
        return 'DROP SEQUENCE ' . $sequence;
623
    }
624
625
    /**
626
     * {@inheritDoc}
627
     */
628 45
    public function getDropForeignKeySQL($foreignKey, $table) : string
629
    {
630 45
        if (! $foreignKey instanceof ForeignKeyConstraint) {
631 22
            $foreignKey = new Identifier($foreignKey);
632
        }
633
634 45
        if (! $table instanceof Table) {
635 45
            $table = new Identifier($table);
636
        }
637
638 45
        $foreignKey = $foreignKey->getQuotedName($this);
639 45
        $table      = $table->getQuotedName($this);
640
641 45
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
642
    }
643
644 212
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
645
    {
646 212
        $referentialAction = '';
647
648 212
        if ($foreignKey->hasOption('onDelete')) {
649 89
            $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete'));
650
        }
651
652 212
        if ($referentialAction !== '') {
653 45
            return ' ON DELETE ' . $referentialAction;
654
        }
655
656 167
        return '';
657
    }
658
659 221
    public function getForeignKeyReferentialActionSQL(string $action) : string
660
    {
661 221
        $action = strtoupper($action);
662
663 11
        switch ($action) {
664 221
            case 'RESTRICT': // RESTRICT is not supported, therefore falling back to NO ACTION.
665 177
            case 'NO ACTION':
666
                // NO ACTION cannot be declared explicitly,
667
                // therefore returning empty string to indicate to OMIT the referential clause.
668 88
                return '';
669
670 133
            case 'CASCADE':
671 66
            case 'SET NULL':
672 111
                return $action;
673
674
            default:
675
                // SET DEFAULT is not supported, throw exception instead.
676 22
                throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $action));
677
        }
678
    }
679
680 24
    public function getDropDatabaseSQL(string $database) : string
681
    {
682 24
        return 'DROP USER ' . $database . ' CASCADE';
683
    }
684
685
    /**
686
     * {@inheritDoc}
687
     */
688 364
    public function getAlterTableSQL(TableDiff $diff) : array
689
    {
690 364
        $sql         = [];
691 364
        $commentsSQL = [];
692 364
        $columnSql   = [];
693
694 364
        $fields = [];
695
696 364
        foreach ($diff->addedColumns as $column) {
697 89
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
698
                continue;
699
            }
700
701 89
            $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
702 89
            $comment  = $this->getColumnComment($column);
703
704 89
            if ($comment === '') {
705 67
                continue;
706
            }
707
708 22
            $commentsSQL[] = $this->getCommentOnColumnSQL(
709 22
                $diff->getName($this)->getQuotedName($this),
710 22
                $column->getQuotedName($this),
711
                $comment
712
            );
713
        }
714
715 364
        if (count($fields) > 0) {
716 89
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ADD (' . implode(', ', $fields) . ')';
717
        }
718
719 364
        $fields = [];
720 364
        foreach ($diff->changedColumns as $columnDiff) {
721 206
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
722
                continue;
723
            }
724
725 206
            $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 206
            if ($column->getType() instanceof BinaryType &&
731 206
                $columnDiff->hasChanged('fixed') &&
732 206
                count($columnDiff->changedProperties) === 1
733
            ) {
734 22
                continue;
735
            }
736
737 184
            $columnHasChangedComment = $columnDiff->hasChanged('comment');
738
739
            /**
740
             * Do not add query part if only comment has changed
741
             */
742 184
            if (! ($columnHasChangedComment && count($columnDiff->changedProperties) === 1)) {
743 135
                $columnInfo = $column->toArray();
744
745 135
                if (! $columnDiff->hasChanged('notnull')) {
746 91
                    unset($columnInfo['notnull']);
747
                }
748
749 135
                $fields[] = $column->getQuotedName($this) . $this->getColumnDeclarationSQL('', $columnInfo);
750
            }
751
752 184
            if (! $columnHasChangedComment) {
753 135
                continue;
754
            }
755
756 49
            $commentsSQL[] = $this->getCommentOnColumnSQL(
757 49
                $diff->getName($this)->getQuotedName($this),
758 49
                $column->getQuotedName($this),
759 49
                $this->getColumnComment($column)
760
            );
761
        }
762
763 364
        if (count($fields) > 0) {
764 135
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' MODIFY (' . implode(', ', $fields) . ')';
765
        }
766
767 364
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
768 89
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
769
                continue;
770
            }
771
772 89
            $oldColumnName = new Identifier($oldColumnName);
773
774 89
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
775 89
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
776
        }
777
778 364
        $fields = [];
779 364
        foreach ($diff->removedColumns as $column) {
780 67
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
781
                continue;
782
            }
783
784 67
            $fields[] = $column->getQuotedName($this);
785
        }
786
787 364
        if (count($fields) > 0) {
788 67
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' DROP (' . implode(', ', $fields) . ')';
789
        }
790
791 364
        $tableSql = [];
792
793 364
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
794 364
            $sql = array_merge($sql, $commentsSQL);
795
796 364
            $newName = $diff->getNewName();
797
798 364
            if ($newName !== null) {
799 45
                $sql[] = sprintf(
800 3
                    'ALTER TABLE %s RENAME TO %s',
801 45
                    $diff->getName($this)->getQuotedName($this),
802 45
                    $newName->getQuotedName($this)
803
                );
804
            }
805
806 364
            $sql = array_merge(
807 364
                $this->getPreAlterTableIndexForeignKeySQL($diff),
808 364
                $sql,
809 364
                $this->getPostAlterTableIndexForeignKeySQL($diff)
810
            );
811
        }
812
813 364
        return array_merge($sql, $tableSql, $columnSql);
814
    }
815
816
    /**
817
     * {@inheritdoc}
818
     */
819 555
    public function getColumnDeclarationSQL(string $name, array $field) : string
820
    {
821 555
        if (isset($field['columnDefinition'])) {
822 22
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
823
        } else {
824 533
            $default = $this->getDefaultValueDeclarationSQL($field);
825
826 533
            $notnull = '';
827
828 533
            if (isset($field['notnull'])) {
829 489
                $notnull = $field['notnull'] ? ' NOT NULL' : ' NULL';
830
            }
831
832 533
            $unique = ! empty($field['unique']) ?
833 533
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
834
835 533
            $check = ! empty($field['check']) ?
836 533
                ' ' . $field['check'] : '';
837
838 533
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
839 533
            $columnDef = $typeDecl . $default . $notnull . $unique . $check;
840
        }
841
842 555
        return $name . ' ' . $columnDef;
843
    }
844
845
    /**
846
     * {@inheritdoc}
847
     */
848 112
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
849
    {
850 112
        if (strpos($tableName, '.') !== false) {
851 44
            [$schema]     = explode('.', $tableName);
852 44
            $oldIndexName = $schema . '.' . $oldIndexName;
853
        }
854
855 112
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
856
    }
857
858
    public function prefersSequences() : bool
859
    {
860
        return true;
861
    }
862
863 23
    public function usesSequenceEmulatedIdentityColumns() : bool
864
    {
865 23
        return true;
866
    }
867
868 286
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
869
    {
870 286
        $table = new Identifier($tableName);
871
872
        // No usage of column name to preserve BC compatibility with <2.5
873 286
        $identitySequenceName = $table->getName() . '_SEQ';
874
875 286
        if ($table->isQuoted()) {
876 67
            $identitySequenceName = '"' . $identitySequenceName . '"';
877
        }
878
879 286
        $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName);
880
881 286
        return $identitySequenceIdentifier->getQuotedName($this);
882
    }
883
884 401
    public function supportsCommentOnStatement() : bool
885
    {
886 401
        return true;
887
    }
888
889 160
    public function getName() : string
890
    {
891 160
        return 'oracle';
892
    }
893
894 162
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
895
    {
896 162
        if ($limit === null && $offset <= 0) {
897 23
            return $query;
898
        }
899
900 139
        if (preg_match('/^\s*SELECT/i', $query) === 1) {
901 139
            if (preg_match('/\sFROM\s/i', $query) === 0) {
902
                $query .= ' FROM dual';
903
            }
904
905 139
            $columns = ['a.*'];
906
907 139
            if ($offset > 0) {
908 51
                $columns[] = 'ROWNUM AS doctrine_rownum';
909
            }
910
911 139
            $query = sprintf('SELECT %s FROM (%s) a', implode(', ', $columns), $query);
912
913 139
            if ($limit !== null) {
914 117
                $query .= sprintf(' WHERE ROWNUM <= %d', $offset + $limit);
915
            }
916
917 139
            if ($offset > 0) {
918 51
                $query = sprintf('SELECT * FROM (%s) WHERE doctrine_rownum >= %d', $query, $offset + 1);
919
            }
920
        }
921
922 139
        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 1
    public function getDateTimeTzFormatString() : string
941
    {
942 1
        return 'Y-m-d H:i:sP';
943
    }
944
945 1
    public function getDateFormatString() : string
946
    {
947 1
        return 'Y-m-d 00:00:00';
948
    }
949
950 1
    public function getTimeFormatString() : string
951
    {
952 1
        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 13
    public function getMaxIdentifierLength() : int
966
    {
967 13
        return 30;
968
    }
969
970 7
    public function supportsSequences() : bool
971
    {
972 7
        return true;
973
    }
974
975
    public function supportsForeignKeyOnUpdate() : bool
976
    {
977
        return false;
978
    }
979
980 1
    public function supportsReleaseSavepoints() : bool
981
    {
982 1
        return false;
983
    }
984
985 31
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
986
    {
987 31
        $tableIdentifier = new Identifier($tableName);
988
989 31
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
990
    }
991
992 81
    public function getDummySelectSQL(string $expression = '1') : string
993
    {
994 81
        return sprintf('SELECT %s FROM DUAL', $expression);
995
    }
996
997 111
    protected function initializeDoctrineTypeMappings() : void
998
    {
999 111
        $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 111
    }
1025
1026
    public function releaseSavePoint(string $savepoint) : string
1027
    {
1028
        return '';
1029
    }
1030
1031 1068
    protected function getReservedKeywordsClass() : string
1032
    {
1033 1068
        return Keywords\OracleKeywords::class;
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039 5
    public function getBlobTypeDeclarationSQL(array $field) : string
1040
    {
1041 5
        return 'BLOB';
1042
    }
1043
1044 46
    public function getListTableCommentsSQL(string $table, ?string $database = null) : string
1045
    {
1046 46
        $tableCommentsName = 'user_tab_comments';
1047 46
        $ownerCondition    = '';
1048
1049 46
        if ($database !== null && $database !== '/') {
1050
            $tableCommentsName = 'all_tab_comments';
1051
            $ownerCondition    = ' AND owner = ' . $this->quoteStringLiteral($this->normalizeIdentifier($database)->getName());
1052
        }
1053
1054 46
        return sprintf(
1055
            <<<'SQL'
1056 46
SELECT comments FROM %s WHERE table_name = %s%s
1057
SQL
1058
            ,
1059 46
            $tableCommentsName,
1060 46
            $this->quoteStringLiteral($this->normalizeIdentifier($table)->getName()),
1061 46
            $ownerCondition
1062
        );
1063
    }
1064
}
1065