OracleSchemaManager.php 13.5 KB
Newer Older
romanb's avatar
romanb committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
Benjamin Morel's avatar
Benjamin Morel committed
17
 * <http://www.doctrine-project.org>.
romanb's avatar
romanb committed
18 19
 */

20
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
21

22 23
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\DriverException;
24
use Doctrine\DBAL\Platforms\OraclePlatform;
25
use Doctrine\DBAL\Types\Type;
26 27 28
use const CASE_LOWER;
use function array_change_key_case;
use function array_values;
29
use function assert;
30 31 32 33 34 35 36
use function is_null;
use function preg_match;
use function sprintf;
use function strpos;
use function strtolower;
use function strtoupper;
use function trim;
37

romanb's avatar
romanb committed
38
/**
Benjamin Morel's avatar
Benjamin Morel committed
39
 * Oracle Schema Manager.
romanb's avatar
romanb committed
40
 *
Benjamin Morel's avatar
Benjamin Morel committed
41 42 43 44
 * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
 * @author Benjamin Eberlei <kontakt@beberlei.de>
 * @since  2.0
romanb's avatar
romanb committed
45
 */
46
class OracleSchemaManager extends AbstractSchemaManager
47
{
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();

            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.
            if ($exception->getErrorCode() !== 1940) {
                throw $exception;
            }

            $this->killUserSessions($database);

            parent::dropDatabase($database);
        }
    }

Benjamin Morel's avatar
Benjamin Morel committed
76 77 78
    /**
     * {@inheritdoc}
     */
79
    protected function _getPortableViewDefinition($view)
romanb's avatar
romanb committed
80
    {
81 82
        $view = \array_change_key_case($view, CASE_LOWER);

83
        return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
romanb's avatar
romanb committed
84 85
    }

Benjamin Morel's avatar
Benjamin Morel committed
86 87 88
    /**
     * {@inheritdoc}
     */
89
    protected function _getPortableUserDefinition($user)
romanb's avatar
romanb committed
90
    {
91 92
        $user = \array_change_key_case($user, CASE_LOWER);

93
        return [
94
            'user' => $user['username'],
95
        ];
96
    }
romanb's avatar
romanb committed
97

Benjamin Morel's avatar
Benjamin Morel committed
98 99 100
    /**
     * {@inheritdoc}
     */
101 102
    protected function _getPortableTableDefinition($table)
    {
103 104
        $table = \array_change_key_case($table, CASE_LOWER);

105
        return $this->getQuotedIdentifierName($table['table_name']);
romanb's avatar
romanb committed
106 107
    }

108
    /**
Benjamin Morel's avatar
Benjamin Morel committed
109 110
     * {@inheritdoc}
     *
111 112 113 114
     * @license New BSD License
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
romanb's avatar
romanb committed
115
    {
116
        $indexBuffer = [];
Steve Müller's avatar
Steve Müller committed
117
        foreach ($tableIndexes as $tableIndex) {
118 119 120
            $tableIndex = \array_change_key_case($tableIndex, CASE_LOWER);

            $keyName = strtolower($tableIndex['name']);
121
            $buffer  = [];
122

Steve Müller's avatar
Steve Müller committed
123
            if (strtolower($tableIndex['is_primary']) == "p") {
124 125 126 127 128
                $keyName = 'primary';
                $buffer['primary'] = true;
                $buffer['non_unique'] = false;
            } else {
                $buffer['primary'] = false;
129
                $buffer['non_unique'] = ! $tableIndex['is_unique'];
130 131
            }
            $buffer['key_name'] = $keyName;
132
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
133 134
            $indexBuffer[] = $buffer;
        }
Benjamin Morel's avatar
Benjamin Morel committed
135

136
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
137
    }
romanb's avatar
romanb committed
138

Benjamin Morel's avatar
Benjamin Morel committed
139 140 141
    /**
     * {@inheritdoc}
     */
142 143
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
144
        $tableColumn = \array_change_key_case($tableColumn, CASE_LOWER);
145

146
        $dbType = strtolower($tableColumn['data_type']);
Steve Müller's avatar
Steve Müller committed
147
        if (strpos($dbType, "timestamp(") === 0) {
148
            if (strpos($dbType, "with time zone")) {
149 150 151 152
                $dbType = "timestamptz";
            } else {
                $dbType = "timestamp";
            }
153 154
        }

Benjamin Morel's avatar
Benjamin Morel committed
155
        $unsigned = $fixed = null;
romanb's avatar
romanb committed
156

