Failed Conditions
Pull Request — master (#2766)
by mon
09:56
created

OracleSchemaManager::getAssetRecordsByTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 6
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Driver\DriverException;
24
use Doctrine\DBAL\Types\Type;
25
26
/**
27
 * Oracle Schema Manager.
28
 *
29
 * @author Konsta Vesterinen <[email protected]>
30
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
31
 * @author Benjamin Eberlei <[email protected]>
32
 * @since  2.0
33
 */
34
class OracleSchemaManager extends AbstractSchemaManager
35
{
36
    /**
37
     * Holds instance of the database platform used for this schema manager.
38
     *
39
     * @var \Doctrine\DBAL\Platforms\OraclePlatform
40
     */
41
    protected $_platform;
42
43
    /**
44
     * {@inheritdoc}
45
     */
46
    public function listTables()
47
    {
48
        $currentDatabase = $this->_conn->getDatabase();
49
50
        $tableNames = $this->listTableNames();
51
52
        // Get all column definitions in one database call.
53
        $columnsByTable = $this->getAssetRecordsByTable($this->_platform->getListAllColumnsSQL($currentDatabase));
54
55
        // Get all foreign keys definitions in one database call.
56
        $foreignKeysByTable = $this->getAssetRecordsByTable($this->_platform->getListAllForeignKeysSQL($currentDatabase));
57
58
        // Get all indexes definitions in one database call.
59
        $indexesByTable = $this->getAssetRecordsByTable($this->_platform->getListAllIndexesSQL($currentDatabase));
60
61
        $tables = [];
62
        foreach ($tableNames as $quotedTableName) {
63
            $tableName = trim($quotedTableName, $this->_platform->getIdentifierQuoteCharacter());
64
65
            $columns = $this->_getPortableTableColumnList($quotedTableName, null, $columnsByTable[$tableName]);
66
67
            $foreignKeys = [];
68
            if (isset($foreignKeysByTable[$tableName])) {
69
                $foreignKeys = $this->_getPortableTableForeignKeysList($foreignKeysByTable[$tableName]);
70
            }
71
72
            $indexes = [];
73
            if (isset($indexesByTable[$tableName])) {
74
                $indexes = $this->_getPortableTableIndexesList($indexesByTable[$tableName], $quotedTableName);
75
            }
76
77
            $tables[] = new Table($quotedTableName, $columns, $indexes, $foreignKeys, false, []);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $idGeneratorType of Doctrine\DBAL\Schema\Table::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

77
            $tables[] = new Table($quotedTableName, $columns, $indexes, $foreignKeys, /** @scrutinizer ignore-type */ false, []);
Loading history...
78
        }
79
80
        return $tables;
81
    }
82
83
    /**
84
     * Helper method to group a set of asset records by the table name.
85
     *
86
     * @param string $sql An SQL statement to be executed, that contains a
87
     *                    TABLE_NAME field for grouping.
88
     *
89
     * @return array An associative array with key being the table name, and
90
     *               value a simple array of records associated with the table.
91
     */
92
    private function getAssetRecordsByTable(string $sql): array
93
    {
94
        $input = $this->_conn->fetchAll($sql);
95
        $output = [];
96
        foreach ($input as $record) {
97
            $output[$record['TABLE_NAME']][] = $record;
98
        }
99
        return $output;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 View Code Duplication
    public function dropDatabase($database)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
106
    {
107
        try {
108
            parent::dropDatabase($database);
109
        } catch (DBALException $exception) {
110
            $exception = $exception->getPrevious();
111
112
            if (! $exception instanceof DriverException) {
113
                throw $exception;
114
            }
115
116
            // If we have a error code 1940 (ORA-01940), the drop database operation failed
117
            // because of active connections on the database.
118
            // To force dropping the database, we first have to close all active connections
119
            // on that database and issue the drop database operation again.
120
            if ($exception->getErrorCode() !== 1940) {
121
                throw $exception;
122
            }
123
124
            $this->killUserSessions($database);
125
126
            parent::dropDatabase($database);
127
        }
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    protected function _getPortableViewDefinition($view)
134
    {
135
        $view = \array_change_key_case($view, CASE_LOWER);
136
137
        return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    protected function _getPortableUserDefinition($user)
144
    {
145
        $user = \array_change_key_case($user, CASE_LOWER);
146
147
        return [
148
            'user' => $user['username'],
149
        ];
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    protected function _getPortableTableDefinition($table)
156
    {
157
        $table = \array_change_key_case($table, CASE_LOWER);
158
159
        return $this->getQuotedIdentifierName($table['table_name']);
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     *
165
     * @license New BSD License
166
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
167
     */
168
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
169
    {
170
        $indexBuffer = [];
171
        foreach ($tableIndexes as $tableIndex) {
172
            $tableIndex = \array_change_key_case($tableIndex, CASE_LOWER);
173
174
            $keyName = strtolower($tableIndex['name']);
175
176
            if (strtolower($tableIndex['is_primary']) == "p") {
177
                $keyName = 'primary';
178
                $buffer['primary'] = true;
179
                $buffer['non_unique'] = false;
180
            } else {
181
                $buffer['primary'] = false;
182
                $buffer['non_unique'] = ($tableIndex['is_unique'] == 0) ? true : false;
183
            }
184
            $buffer['key_name'] = $keyName;
185
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
186
            $indexBuffer[] = $buffer;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $buffer seems to be defined later in this foreach loop on line 178. Are you sure it is defined here?
Loading history...
187
        }
188
189
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    protected function _getPortableTableColumnDefinition($tableColumn)
196
    {
197
        $tableColumn = \array_change_key_case($tableColumn, CASE_LOWER);
198
199
        $dbType = strtolower($tableColumn['data_type']);
200
        if (strpos($dbType, "timestamp(") === 0) {
201
            if (strpos($dbType, "with time zone")) {
202
                $dbType = "timestamptz";
203
            } else {
204
                $dbType = "timestamp";
205
            }
206
        }
207
208
        $unsigned = $fixed = null;
209
210
        if ( ! isset($tableColumn['column_name'])) {
211
            $tableColumn['column_name'] = '';
212
        }
213
214
        // Default values returned from database sometimes have trailing spaces.
215
        $tableColumn['data_default'] = trim($tableColumn['data_default']);
216
217 View Code Duplication
        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
            $tableColumn['data_default'] = null;
219
        }
220
221 View Code Duplication
        if (null !== $tableColumn['data_default']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
222
            // Default values returned from database are enclosed in single quotes.
223
            $tableColumn['data_default'] = trim($tableColumn['data_default'], "'");
224
        }
225
226
        $precision = null;
227
        $scale = null;
228
229
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
230
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
231
        $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);
232
233
        switch ($dbType) {
234
            case 'number':
235
                if ($tableColumn['data_precision'] == 20 && $tableColumn['data_scale'] == 0) {
236
                    $precision = 20;
237
                    $scale = 0;
238
                    $type = 'bigint';
239
                } elseif ($tableColumn['data_precision'] == 5 && $tableColumn['data_scale'] == 0) {
240
                    $type = 'smallint';
241
                    $precision = 5;
242
                    $scale = 0;
243
                } elseif ($tableColumn['data_precision'] == 1 && $tableColumn['data_scale'] == 0) {
244
                    $precision = 1;
245
                    $scale = 0;
246
                    $type = 'boolean';
247
                } elseif ($tableColumn['data_scale'] > 0) {
248
                    $precision = $tableColumn['data_precision'];
249
                    $scale = $tableColumn['data_scale'];
250
                    $type = 'decimal';
251
                }
252
                $length = null;
253
                break;
254
            case 'pls_integer':
255
            case 'binary_integer':
256
                $length = null;
257
                break;
258
            case 'varchar':
259
            case 'varchar2':
260
            case 'nvarchar2':
261
                $length = $tableColumn['char_length'];
262
                $fixed = false;
263
                break;
264
            case 'char':
265
            case 'nchar':
266
                $length = $tableColumn['char_length'];
267
                $fixed = true;
268
                break;
269
            case 'date':
270
            case 'timestamp':
271
                $length = null;
272
                break;
273
            case 'float':
274
            case 'binary_float':
275
            case 'binary_double':
276
                $precision = $tableColumn['data_precision'];
277
                $scale = $tableColumn['data_scale'];
278
                $length = null;
279
                break;
280
            case 'clob':
281
            case 'nclob':
282
                $length = null;
283
                break;
284
            case 'blob':
285
            case 'raw':
286
            case 'long raw':
287
            case 'bfile':
288
                $length = null;
289
                break;
290
            case 'rowid':
291
            case 'urowid':
292
            default:
293
                $length = null;
294
        }
295
296
        $options = [
297
            'notnull'    => (bool) ($tableColumn['nullable'] === 'N'),
298
            'fixed'      => (bool) $fixed,
299
            'unsigned'   => (bool) $unsigned,
300
            'default'    => $tableColumn['data_default'],
301
            'length'     => $length,
302
            'precision'  => $precision,
303
            'scale'      => $scale,
304
            'comment'    => isset($tableColumn['comments']) && '' !== $tableColumn['comments']
305
                ? $tableColumn['comments']
306
                : null,
307
        ];
308
309
        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
316
    {
317
        $list = [];
318
        foreach ($tableForeignKeys as $value) {
319
            $value = \array_change_key_case($value, CASE_LOWER);
320
            if (!isset($list[$value['constraint_name']])) {
321
                if ($value['delete_rule'] == "NO ACTION") {
322
                    $value['delete_rule'] = null;
323
                }
324
325
                $list[$value['constraint_name']] = [
326
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
327
                    'local' => [],
328
                    'foreign' => [],
329
                    'foreignTable' => $value['references_table'],
330
                    'onDelete' => $value['delete_rule'],
331
                ];
332
            }
333
334
            $localColumn = $this->getQuotedIdentifierName($value['local_column']);
335
            $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);
336
337
            $list[$value['constraint_name']]['local'][$value['position']] = $localColumn;
338
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
339
        }
340
341
        $result = [];
342
        foreach ($list as $constraint) {
343
            $result[] = new ForeignKeyConstraint(
344
                array_values($constraint['local']), $this->getQuotedIdentifierName($constraint['foreignTable']),
345
                array_values($constraint['foreign']), $this->getQuotedIdentifierName($constraint['name']),
346
                ['onDelete' => $constraint['onDelete']]
347
            );
348
        }
349
350
        return $result;
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356
    protected function _getPortableSequenceDefinition($sequence)
357
    {
358
        $sequence = \array_change_key_case($sequence, CASE_LOWER);
359
360
        return new Sequence(
361
            $this->getQuotedIdentifierName($sequence['sequence_name']),
362
            $sequence['increment_by'],
363
            $sequence['min_value']
364
        );
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    protected function _getPortableFunctionDefinition($function)
371
    {
372
        $function = \array_change_key_case($function, CASE_LOWER);
373
374
        return $function['name'];
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     */
380
    protected function _getPortableDatabaseDefinition($database)
381
    {
382
        $database = \array_change_key_case($database, CASE_LOWER);
383
384
        return $database['username'];
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390
    public function createDatabase($database = null)
391
    {
392
        if (is_null($database)) {
393
            $database = $this->_conn->getDatabase();
394
        }
395
396
        $params = $this->_conn->getParams();
397
        $username   = $database;
398
        $password   = $params['password'];
399
400
        $query  = 'CREATE USER ' . $username . ' IDENTIFIED BY ' . $password;
401
        $this->_conn->executeUpdate($query);
402
403
        $query = 'GRANT DBA TO ' . $username;
404
        $this->_conn->executeUpdate($query);
405
406
        return true;
407
    }
408
409
    /**
410
     * @param string $table
411
     *
412
     * @return boolean
413
     */
414
    public function dropAutoincrement($table)
415
    {
416
        $sql = $this->_platform->getDropAutoincrementSql($table);
417
        foreach ($sql as $query) {
418
            $this->_conn->executeUpdate($query);
419
        }
420
421
        return true;
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function dropTable($name)
428
    {
429
        $this->tryMethod('dropAutoincrement', $name);
430
431
        parent::dropTable($name);
432
    }
433
434
    /**
435
     * Returns the quoted representation of the given identifier name.
436
     *
437
     * Quotes non-uppercase identifiers explicitly to preserve case
438
     * and thus make references to the particular identifier work.
439
     *
440
     * @param string $identifier The identifier to quote.
441
     *
442
     * @return string The quoted identifier.
443
     */
444
    private function getQuotedIdentifierName($identifier)
445
    {
446
        if (preg_match('/[a-z]/', $identifier)) {
447
            return $this->_platform->quoteIdentifier($identifier);
448
        }
449
450
        return $identifier;
451
    }
452
453
    /**
454
     * Kills sessions connected with the given user.
455
     *
456
     * This is useful to force DROP USER operations which could fail because of active user sessions.
457
     *
458
     * @param string $user The name of the user to kill sessions for.
459
     *
460
     * @return void
461
     */
462
    private function killUserSessions($user)
463
    {
464
        $sql = <<<SQL
465
SELECT
466
    s.sid,
467
    s.serial#
468
FROM
469
    gv\$session s,
470
    gv\$process p
471
WHERE
472
    s.username = ?
473
    AND p.addr(+) = s.paddr
474
SQL;
475
476
        $activeUserSessions = $this->_conn->fetchAll($sql, [strtoupper($user)]);
477
478
        foreach ($activeUserSessions as $activeUserSession) {
479
            $activeUserSession = array_change_key_case($activeUserSession, \CASE_LOWER);
480
481
            $this->_execSql(
482
                sprintf(
483
                    "ALTER SYSTEM KILL SESSION '%s, %s' IMMEDIATE",
484
                    $activeUserSession['sid'],
485
                    $activeUserSession['serial#']
486
                )
487
            );
488
        }
489
    }
490
}
491