OracleSchemaManager.php 11.6 KB
Newer Older
romanb's avatar
romanb committed
1 2
<?php

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

5
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
6

7 8
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\DriverException;
9
use Doctrine\DBAL\Platforms\OraclePlatform;
10
use Doctrine\DBAL\Types\Type;
Sergei Morozov's avatar
Sergei Morozov committed
11
use Throwable;
12 13 14
use const CASE_LOWER;
use function array_change_key_case;
use function array_values;
15
use function assert;
16 17
use function preg_match;
use function sprintf;
18
use function str_replace;
19 20 21 22
use function strpos;
use function strtolower;
use function strtoupper;
use function trim;
23

romanb's avatar
romanb committed
24
/**
Benjamin Morel's avatar
Benjamin Morel committed
25
 * Oracle Schema Manager.
romanb's avatar
romanb committed
26
 */
27
class OracleSchemaManager extends AbstractSchemaManager
28
{
29 30 31
    /**
     * {@inheritdoc}
     */
32
    public function dropDatabase(string $database) : void
33 34 35 36 37
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();
Sergei Morozov's avatar
Sergei Morozov committed
38
            assert($exception instanceof Throwable);
39 40 41 42 43 44 45 46 47

            if (! $exception instanceof DriverException) {
                throw $exception;
            }

            // If we have a error code 1940 (ORA-01940), the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
48
            if ($exception->getCode() !== 1940) {
49 50 51 52 53 54 55 56 57
                throw $exception;
            }

            $this->killUserSessions($database);

            parent::dropDatabase($database);
        }
    }

Benjamin Morel's avatar
Benjamin Morel committed
58 59 60
    /**
     * {@inheritdoc}
     */
61
    protected function _getPortableViewDefinition(array $view) : View
romanb's avatar
romanb committed
62
    {
63
        $view = array_change_key_case($view, CASE_LOWER);
64

65
        return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
romanb's avatar
romanb committed
66 67
    }

Benjamin Morel's avatar
Benjamin Morel committed
68 69 70
    /**
     * {@inheritdoc}
     */
71
    protected function _getPortableUserDefinition(array $user) : array
romanb's avatar
romanb committed
72
    {
73
        $user = array_change_key_case($user, CASE_LOWER);
74

75
        return [
76
            'user' => $user['username'],
77
        ];
78
    }
romanb's avatar
romanb committed
79

Benjamin Morel's avatar
Benjamin Morel committed
80 81 82
    /**
     * {@inheritdoc}
     */
83
    protected function _getPortableTableDefinition(array $table) : string
84
    {
85
        $table = array_change_key_case($table, CASE_LOWER);
86

87
        return $this->getQuotedIdentifierName($table['table_name']);
romanb's avatar
romanb committed
88 89
    }

90
    /**
Benjamin Morel's avatar
Benjamin Morel committed
91 92
     * {@inheritdoc}
     *
93 94
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
95
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
romanb's avatar
romanb committed
96
    {
97
        $indexBuffer = [];
98
        foreach ($tableIndexRows as $tableIndex) {
99
            $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
100 101

            $keyName = strtolower($tableIndex['name']);
102
            $buffer  = [];
103

Michael Moravec's avatar
Michael Moravec committed
104
            if ($tableIndex['is_primary'] === 'P') {
105 106
                $keyName              = 'primary';
                $buffer['primary']    = true;
107 108
                $buffer['non_unique'] = false;
            } else {
109
                $buffer['primary']    = false;
110
                $buffer['non_unique'] = ! $tableIndex['is_unique'];
111
            }
112
            $buffer['key_name']    = $keyName;
113
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
114
            $indexBuffer[]         = $buffer;
115
        }
Benjamin Morel's avatar
Benjamin Morel committed
116

117
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
118
    }
romanb's avatar
romanb committed
119

Benjamin Morel's avatar
Benjamin Morel committed
120 121 122
    /**
     * {@inheritdoc}
     */
123
    protected function _getPortableTableColumnDefinition(array $tableColumn) : Column
