DB2Platform.php 23.3 KB
Newer Older
1 2
<?php

Michael Moravec's avatar
Michael Moravec committed
3 4
declare(strict_types=1);

5 6
namespace Doctrine\DBAL\Platforms;

7
use Doctrine\DBAL\Platforms\Exception\NotSupported;
8
use Doctrine\DBAL\Schema\ColumnDiff;
9
use Doctrine\DBAL\Schema\Identifier;
10 11
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\Types\Type;
13
use Doctrine\DBAL\Types\Types;
14 15 16 17 18 19 20 21
use function array_merge;
use function count;
use function current;
use function explode;
use function implode;
use function sprintf;
use function strpos;
use function strtoupper;
22

23
class DB2Platform extends AbstractPlatform
24
{
25
    /**
26
     * {@inheritDoc}
27
     */
28
    public function getBlobTypeDeclarationSQL(array $field) : string
29
    {
30 31
        // todo blob(n) with $field['length'];
        return 'BLOB(1M)';
32 33
    }

34
    public function initializeDoctrineTypeMappings() : void
35
    {
36
        $this->doctrineTypeMapping = [
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
            'bigint'    => 'bigint',
            'binary'    => 'binary',
            'blob'      => 'blob',
            'character' => 'string',
            'clob'      => 'text',
            'date'      => 'date',
            'decimal'   => 'decimal',
            'double'    => 'float',
            'integer'   => 'integer',
            'real'      => 'float',
            'smallint'  => 'smallint',
            'time'      => 'time',
            'timestamp' => 'datetime',
            'varbinary' => 'binary',
            'varchar'   => 'string',
52
        ];
53 54
    }

55
    public function isCommentedDoctrineType(Type $doctrineType) : bool
56
    {
57
        if ($doctrineType->getName() === Types::BOOLEAN) {
58 59 60 61 62 63 64 65
            // We require a commented boolean type in order to distinguish between boolean and smallint
            // as both (have to) map to the same native type.
            return true;
        }

        return parent::isCommentedDoctrineType($doctrineType);
    }

66
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length) : string
67
    {
68
        return $this->getCharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA';
69 70
    }

71
    protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length) : string
Steve Müller's avatar
Steve Müller committed
72
    {
73
        return $this->getVarcharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA';
Steve Müller's avatar
Steve Müller committed
74 75
    }

76
    /**
77
     * {@inheritDoc}
78
     */
79
    public function getClobTypeDeclarationSQL(array $field) : string
80 81 82 83 84
    {
        // todo clob(n) with $field['length'];
        return 'CLOB(1M)';
    }

85
    public function getName() : string
86 87 88 89 90
    {
        return 'db2';
    }

    /**
91
     * {@inheritDoc}
92
     */
93
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
94 95 96 97 98
    {
        return 'SMALLINT';
    }

    /**
99
     * {@inheritDoc}
100
     */
101
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
102
    {
103
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
104 105 106
    }

    /**
107
     * {@inheritDoc}
108
     */
109
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
110
    {
111
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
112 113 114
    }

    /**
115
     * {@inheritDoc}
116
     */
117
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
118
    {
119
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
120 121 122
    }

    /**
123
     * {@inheritDoc}
124
     */
125
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
126
    {
127
        $autoinc = '';
128
        if (! empty($columnDef['autoincrement'])) {
129 130
            $autoinc = ' GENERATED BY DEFAULT AS IDENTITY';
        }
131

132
        return $autoinc;
133 134
    }

135
    public function getBitAndComparisonExpression(string $value1, string $value2) : string
136 137 138 139
    {
        return 'BITAND(' . $value1 . ', ' . $value2 . ')';
    }

140
    public function getBitOrComparisonExpression(string $value1, string $value2) : string
141 142 143 144
    {
        return 'BITOR(' . $value1 . ', ' . $value2 . ')';
    }

145
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
146 147
    {
        switch ($unit) {
148
            case DateIntervalUnit::WEEK:
149 150
                $interval = $this->multiplyInterval($interval, 7);
                $unit     = DateIntervalUnit::DAY;
151 152
                break;

153
            case DateIntervalUnit::QUARTER:
154 155
                $interval = $this->multiplyInterval($interval, 3);
                $unit     = DateIntervalUnit::MONTH;
156 157
                break;
        }
158

159
        return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit;
160 161
    }

162
    public function getDateDiffExpression(string $date1, string $date2) : string
163 164 165 166
    {
        return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')';
    }

