Failed Conditions
Pull Request — develop (#3518)
by Michael
29:00 queued 25:29
created

OracleSchemaManager::createDatabase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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