157 158 159
        if ( ! isset($tableColumn['column_name'])) {
            $tableColumn['column_name'] = '';
        }
romanb's avatar
romanb committed
160

161 162 163 164
        // Default values returned from database sometimes have trailing spaces.
        $tableColumn['data_default'] = trim($tableColumn['data_default']);

        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
165
            $tableColumn['data_default'] = null;
romanb's avatar
romanb committed
166 167
        }

168 169
        if (null !== $tableColumn['data_default']) {
            // Default values returned from database are enclosed in single quotes.
170
            $tableColumn['data_default'] = trim($tableColumn['data_default'], "'");
171 172
        }

173 174
        $precision = null;
        $scale = null;
175 176

        $type = $this->_platform->getDoctrineTypeMapping($dbType);
177 178 179
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
        $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);

180
        switch ($dbType) {
181
            case 'number':
182 183 184 185 186 187 188 189 190 191 192 193 194
                if ($tableColumn['data_precision'] == 20 && $tableColumn['data_scale'] == 0) {
                    $precision = 20;
                    $scale = 0;
                    $type = 'bigint';
                } elseif ($tableColumn['data_precision'] == 5 && $tableColumn['data_scale'] == 0) {
                    $type = 'smallint';
                    $precision = 5;
                    $scale = 0;
                } elseif ($tableColumn['data_precision'] == 1 && $tableColumn['data_scale'] == 0) {
                    $precision = 1;
                    $scale = 0;
                    $type = 'boolean';
                } elseif ($tableColumn['data_scale'] > 0) {
195 196
                    $precision = $tableColumn['data_precision'];
                    $scale = $tableColumn['data_scale'];
197
                    $type = 'decimal';
198
                }
199 200
                $length = null;
                break;
201 202
            case 'pls_integer':
            case 'binary_integer':
203
                $length = null;
204 205 206 207
                break;
            case 'varchar':
            case 'varchar2':
            case 'nvarchar2':
208
                $length = $tableColumn['char_length'];
209
                $fixed = false;
210
                break;
211 212
            case 'char':
            case 'nchar':
213
                $length = $tableColumn['char_length'];
214
                $fixed = true;
215 216 217 218 219 220
                break;
            case 'date':
            case 'timestamp':
                $length = null;
                break;
            case 'float':
221 222
            case 'binary_float':
            case 'binary_double':
223 224
                $precision = $tableColumn['data_precision'];
                $scale = $tableColumn['data_scale'];
225
                $length = null;
226 227 228
                break;
            case 'clob':
            case 'nclob':
229
                $length = null;
230 231 232 233 234 235
                break;
            case 'blob':
            case 'raw':
            case 'long raw':
            case 'bfile':
                $length = null;
236
                break;
237 238 239 240 241
            case 'rowid':
            case 'urowid':
            default:
                $length = null;
        }
romanb's avatar
romanb committed
242

243
        $options = [
244 245 246 247 248 249 250
            'notnull'    => (bool) ($tableColumn['nullable'] === 'N'),
            'fixed'      => (bool) $fixed,
            'unsigned'   => (bool) $unsigned,
            'default'    => $tableColumn['data_default'],
            'length'     => $length,
            'precision'  => $precision,
            'scale'      => $scale,
251 252 253
            'comment'    => isset($tableColumn['comments']) && '' !== $tableColumn['comments']
                ? $tableColumn['comments']
                : null,
254
        ];
255

256
        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
257 258
    }

Benjamin Morel's avatar
Benjamin Morel committed
259 260 261
    /**
     * {@inheritdoc}
     */
262 263
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
264
        $list = [];
265
        foreach ($tableForeignKeys as $value) {
266 267
            $value = \array_change_key_case($value, CASE_LOWER);
            if (!isset($list[$value['constraint_name']])) {
268 269 270 271
                if ($value['delete_rule'] == "NO ACTION") {
                    $value['delete_rule'] = null;
                }

272
                $list[$value['constraint_name']] = [
273
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
274 275
                    'local' => [],
                    'foreign' => [],
276
                    'foreignTable' => $value['references_table'],
277
                    'onDelete' => $value['delete_rule'],
278
                ];
279
            }
280 281 282 283 284 285

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

            $list[$value['constraint_name']]['local'][$value['position']] = $localColumn;
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
286 287
        }

288
        $result = [];