167
    /**
168
     * {@inheritDoc}
169
     */
170
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
171
    {
172 173
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] === true) {
            return 'TIMESTAMP(0) WITH DEFAULT';
174 175
        }

176 177 178 179
        return 'TIMESTAMP(0)';
    }

    /**
180
     * {@inheritDoc}
181
     */
182
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
183 184 185 186 187
    {
        return 'DATE';
    }

    /**
188
     * {@inheritDoc}
189
     */
190
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
191 192 193 194
    {
        return 'TIME';
    }

195
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
196
    {
197 198 199
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE';
200 201
    }

202
    /**
203
     * This code fragment is originally from the Zend_Db_Adapter_Db2 class, but has been edited.
204
     */
205
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
206
    {
207 208
        $table = $this->quoteStringLiteral($table);

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
        // We do the funky subquery and join syscat.columns.default this crazy way because
        // as of db2 v10, the column is CLOB(64k) and the distinct operator won't allow a CLOB,
        // it wants shorter stuff like a varchar.
        return "
        SELECT
          cols.default,
          subq.*
        FROM (
               SELECT DISTINCT
                 c.tabschema,
                 c.tabname,
                 c.colname,
                 c.colno,
                 c.typename,
                 c.nulls,
                 c.length,
                 c.scale,
                 c.identity,
                 tc.type AS tabconsttype,
228
                 c.remarks AS comment,
229 230 231 232 233 234 235 236 237 238 239 240 241
                 k.colseq,
                 CASE
                 WHEN c.generated = 'D' THEN 1
                 ELSE 0
                 END     AS autoincrement
               FROM syscat.columns c
                 LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc
                     ON (k.tabschema = tc.tabschema
                         AND k.tabname = tc.tabname
                         AND tc.type = 'P'))
                   ON (c.tabschema = k.tabschema
                       AND c.tabname = k.tabname
                       AND c.colname = k.colname)
242
               WHERE UPPER(c.tabname) = UPPER(" . $table . ')
243 244 245 246 247 248 249
               ORDER BY c.colno
             ) subq
          JOIN syscat.columns cols
            ON subq.tabschema = cols.tabschema
               AND subq.tabname = cols.tabname
               AND subq.colno = cols.colno
        ORDER BY subq.colno
250
        ';
251 252
    }

253
    public function getListTablesSQL() : string
254
    {
255
        return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = 'T'";
256 257
    }

258
    public function getListViewsSQL(string $database) : string
259
    {
260
        return 'SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS';
261 262
    }

