Passed
Pull Request — 2.11.x (#3971)
by Grégoire
03:18
created

_getPortableTableIndexesList()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

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