Failed Conditions
Pull Request — master (#2766)
by mon
18:50
created

OracleSchemaManager::listTables()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 35
rs 9.7
c 0
b 0
f 0
ccs 18
cts 18
cp 1
cc 4
nc 5
nop 0
crap 4
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 6
    public function listTables()
38
    {
39 6
        $currentDatabase = $this->_conn->getDatabase();
40
41 6
        $tableNames = $this->listTableNames();
42
43
        // Get all column definitions in one database call.
44 6
        $columnsByTable = $this->getAssetRecordsByTable($this->_platform->getListAllColumnsSQL($currentDatabase));
45
46
        // Get all foreign keys definitions in one database call.
47 6
        $foreignKeysByTable = $this->getAssetRecordsByTable($this->_platform->getListAllForeignKeysSQL($currentDatabase));
48
49
        // Get all indexes definitions in one database call.
50 6
        $indexesByTable = $this->getAssetRecordsByTable($this->_platform->getListAllIndexesSQL($currentDatabase));
51
52 6
        $tables = [];
53 6
        foreach ($tableNames as $quotedTableName) {
54 6
            $tableName = trim($quotedTableName, $this->_platform->getIdentifierQuoteCharacter());
55
56 6
            $columns = $this->_getPortableTableColumnList($quotedTableName, '', $columnsByTable[$tableName]);
57
58 6
            $foreignKeys = [];
59 6
            if (isset($foreignKeysByTable[$tableName])) {
60 4
                $foreignKeys = $this->_getPortableTableForeignKeysList($foreignKeysByTable[$tableName]);
61
            }
62
63 6
            $indexes = [];
64 6
            if (isset($indexesByTable[$tableName])) {
65 6
                $indexes = $this->_getPortableTableIndexesList($indexesByTable[$tableName], $quotedTableName);
66
            }
67
68 6
            $tables[] = new Table($quotedTableName, $columns, $indexes, $foreignKeys);
69
        }
70
71 6
        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 6
    private function getAssetRecordsByTable(string $sql) : array
84
    {
85 6
        $input  = $this->_conn->fetchAll($sql);
86 6
        $output = [];
87 6
        foreach ($input as $record) {
88 6
            $output[$record['TABLE_NAME']][] = $record;
89
        }
90
91 6
        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 126
    protected function _getPortableTableDefinition($table)
149
    {
150 126
        $table = array_change_key_case($table, CASE_LOWER);
151
152 126
        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 90
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
161
    {
162 90
        $indexBuffer = [];
163 90
        foreach ($tableIndexes as $tableIndex) {
164 30
            $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
165
166 30
            $keyName = strtolower($tableIndex['name']);
167 30
            $buffer  = [];
168
169 30
            if (strtolower($tableIndex['is_primary']) === 'p') {
170 26
                $keyName              = 'primary';
171 26
                $buffer['primary']    = true;
172 26
                $buffer['non_unique'] = false;
173
            } else {
174 24
                $buffer['primary']    = false;
175 24
                $buffer['non_unique'] = ! $tableIndex['is_unique'];
176
            }
177 30
            $buffer['key_name']    = $keyName;
178 30
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
179 30
            $indexBuffer[]         = $buffer;
180
        }
181
182 90
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 104
    protected function _getPortableTableColumnDefinition($tableColumn)
189
    {
190 104
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
191
192 104
        $dbType = strtolower($tableColumn['data_type']);
193 104
        if (strpos($dbType, 'timestamp(') === 0) {
194 14
            if (strpos($dbType, 'with time zone')) {
195 6
                $dbType = 'timestamptz';
196
            } else {
197 14
                $dbType = 'timestamp';
198
            }
199
        }
200
201 104
        $unsigned = $fixed = $precision = $scale = $length = null;
202
203 104
        if (! isset($tableColumn['column_name'])) {
204
            $tableColumn['column_name'] = '';
205
        }
206
207
        // Default values returned from database sometimes have trailing spaces.
208 104
        $tableColumn['data_default'] = trim($tableColumn['data_default']);
209
210 104
        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
211 98
            $tableColumn['data_default'] = null;
212
        }
213
214 104
        if ($tableColumn['data_default'] !== null) {
215
            // Default values returned from database are represented as literal expressions
216 54
            if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches)) {
217 52
                $tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
218
            }
219
        }
220
221 104
        if ($tableColumn['data_precision'] !== null) {
222 96
            $precision = (int) $tableColumn['data_precision'];
223
        }
224
225 104
        if ($tableColumn['data_scale'] !== null) {
226 98
            $scale = (int) $tableColumn['data_scale'];
227
        }
228
229 104
        $type                    = $this->_platform->getDoctrineTypeMapping($dbType);
230 104
        $type                    = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
231 104
        $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);
232
233 104
        switch ($dbType) {
234 104
            case 'number':
235 96
                if ($precision === 20 && $scale === 0) {
236
                    $type = 'bigint';
237 96
                } elseif ($precision === 5 && $scale === 0) {
238
                    $type = 'smallint';
239 96
                } elseif ($precision === 1 && $scale === 0) {
240 2
                    $type = 'boolean';
241 94
                } elseif ($scale > 0) {
242 10
                    $type = 'decimal';
243
                }
244
245 96
                break;
246 72
            case 'varchar':
247 72
            case 'varchar2':
248 28
            case 'nvarchar2':
249 56
                $length = $tableColumn['char_length'];
250 56
                $fixed  = false;
251 56
                break;
252 28
            case 'char':
253 24
            case 'nchar':
254 10
                $length = $tableColumn['char_length'];
255 10
                $fixed  = true;
256 10
                break;
257
        }
258
259
        $options = [
260 104
            'notnull'    => (bool) ($tableColumn['nullable'] === 'N'),
261 104
            'fixed'      => (bool) $fixed,
262 104
            'unsigned'   => (bool) $unsigned,
263 104
            'default'    => $tableColumn['data_default'],
264 104
            'length'     => $length,
265 104
            'precision'  => $precision,
266 104
            'scale'      => $scale,
267 104
            'comment'    => isset($tableColumn['comments']) && $tableColumn['comments'] !== ''
268 34
                ? $tableColumn['comments']
269
                : null,
270
        ];
271
272 104
        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 82
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
279
    {
280 82
        $list = [];
281 82
        foreach ($tableForeignKeys as $value) {
282 18
            $value = array_change_key_case($value, CASE_LOWER);
283 18
            if (! isset($list[$value['constraint_name']])) {
284 18
                if ($value['delete_rule'] === 'NO ACTION') {
285 16
                    $value['delete_rule'] = null;
286
                }
287
288 18
                $list[$value['constraint_name']] = [
289 18
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
290
                    'local' => [],
291
                    'foreign' => [],
292 18
                    'foreignTable' => $value['references_table'],
293 18
                    'onDelete' => $value['delete_rule'],
294
                ];
295
            }
296
297 18
            $localColumn   = $this->getQuotedIdentifierName($value['local_column']);
298 18
            $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);
299
300 18
            $list[$value['constraint_name']]['local'][$value['position']]   = $localColumn;
301 18
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
302
        }
303
304 82
        $result = [];
305 82
        foreach ($list as $constraint) {
306 18
            $result[] = new ForeignKeyConstraint(
307 18
                array_values($constraint['local']),
308 18
                $this->getQuotedIdentifierName($constraint['foreignTable']),
309 18
                array_values($constraint['foreign']),
310 18
                $this->getQuotedIdentifierName($constraint['name']),
311 18
                ['onDelete' => $constraint['onDelete']]
312
            );
313
        }
314
315 82
        return $result;
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321 9
    protected function _getPortableSequenceDefinition($sequence)
322
    {
323 9
        $sequence = array_change_key_case($sequence, CASE_LOWER);
324
325 9
        return new Sequence(
326 9
            $this->getQuotedIdentifierName($sequence['sequence_name']),
327 9
            (int) $sequence['increment_by'],
328 9
            (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 224
    public function dropAutoincrement($table)
378
    {
379 224
        assert($this->_platform instanceof OraclePlatform);
380
381 224
        $sql = $this->_platform->getDropAutoincrementSql($table);
382 224
        foreach ($sql as $query) {
383 224
            $this->_conn->executeUpdate($query);
384
        }
385
386
        return true;
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392 224
    public function dropTable($name)
393
    {
394 224
        $this->tryMethod('dropAutoincrement', $name);
395
396 224
        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 165
    private function getQuotedIdentifierName($identifier)
410
    {
411 165
        if (preg_match('/[a-z]/', $identifier)) {
412 103
            return $this->_platform->quoteIdentifier($identifier);
413
        }
414
415 165
        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