Failed Conditions
Pull Request — master (#2766)
by mon
14:34
created

OracleSchemaManager::dropTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Driver\DriverException;
7
use Doctrine\DBAL\Platforms\OraclePlatform;
8
use Doctrine\DBAL\Types\Type;
9
use Throwable;
10
use const CASE_LOWER;
11
use function array_change_key_case;
12
use function array_values;
13
use function assert;
14
use function preg_match;
15
use function sprintf;
16
use function str_replace;
17
use function strpos;
18
use function strtolower;
19
use function strtoupper;
20
use function trim;
21
22
/**
23
 * Oracle Schema Manager.
24
 */
25
class OracleSchemaManager extends AbstractSchemaManager
26
{
27
    /**
28
     * Holds instance of the database platform used for this schema manager.
29
     *
30
     * @var OraclePlatform
31
     */
32
    protected $_platform;
33
34
    /**
35
     * {@inheritdoc}
36
     */
37 8
    public function listTables()
38
    {
39 8
        $currentDatabase = $this->_conn->getDatabase();
40
41 8
        $tableNames = $this->listTableNames();
42
43
        // Get all column definitions in one database call.
44 8
        $columnsByTable = $this->getAssetRecordsByTable($this->_platform->getListAllColumnsSQL($currentDatabase));
45
46
        // Get all foreign keys definitions in one database call.
47 8
        $foreignKeysByTable = $this->getAssetRecordsByTable($this->_platform->getListAllForeignKeysSQL($currentDatabase));
48
49
        // Get all indexes definitions in one database call.
50 8
        $indexesByTable = $this->getAssetRecordsByTable($this->_platform->getListAllIndexesSQL($currentDatabase));
51
52 8
        $tables = [];
53 8
        foreach ($tableNames as $quotedTableName) {
54 8
            $tableName = trim($quotedTableName, $this->_platform->getIdentifierQuoteCharacter());
55
56 8
            $columns = $this->_getPortableTableColumnList($quotedTableName, '', $columnsByTable[$tableName]);
57
58 8
            $foreignKeys = [];
59 8
            if (isset($foreignKeysByTable[$tableName])) {
60 6
                $foreignKeys = $this->_getPortableTableForeignKeysList($foreignKeysByTable[$tableName]);
61
            }
62
63 8
            $indexes = [];
64 8
            if (isset($indexesByTable[$tableName])) {
65 8
                $indexes = $this->_getPortableTableIndexesList($indexesByTable[$tableName], $quotedTableName);
66
            }
67
68 8
            $tables[] = new Table($quotedTableName, $columns, $indexes, $foreignKeys);
69
        }
70
71 8
        return $tables;
72
    }
73
74
    /**
75
     * Helper method to group a set of asset records by the table name.
76
     *
77
     * @param string $sql An SQL statement to be executed, that contains a
78
     *                    TABLE_NAME field for grouping.
79
     *
80
     * @return mixed[] An associative array with key being the table name, and
81
     *                 value a simple array of records associated with the table.
82
     */
83 8
    private function getAssetRecordsByTable(string $sql) : array
84
    {
85 8
        $input  = $this->_conn->fetchAll($sql);
86 8
        $output = [];
87 8
        foreach ($input as $record) {
88 8
            $output[$record['TABLE_NAME']][] = $record;
89
        }
90
91 8
        return $output;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 4
    public function dropDatabase($database)
98
    {
99
        try {
100 4
            parent::dropDatabase($database);
101 4
        } catch (DBALException $exception) {
102 4
            $exception = $exception->getPrevious();
103 4
            assert($exception instanceof Throwable);
104
105 4
            if (! $exception instanceof DriverException) {
106
                throw $exception;
107
            }
108
109
            // If we have a error code 1940 (ORA-01940), the drop database operation failed
110
            // because of active connections on the database.
111
            // To force dropping the database, we first have to close all active connections
112
            // on that database and issue the drop database operation again.
113 4
            if ($exception->getErrorCode() !== 1940) {
114 4
                throw $exception;
115
            }
116
117 2
            $this->killUserSessions($database);
118
119 2
            parent::dropDatabase($database);
120
        }
121 2
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 2
    protected function _getPortableViewDefinition($view)
127
    {
128 2
        $view = array_change_key_case($view, CASE_LOWER);
129
130 2
        return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    protected function _getPortableUserDefinition($user)
137
    {
138
        $user = array_change_key_case($user, CASE_LOWER);
139
140
        return [
141
            'user' => $user['username'],
142
        ];
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 128
    protected function _getPortableTableDefinition($table)
149
    {
150 128
        $table = array_change_key_case($table, CASE_LOWER);
151
152 128
        return $this->getQuotedIdentifierName($table['table_name']);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     *
158
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
159
     */
160 92
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
161
    {
162 92
        $indexBuffer = [];
163 92
        foreach ($tableIndexes as $tableIndex) {
164 32
            $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
165
166 32
            $keyName = strtolower($tableIndex['name']);
167 32
            $buffer  = [];
168
169 32
            if (strtolower($tableIndex['is_primary']) === 'p') {
170 28
                $keyName              = 'primary';
171 28
                $buffer['primary']    = true;
172 28
                $buffer['non_unique'] = false;
173
            } else {
174 26
                $buffer['primary']    = false;
175 26
                $buffer['non_unique'] = ! $tableIndex['is_unique'];
176
            }
177 32
            $buffer['key_name']    = $keyName;
178 32
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
179 32
            $indexBuffer[]         = $buffer;
180
        }
181
182 92
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 106
    protected function _getPortableTableColumnDefinition($tableColumn)
189
    {
190 106
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
191
192 106
        $dbType = strtolower($tableColumn['data_type']);
193 106
        if (strpos($dbType, 'timestamp(') === 0) {
194 16
            if (strpos($dbType, 'with time zone')) {
195 8
                $dbType = 'timestamptz';
196
            } else {
197 16
                $dbType = 'timestamp';
198
            }
199
        }
200
201 106
        $unsigned = $fixed = $precision = $scale = $length = null;
202
203 106
        if (! isset($tableColumn['column_name'])) {
204
            $tableColumn['column_name'] = '';
205
        }
206
207
        // Default values returned from database sometimes have trailing spaces.
208 106
        $tableColumn['data_default'] = trim($tableColumn['data_default']);
209
210 106
        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
211 100
            $tableColumn['data_default'] = null;
212
        }
213
214 106
        if ($tableColumn['data_default'] !== null) {
215
            // Default values returned from database are represented as literal expressions
216 56
            if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches)) {
217 54
                $tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
218
            }
219
        }
220
221 106
        if ($tableColumn['data_precision'] !== null) {
222 98
            $precision = (int) $tableColumn['data_precision'];
223
        }
224
225 106
        if ($tableColumn['data_scale'] !== null) {
226 100
            $scale = (int) $tableColumn['data_scale'];
227
        }
228
229 106
        $type                    = $this->_platform->getDoctrineTypeMapping($dbType);
230 106
        $type                    = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
231 106
        $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);
232
233 106
        switch ($dbType) {
234 106
            case 'number':
235 98
                if ($precision === 20 && $scale === 0) {
236
                    $type = 'bigint';
237 98
                } elseif ($precision === 5 && $scale === 0) {
238
                    $type = 'smallint';
239 98
                } elseif ($precision === 1 && $scale === 0) {
240 2
                    $type = 'boolean';
241 96
                } elseif ($scale > 0) {
242 12
                    $type = 'decimal';
243
                }
244
245 98
                break;
246 74
            case 'varchar':
247 74
            case 'varchar2':
248 30
            case 'nvarchar2':
249 58
                $length = $tableColumn['char_length'];
250 58
                $fixed  = false;
251 58
                break;
252 30
            case 'char':
253 26
            case 'nchar':
254 12
                $length = $tableColumn['char_length'];
255 12
                $fixed  = true;
256 12
                break;
257
        }
258
259
        $options = [
260 106
            'notnull'    => (bool) ($tableColumn['nullable'] === 'N'),
261 106
            'fixed'      => (bool) $fixed,
262 106
            'unsigned'   => (bool) $unsigned,
263 106
            'default'    => $tableColumn['data_default'],
264 106
            'length'     => $length,
265 106
            'precision'  => $precision,
266 106
            'scale'      => $scale,
267 106
            'comment'    => isset($tableColumn['comments']) && $tableColumn['comments'] !== ''
268 36
                ? $tableColumn['comments']
269
                : null,
270
        ];
271
272 106
        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 84
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
279
    {
280 84
        $list = [];
281 84
        foreach ($tableForeignKeys as $value) {
282 20
            $value = array_change_key_case($value, CASE_LOWER);
283 20
            if (! isset($list[$value['constraint_name']])) {
284 20
                if ($value['delete_rule'] === 'NO ACTION') {
285 18
                    $value['delete_rule'] = null;
286
                }
287
288 20
                $list[$value['constraint_name']] = [
289 20
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
290
                    'local' => [],
291
                    'foreign' => [],
292 20
                    'foreignTable' => $value['references_table'],
293 20
                    'onDelete' => $value['delete_rule'],
294
                ];
295
            }
296
297 20
            $localColumn   = $this->getQuotedIdentifierName($value['local_column']);
298 20
            $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);
299
300 20
            $list[$value['constraint_name']]['local'][$value['position']]   = $localColumn;
301 20
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
302
        }
303
304 84
        $result = [];
305 84
        foreach ($list as $constraint) {
306 20
            $result[] = new ForeignKeyConstraint(
307 20
                array_values($constraint['local']),
308 20
                $this->getQuotedIdentifierName($constraint['foreignTable']),
309 20
                array_values($constraint['foreign']),
310 20
                $this->getQuotedIdentifierName($constraint['name']),
311 20
                ['onDelete' => $constraint['onDelete']]
312
            );
313
        }
314
315 84
        return $result;
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321 11
    protected function _getPortableSequenceDefinition($sequence)
322
    {
323 11
        $sequence = array_change_key_case($sequence, CASE_LOWER);
324
325 11
        return new Sequence(
326 11
            $this->getQuotedIdentifierName($sequence['sequence_name']),
327 11
            (int) $sequence['increment_by'],
328 11
            (int) $sequence['min_value']
329
        );
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    protected function _getPortableFunctionDefinition($function)
336
    {
337
        $function = array_change_key_case($function, CASE_LOWER);
338
339
        return $function['name'];
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345 4
    protected function _getPortableDatabaseDefinition($database)
346
    {
347 4
        $database = array_change_key_case($database, CASE_LOWER);
348
349 4
        return $database['username'];
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355 4
    public function createDatabase($database = null)
356
    {
357 4
        if ($database === null) {
358
            $database = $this->_conn->getDatabase();
359
        }
360
361 4
        $params   = $this->_conn->getParams();
362 4
        $username = $database;
363 4
        $password = $params['password'];
364
365 4
        $query = 'CREATE USER ' . $username . ' IDENTIFIED BY ' . $password;
366 4
        $this->_conn->executeUpdate($query);
367
368 4
        $query = 'GRANT DBA TO ' . $username;
369 4
        $this->_conn->executeUpdate($query);
370 4
    }
371
372
    /**
373
     * @param string $table
374
     *
375
     * @return bool
376
     */
377 226
    public function dropAutoincrement($table)
378
    {
379 226
        assert($this->_platform instanceof OraclePlatform);
380
381 226
        $sql = $this->_platform->getDropAutoincrementSql($table);
382 226
        foreach ($sql as $query) {
383 226
            $this->_conn->executeUpdate($query);
384
        }
385
386
        return true;
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392 226
    public function dropTable($name)
393
    {
394 226
        $this->tryMethod('dropAutoincrement', $name);
395
396 226
        parent::dropTable($name);
397 130
    }
398
399
    /**
400
     * Returns the quoted representation of the given identifier name.
401
     *
402
     * Quotes non-uppercase identifiers explicitly to preserve case
403
     * and thus make references to the particular identifier work.
404
     *
405
     * @param string $identifier The identifier to quote.
406
     *
407
     * @return string The quoted identifier.
408
     */
409 167
    private function getQuotedIdentifierName($identifier)
410
    {
411 167
        if (preg_match('/[a-z]/', $identifier)) {
412 105
            return $this->_platform->quoteIdentifier($identifier);
413
        }
414
415 167
        return $identifier;
416
    }
417
418
    /**
419
     * Kills sessions connected with the given user.
420
     *
421
     * This is useful to force DROP USER operations which could fail because of active user sessions.
422
     *
423
     * @param string $user The name of the user to kill sessions for.
424
     *
425
     * @return void
426
     */
427 2
    private function killUserSessions($user)
428
    {
429
        $sql = <<<SQL
430 2
SELECT
431
    s.sid,
432
    s.serial#
433
FROM
434
    gv\$session s,
435
    gv\$process p
436
WHERE
437
    s.username = ?
438
    AND p.addr(+) = s.paddr
439
SQL;
440
441 2
        $activeUserSessions = $this->_conn->fetchAll($sql, [strtoupper($user)]);
442
443 2
        foreach ($activeUserSessions as $activeUserSession) {
444 2
            $activeUserSession = array_change_key_case($activeUserSession, CASE_LOWER);
445
446 2
            $this->_execSql(
447 2
                sprintf(
448 2
                    "ALTER SYSTEM KILL SESSION '%s, %s' IMMEDIATE",
449 2
                    $activeUserSession['sid'],
450 2
                    $activeUserSession['serial#']
451
                )
452
            );
453
        }
454 2
    }
455
}
456