Steve Müller's avatar
Steve Müller committed
289
        foreach ($list as $constraint) {
290
            $result[] = new ForeignKeyConstraint(
291 292
                array_values($constraint['local']), $this->getQuotedIdentifierName($constraint['foreignTable']),
                array_values($constraint['foreign']), $this->getQuotedIdentifierName($constraint['name']),
293
                ['onDelete' => $constraint['onDelete']]
294 295 296 297 298 299
            );
        }

        return $result;
    }

Benjamin Morel's avatar
Benjamin Morel committed
300 301 302
    /**
     * {@inheritdoc}
     */
303 304 305
    protected function _getPortableSequenceDefinition($sequence)
    {
        $sequence = \array_change_key_case($sequence, CASE_LOWER);
Benjamin Morel's avatar
Benjamin Morel committed
306

307 308
        return new Sequence(
            $this->getQuotedIdentifierName($sequence['sequence_name']),
309 310
            (int) $sequence['increment_by'],
            (int) $sequence['min_value']
311
        );
romanb's avatar
romanb committed
312 313
    }

Benjamin Morel's avatar
Benjamin Morel committed
314 315 316
    /**
     * {@inheritdoc}
     */
317 318
    protected function _getPortableFunctionDefinition($function)
    {
319
        $function = \array_change_key_case($function, CASE_LOWER);
Benjamin Morel's avatar
Benjamin Morel committed
320

321 322
        return $function['name'];
    }
romanb's avatar
romanb committed
323

Benjamin Morel's avatar
Benjamin Morel committed
324 325 326
    /**
     * {@inheritdoc}
     */
327 328
    protected function _getPortableDatabaseDefinition($database)
    {
329
        $database = \array_change_key_case($database, CASE_LOWER);
Benjamin Morel's avatar
Benjamin Morel committed
330

331 332
        return $database['username'];
    }
romanb's avatar
romanb committed
333

Benjamin Morel's avatar
Benjamin Morel committed
334 335 336
    /**
     * {@inheritdoc}
     */
337 338
    public function createDatabase($database = null)
    {
339
        if ($database === null) {
340 341
            $database = $this->_conn->getDatabase();
        }
romanb's avatar
romanb committed
342

343 344 345
        $params = $this->_conn->getParams();
        $username   = $database;
        $password   = $params['password'];
romanb's avatar
romanb committed
346

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

350
        $query = 'GRANT DBA TO ' . $username;
Benjamin Morel's avatar
Benjamin Morel committed
351
        $this->_conn->executeUpdate($query);
romanb's avatar
romanb committed
352
    }
353

Benjamin Morel's avatar
Benjamin Morel committed
354 355 356
    /**
     * @param string $table
     *
357
     * @return bool
Benjamin Morel's avatar
Benjamin Morel committed
358
     */
359
    public function dropAutoincrement($table)
romanb's avatar
romanb committed
360
    {
361 362
        assert($this->_platform instanceof OraclePlatform);

363 364
        $sql = $this->_platform->getDropAutoincrementSql($table);
        foreach ($sql as $query) {
365
            $this->_conn->executeUpdate($query);
366 367 368
        }

        return true;
romanb's avatar
romanb committed
369 370
    }

Benjamin Morel's avatar
Benjamin Morel committed
371 372 373
    /**
     * {@inheritdoc}
     */
romanb's avatar
romanb committed
374 375
    public function dropTable($name)
    {
376
        $this->tryMethod('dropAutoincrement', $name);
romanb's avatar
romanb committed
377

Benjamin Morel's avatar
Benjamin Morel committed
378
        parent::dropTable($name);
romanb's avatar
romanb committed
379
    }
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398

    /**
     * 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.
     *
     * @param string $identifier The identifier to quote.
     *
     * @return string The quoted identifier.
     */
    private function getQuotedIdentifierName($identifier)
    {
        if (preg_match('/[a-z]/', $identifier)) {
            return $this->_platform->quoteIdentifier($identifier);
        }

        return $identifier;
    }
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422

    /**
     * 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.
     *
     * @return void
     */
    private function killUserSessions($user)
    {
        $sql = <<<SQL
SELECT
    s.sid,
    s.serial#
FROM
    gv\$session s,
    gv\$process p
WHERE
    s.username = ?
    AND p.addr(+) = s.paddr
SQL;

423
        $activeUserSessions = $this->_conn->fetchAll($sql, [strtoupper($user)]);
424 425 426 427 428 429 430 431 432 433 434 435 436

        foreach ($activeUserSessions as $activeUserSession) {
            $activeUserSession = array_change_key_case($activeUserSession, \CASE_LOWER);

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