124
    {
125
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
126

127
        $dbType = strtolower($tableColumn['data_type']);
128 129 130
        if (strpos($dbType, 'timestamp(') === 0) {
            if (strpos($dbType, 'with time zone')) {
                $dbType = 'timestamptz';
131
            } else {
132
                $dbType = 'timestamp';
133
            }
134 135
        }

136 137 138
        $length = $precision = null;
        $scale  = 0;
        $fixed  = false;
romanb's avatar
romanb committed
139

140
        if (! isset($tableColumn['column_name'])) {
141 142
            $tableColumn['column_name'] = '';
        }
romanb's avatar
romanb committed
143

144
        // Default values returned from database sometimes have trailing spaces.
Michael Moravec's avatar
Michael Moravec committed
145 146 147
        if ($tableColumn['data_default'] !== null) {
            $tableColumn['data_default'] = trim($tableColumn['data_default']);
        }
148 149

        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
150
            $tableColumn['data_default'] = null;
romanb's avatar
romanb committed
151 152
        }

153
        if ($tableColumn['data_default'] !== null) {
154 155 156 157
            // Default values returned from database are represented as literal expressions
            if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches)) {
                $tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
            }
158 159
        }

160 161 162 163 164 165 166
        if ($tableColumn['data_precision'] !== null) {
            $precision = (int) $tableColumn['data_precision'];
        }

        if ($tableColumn['data_scale'] !== null) {
            $scale = (int) $tableColumn['data_scale'];
        }
167

168 169
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'])
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
170

171
        switch ($dbType) {
172
            case 'number':
173 174 175 176 177 178 179 180
                if ($precision === 20 && $scale === 0) {
                    $type = 'bigint';
                } elseif ($precision === 5 && $scale === 0) {
                    $type = 'smallint';
                } elseif ($precision === 1 && $scale === 0) {
                    $type = 'boolean';
                } elseif ($scale > 0) {
                    $type = 'decimal';
181
                }
182

183 184 185 186
                break;
            case 'varchar':
            case 'varchar2':
            case 'nvarchar2':
187
                $length = (int) $tableColumn['char_length'];
188
                break;
189 190
            case 'char':
            case 'nchar':
191
                $length = (int) $tableColumn['char_length'];
192
                $fixed  = true;
193 194
                break;
        }
romanb's avatar
romanb committed
195

196
        $options = [
197 198
            'notnull'    => $tableColumn['nullable'] === 'N',
            'fixed'      => $fixed,
199 200 201 202
            'default'    => $tableColumn['data_default'],
            'length'     => $length,
            'precision'  => $precision,
            'scale'      => $scale,
203
            'comment'    => isset($tableColumn['comments']) && $tableColumn['comments'] !== ''
204 205
                ? $tableColumn['comments']
                : null,
206
        ];
207

208
        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
209 210
    }

Benjamin Morel's avatar
Benjamin Morel committed
211 212 213
    /**
     * {@inheritdoc}
     */
214
    protected function _getPortableTableForeignKeysList(array $tableForeignKeys) : array
215
    {
216
        $list = [];
217
        foreach ($tableForeignKeys as $value) {
218 219 220
            $value = array_change_key_case($value, CASE_LOWER);
            if (! isset($list[$value['constraint_name']])) {
                if ($value['delete_rule'] === 'NO ACTION') {
221 222 223
                    $value['delete_rule'] = null;
                }

224
                $list[$value['constraint_name']] = [
225
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
226 227
                    'local' => [],
                    'foreign' => [],
228
                    'foreignTable' => $value['references_table'],
229
                    'onDelete' => $value['delete_rule'],
230
                ];
231
            }
232

233
            $localColumn   = $this->getQuotedIdentifierName($value['local_column']);
234 235
            $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);

236
            $list[$value['constraint_name']]['local'][$value['position']]   = $localColumn;
237
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
238 239
        }

240
        $result = [];
Steve Müller's avatar
Steve Müller committed
241
        foreach ($list as $constraint) {
242
            $result[] = new ForeignKeyConstraint(
243 244 245 246
                array_values($constraint['local']),
                $this->getQuotedIdentifierName($constraint['foreignTable']),
                array_values($constraint['foreign']),
                $this->getQuotedIdentifierName($constraint['name']),
247
                ['onDelete' => $constraint['onDelete']]
248 249 250 251 252 253
            );
        }

        return $result;
    }

Benjamin Morel's avatar
Benjamin Morel committed
254 255 256
    /**
     * {@inheritdoc}
     */
257
    protected function _getPortableSequenceDefinition(array $sequence) : Sequence
258
    {
259
        $sequence = array_change_key_case($sequence, CASE_LOWER);
Benjamin Morel's avatar
Benjamin Morel committed
260

261 262
        return new Sequence(
            $this->getQuotedIdentifierName($sequence['sequence_name']),
263 264
            (int) $sequence['increment_by'],
            (int) $sequence['min_value']
265
        );
romanb's avatar
romanb committed
266 267
    }