263
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
264
    {
265 266
        $table = $this->quoteStringLiteral($table);

267 268 269 270 271 272 273 274 275 276 277 278 279
        return "SELECT   idx.INDNAME AS key_name,
                         idxcol.COLNAME AS column_name,
                         CASE
                             WHEN idx.UNIQUERULE = 'P' THEN 1
                             ELSE 0
                         END AS primary,
                         CASE
                             WHEN idx.UNIQUERULE = 'D' THEN 1
                             ELSE 0
                         END AS non_unique
                FROM     SYSCAT.INDEXES AS idx
                JOIN     SYSCAT.INDEXCOLUSE AS idxcol
                ON       idx.INDSCHEMA = idxcol.INDSCHEMA AND idx.INDNAME = idxcol.INDNAME
280 281
                WHERE    idx.TABNAME = UPPER(" . $table . ')
                ORDER BY idxcol.COLSEQ ASC';
282 283
    }

284
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
285
    {
286 287
        $table = $this->quoteStringLiteral($table);

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
        return "SELECT   fkcol.COLNAME AS local_column,
                         fk.REFTABNAME AS foreign_table,
                         pkcol.COLNAME AS foreign_column,
                         fk.CONSTNAME AS index_name,
                         CASE
                             WHEN fk.UPDATERULE = 'R' THEN 'RESTRICT'
                             ELSE NULL
                         END AS on_update,
                         CASE
                             WHEN fk.DELETERULE = 'C' THEN 'CASCADE'
                             WHEN fk.DELETERULE = 'N' THEN 'SET NULL'
                             WHEN fk.DELETERULE = 'R' THEN 'RESTRICT'
                             ELSE NULL
                         END AS on_delete
                FROM     SYSCAT.REFERENCES AS fk
                JOIN     SYSCAT.KEYCOLUSE AS fkcol
                ON       fk.CONSTNAME = fkcol.CONSTNAME
                AND      fk.TABSCHEMA = fkcol.TABSCHEMA
                AND      fk.TABNAME = fkcol.TABNAME
                JOIN     SYSCAT.KEYCOLUSE AS pkcol
                ON       fk.REFKEYNAME = pkcol.CONSTNAME
                AND      fk.REFTABSCHEMA = pkcol.TABSCHEMA
                AND      fk.REFTABNAME = pkcol.TABNAME
311 312
                WHERE    fk.TABNAME = UPPER(" . $table . ')
                ORDER BY fkcol.COLSEQ ASC';
313 314
    }

315
    public function getCreateViewSQL(string $name, string $sql) : string
316
    {
317
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
318 319
    }

320
    public function getDropViewSQL(string $name) : string
321
    {
322
        return 'DROP VIEW ' . $name;
323 324
    }

325
    public function getCreateDatabaseSQL(string $database) : string
326
    {
327
        return 'CREATE DATABASE ' . $database;
328 329
    }

330
    public function getDropDatabaseSQL(string $database) : string
331
    {
332
        return 'DROP DATABASE ' . $database;
333 334
    }

335
    public function supportsCreateDropDatabase() : bool
336 337 338
    {
        return false;
    }
339

340
    public function supportsReleaseSavepoints() : bool
341 342 343 344
    {
        return false;
    }

345
    public function supportsCommentOnStatement() : bool
346 347 348 349
    {
        return true;
    }

350
    public function getCurrentDateSQL() : string
351
    {
352
        return 'CURRENT DATE';
353 354
    }

355
    public function getCurrentTimeSQL() : string
356
    {
357
        return 'CURRENT TIME';
358 359
    }

360
    public function getCurrentTimestampSQL() : string
361
    {
362
        return 'CURRENT TIMESTAMP';
363
    }
364

365
    public function getIndexDeclarationSQL(string $name, Index $index) : string
366
    {
367
        // Index declaration in statements like CREATE TABLE is not supported.
368
        throw NotSupported::new(__METHOD__);
369 370 371
    }

    /**
372
     * {@inheritDoc}
373
     */
374
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
375
    {
376
        $indexes = [];
377 378 379
        if (isset($options['indexes'])) {
            $indexes = $options['indexes'];
        }
380

381
        $options['indexes'] = [];
382

383 384
        $sqls = parent::_getCreateTableSQL($tableName, $columns, $options);

385
        foreach ($indexes as $definition) {
386 387
            $sqls[] = $this->getCreateIndexSQL($definition, $tableName);
        }
388

389 390 391 392
        return $sqls;
    }

    /**
393
     * {@inheritDoc}
394
     */
395
    public function getAlterTableSQL(TableDiff $diff) : array
396
    {
397 398
        $sql         = [];
        $columnSql   = [];
399
        $commentsSQL = [];
400

401
        $queryParts = [];
402
        foreach ($diff->addedColumns as $column) {
403 404
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
405 406
            }

407 408 409 410
            $columnDef = $column->toArray();
            $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

            // Adding non-nullable columns to a table requires a default value to be specified.
411
            if (! empty($columnDef['notnull']) &&
412 413 414 415 416 417 418
                ! isset($columnDef['default']) &&
                empty($columnDef['autoincrement'])
            ) {
                $queryPart .= ' WITH DEFAULT';
            }

            $queryParts[] = $queryPart;
419 420 421

            $comment = $this->getColumnComment($column);

422
            if ($comment === '') {
423
                continue;
424
            }
425 426 427 428 429 430

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
431 432
        }

433
        foreach ($diff->removedColumns as $column) {
434 435
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
436 437
            }

438
            $queryParts[] =  'DROP COLUMN ' . $column->getQuotedName($this);
439 440
        }

441
        foreach ($diff->changedColumns as $columnDiff) {
442 443
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
444 445
            }

446 447 448 449 450 451 452 453 454 455 456 457
            if ($columnDiff->hasChanged('comment')) {
                $commentsSQL[] = $this->getCommentOnColumnSQL(
                    $diff->getName($this)->getQuotedName($this),
                    $columnDiff->column->getQuotedName($this),
                    $this->getColumnComment($columnDiff->column)
                );

                if (count($columnDiff->changedProperties) === 1) {
                    continue;
                }
            }

Sergei Morozov's avatar
Sergei Morozov committed
458
            $this->gatherAlterColumnSQL($diff->getName($this), $columnDiff, $sql, $queryParts);
459 460
        }