Benjamin Morel's avatar
Benjamin Morel committed
268 269
    /**
     * {@inheritdoc}
270 271
     *
     * @deprecated
Benjamin Morel's avatar
Benjamin Morel committed
272
     */
273
    protected function _getPortableDatabaseDefinition(array $database) : string
274
    {
275
        $database = array_change_key_case($database, CASE_LOWER);
Benjamin Morel's avatar
Benjamin Morel committed
276

277 278
        return $database['username'];
    }
romanb's avatar
romanb committed
279

Benjamin Morel's avatar
Benjamin Morel committed
280 281
    /**
     * {@inheritdoc}
282 283
     *
     * Calling this method without an argument or by passing NULL is deprecated.
Benjamin Morel's avatar
Benjamin Morel committed
284
     */
285
    public function createDatabase(string $database) : void
286
    {
287 288 289
        $params   = $this->_conn->getParams();
        $username = $database;
        $password = $params['password'];
romanb's avatar
romanb committed
290

291
        $query = 'CREATE USER ' . $username . ' IDENTIFIED BY ' . $password;
Benjamin Morel's avatar
Benjamin Morel committed
292
        $this->_conn->executeUpdate($query);
293

294
        $query = 'GRANT DBA TO ' . $username;
Benjamin Morel's avatar
Benjamin Morel committed
295
        $this->_conn->executeUpdate($query);
romanb's avatar
romanb committed
296
    }
297

298
    public function dropAutoincrement(string $table) : bool
romanb's avatar
romanb committed
299
    {
300 301
        assert($this->_platform instanceof OraclePlatform);

302 303
        $sql = $this->_platform->getDropAutoincrementSql($table);
        foreach ($sql as $query) {
304
            $this->_conn->executeUpdate($query);
305 306 307
        }

        return true;
romanb's avatar
romanb committed
308 309
    }

Benjamin Morel's avatar
Benjamin Morel committed
310 311 312
    /**
     * {@inheritdoc}
     */
313
    public function dropTable(string $name) : void
romanb's avatar
romanb committed
314
    {
315
        $this->tryMethod('dropAutoincrement', $name);
romanb's avatar
romanb committed
316

Benjamin Morel's avatar
Benjamin Morel committed
317
        parent::dropTable($name);
romanb's avatar
romanb committed
318
    }
319 320 321 322 323 324 325

    /**
     * Returns the quoted representation of the given identifier name.
     *
     * Quotes non-uppercase identifiers explicitly to preserve case
     * and thus make references to the particular identifier work.
     */
326
    private function getQuotedIdentifierName(string $identifier) : string
327 328 329 330 331 332 333
    {
        if (preg_match('/[a-z]/', $identifier)) {
            return $this->_platform->quoteIdentifier($identifier);
        }

        return $identifier;
    }
334 335 336 337 338 339 340 341

    /**
     * Kills sessions connected with the given user.
     *
     * This is useful to force DROP USER operations which could fail because of active user sessions.
     *
     * @param string $user The name of the user to kill sessions for.
     */
342
    private function killUserSessions(string $user) : void
343 344 345 346 347 348 349 350 351 352 353 354 355
    {
        $sql = <<<SQL
SELECT
    s.sid,
    s.serial#
FROM
    gv\$session s,
    gv\$process p
WHERE
    s.username = ?
    AND p.addr(+) = s.paddr
SQL;

356
        $activeUserSessions = $this->_conn->fetchAll($sql, [strtoupper($user)]);
357 358

        foreach ($activeUserSessions as $activeUserSession) {
359
            $activeUserSession = array_change_key_case($activeUserSession, CASE_LOWER);
360 361 362 363 364 365 366 367 368 369

            $this->_execSql(
                sprintf(
                    "ALTER SYSTEM KILL SESSION '%s, %s' IMMEDIATE",
                    $activeUserSession['sid'],
                    $activeUserSession['serial#']
                )
            );
        }
    }
370

371
    public function listTableDetails(string $tableName) : Table
372 373 374 375 376 377 378 379 380 381 382 383
    {
        $table = parent::listTableDetails($tableName);

        /** @var OraclePlatform $platform */
        $platform = $this->_platform;
        $sql      = $platform->getListTableCommentsSQL($tableName);

        $tableOptions = $this->_conn->fetchAssoc($sql);
        $table->addOption('comment', $tableOptions['COMMENTS']);

        return $table;
    }
384
}