461
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
462 463
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
464 465
            }

466 467 468 469
            $oldColumnName = new Identifier($oldColumnName);

            $queryParts[] =  'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) .
                ' TO ' . $column->getQuotedName($this);
470 471
        }

472
        $tableSql = [];
473

474
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
475
            if (count($queryParts) > 0) {
476
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . implode(' ', $queryParts);
477
            }
478

479
            // Some table alteration operations require a table reorganization.
480
            if (! empty($diff->removedColumns) || ! empty($diff->changedColumns)) {
481 482 483
                $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $diff->getName($this)->getQuotedName($this) . "')";
            }

484 485
            $sql = array_merge($sql, $commentsSQL);

Sergei Morozov's avatar
Sergei Morozov committed
486 487
            $newName = $diff->getNewName();

488
            if ($newName !== null) {
Sergei Morozov's avatar
Sergei Morozov committed
489 490 491 492 493
                $sql[] = sprintf(
                    'RENAME TABLE %s TO %s',
                    $diff->getName($this)->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
494 495
            }

496 497 498 499 500
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
501 502
        }

503
        return array_merge($sql, $tableSql, $columnSql);
504 505
    }

506 507 508
    /**
     * Gathers the table alteration SQL for a given column diff.
     *
Sergei Morozov's avatar
Sergei Morozov committed
509
     * @param Identifier $table      The table to gather the SQL for.
510
     * @param ColumnDiff $columnDiff The column diff to evaluate.
511 512
     * @param string[]   $sql        The sequence of table alteration statements to fill.
     * @param mixed[]    $queryParts The sequence of column alteration clauses to fill.
513
     */
514
    private function gatherAlterColumnSQL(Identifier $table, ColumnDiff $columnDiff, array &$sql, array &$queryParts) : void
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
    {
        $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff);

        if (empty($alterColumnClauses)) {
            return;
        }

        // If we have a single column alteration, we can append the clause to the main query.
        if (count($alterColumnClauses) === 1) {
            $queryParts[] = current($alterColumnClauses);

            return;
        }

        // We have multiple alterations for the same column,
        // so we need to trigger a complete ALTER TABLE statement
        // for each ALTER COLUMN clause.
        foreach ($alterColumnClauses as $alterColumnClause) {
            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ' . $alterColumnClause;
        }
    }

    /**
     * Returns the ALTER COLUMN SQL clauses for altering a column described by the given column diff.
     *
     * @param ColumnDiff $columnDiff The column diff to evaluate.
     *
542
     * @return string[]
543
     */
544
    private function getAlterColumnClausesSQL(ColumnDiff $columnDiff) : array
545 546 547 548 549
    {
        $column = $columnDiff->column->toArray();

        $alterClause = 'ALTER COLUMN ' . $columnDiff->column->getQuotedName($this);

550
        if ($column['columnDefinition'] !== null) {
551
            return [$alterClause . ' ' . $column['columnDefinition']];
552 553
        }

554
        $clauses = [];
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572

        if ($columnDiff->hasChanged('type') ||
            $columnDiff->hasChanged('length') ||
            $columnDiff->hasChanged('precision') ||
            $columnDiff->hasChanged('scale') ||
            $columnDiff->hasChanged('fixed')
        ) {
            $clauses[] = $alterClause . ' SET DATA TYPE ' . $column['type']->getSQLDeclaration($column, $this);
        }

        if ($columnDiff->hasChanged('notnull')) {
            $clauses[] = $column['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL';
        }

        if ($columnDiff->hasChanged('default')) {
            if (isset($column['default'])) {
                $defaultClause = $this->getDefaultValueDeclarationSQL($column);

573
                if ($defaultClause !== '') {
574 575 576 577 578 579 580 581 582 583
                    $clauses[] = $alterClause . ' SET' . $defaultClause;
                }
            } else {
                $clauses[] = $alterClause . ' DROP DEFAULT';
            }
        }

        return $clauses;
    }

584 585 586
    /**
     * {@inheritDoc}
     */
587
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) : array
588
    {
589
        $sql   = [];
590
        $table = $diff->getName($this)->getQuotedName($this);
591 592 593

        foreach ($diff->removedIndexes as $remKey => $remIndex) {
            foreach ($diff->addedIndexes as $addKey => $addIndex) {
594
                if ($remIndex->getColumns() === $addIndex->getColumns()) {
595 596 597 598 599 600 601 602 603 604
                    if ($remIndex->isPrimary()) {
                        $sql[] = 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
                    } elseif ($remIndex->isUnique()) {
                        $sql[] = 'ALTER TABLE ' . $table . ' DROP UNIQUE ' . $remIndex->getQuotedName($this);
                    } else {
                        $sql[] = $this->getDropIndexSQL($remIndex, $table);
                    }

                    $sql[] = $this->getCreateIndexSQL($addIndex, $table);

605
                    unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);
606 607 608 609 610 611 612 613 614 615 616

                    break;
                }
            }
        }

        $sql = array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff));

        return $sql;
    }

617 618 619
    /**
     * {@inheritdoc}
     */
620
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
621
    {
622
        if (strpos($tableName, '.') !== false) {
623
            [$schema]     = explode('.', $tableName);
624 625 626
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

627
        return ['RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)];
628 629
    }

630 631 632
    /**
     * {@inheritDoc}
     */
633
    public function getDefaultValueDeclarationSQL(array $field) : string
634
    {
635
        if (! empty($field['autoincrement'])) {
636
            return '';
637 638
        }

639
        if (! empty($field['version'])) {
640 641
            if ((string) $field['type'] !== 'DateTime') {
                $field['default'] = '1';
642 643 644
            }
        }

645 646 647
        return parent::getDefaultValueDeclarationSQL($field);
    }

648
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
649 650 651 652
    {
        return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)';
    }

653
    public function getCreateTemporaryTableSnippetSQL() : string
654
    {
655
        return 'DECLARE GLOBAL TEMPORARY TABLE';
656 657
    }

658
    public function getTemporaryTableName(string $tableName) : string
659
    {
660
        return 'SESSION.' . $tableName;
661 662
    }

663
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
664
    {
665
        $where = [];
666 667 668

        if ($offset > 0) {
            $where[] = sprintf('db22.DC_ROWNUM >= %d', $offset + 1);
669 670
        }

671 672 673
        if ($limit !== null) {
            $where[] = sprintf('db22.DC_ROWNUM <= %d', $offset + $limit);
        }
674

675 676 677
        if (empty($where)) {
            return $query;
        }
678

679 680 681 682 683 684
        // Todo OVER() needs ORDER BY data!
        return sprintf(
            'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM FROM (%s) db21) db22 WHERE %s',
            $query,
            implode(' AND ', $where)
        );
685 686
    }

687
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
688
    {
689 690
        if ($start === null) {
            return sprintf('LOCATE(%s, %s)', $substring, $string);
691
        }
692

693
        return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start);
694 695
    }

696
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
697
    {
698
        if ($length === null) {
699
            return sprintf('SUBSTR(%s, %s)', $string, $start);
700
        }
701

702
        return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length);
703 704
    }

705 706 707 708 709
    public function getCurrentDatabaseExpression() : string
    {
        return 'CURRENT_USER';
    }

710
    public function supportsIdentityColumns() : bool
711 712 713 714
    {
        return true;
    }

715
    public function prefersIdentityColumns() : bool
716 717 718
    {
        return true;
    }
719 720

    /**
721
     * {@inheritDoc}
722 723 724
     *
     * DB2 returns all column names in SQL result sets in uppercase.
     */
725
    public function getSQLResultCasing(string $column) : string
726 727 728
    {
        return strtoupper($column);
    }
729

730
    public function getForUpdateSQL() : string
731 732 733
    {
        return ' WITH RR USE AND KEEP UPDATE LOCKS';
    }
734

735
    public function getDummySelectSQL(string $expression = '1') : string
736
    {
737
        return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression);
738
    }
739 740

    /**
741 742
     * {@inheritDoc}
     *
743 744 745 746
     * DB2 supports savepoints, but they work semantically different than on other vendor platforms.
     *
     * TODO: We have to investigate how to get DB2 up and running with savepoints.
     */
747
    public function supportsSavepoints() : bool
748 749 750
    {
        return false;
    }
751

752
    protected function getReservedKeywordsClass() : string
753
    {
754
        return Keywords\DB2Keywords::class;
755
    }
756 757 758 759 760 761 762 763 764 765 766 767 768

    public function getListTableCommentsSQL(string $table) : string
    {
        return sprintf(
            <<<'SQL'
SELECT REMARKS
  FROM SYSIBM.SYSTABLES
  WHERE NAME = UPPER( %s )
SQL
            ,
            $this->quoteStringLiteral($table)
        );
    }
769
}