Failed Conditions
Pull Request — develop (#3348)
by Sergei
60:44
created

SQLServerSchemaManager   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Test Coverage

Coverage 25.95%

Importance

Changes 0
Metric Value
wmc 53
eloc 125
dl 0
loc 308
ccs 34
cts 131
cp 0.2595
rs 6.96
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A dropDatabase() 0 23 4
A _getPortableSequenceDefinition() 0 3 1
A _getPortableTableDefinition() 0 7 3
A closeActiveDatabaseConnections() 0 8 1
A _getPortableTableForeignKeysList() 0 23 3
F _getPortableTableColumnDefinition() 0 72 19
A _getPortableTableForeignKeyDefinition() 0 8 1
A parseDefaultExpression() 0 19 5
A _getPortableTableIndexesList() 0 9 3
A getPortableNamespaceDefinition() 0 3 1
A alterTable() 0 18 4
A _getPortableDatabaseDefinition() 0 3 1
A _getPortableViewDefinition() 0 4 1
A listTableIndexes() 0 21 5
A getColumnConstraintSQL() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like SQLServerSchemaManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SQLServerSchemaManager, and based on these observations, apply Extract Interface, too.

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\Types\Type;
10
use PDOException;
11
use Throwable;
12
use function assert;
13
use function count;
14
use function in_array;
15
use function is_string;
16
use function preg_match;
17
use function sprintf;
18
use function str_replace;
19
use function strpos;
20
use function strtok;
21
22
/**
23
 * SQL Server Schema Manager.
24
 */
25
class SQLServerSchemaManager extends AbstractSchemaManager
26
{
27
    /**
28
     * {@inheritdoc}
29
     */
30
    public function dropDatabase(string $database) : void
31
    {
32
        try {
33
            parent::dropDatabase($database);
34
        } catch (DBALException $exception) {
35
            $exception = $exception->getPrevious();
36
            assert($exception instanceof Throwable);
37
38
            if (! $exception instanceof DriverException) {
39
                throw $exception;
40
            }
41
42
            // If we have a error code 3702, 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
            if ($exception->getCode() !== 3702) {
47
                throw $exception;
48
            }
49
50
            $this->closeActiveDatabaseConnections($database);
51
52
            parent::dropDatabase($database);
53
        }
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    protected function _getPortableSequenceDefinition(array $sequence) : Sequence
60
    {
61
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    protected function _getPortableTableColumnDefinition(array $tableColumn) : Column
68 27
    {
69
        $dbType = strtok($tableColumn['type'], '(), ');
70 27
        assert(is_string($dbType));
71 27
72
        $length = (int) $tableColumn['length'];
73 27
74
        $precision = $default = null;
75 27
76
        $scale = 0;
77 27
        $fixed = false;
78 27
79
        if (! isset($tableColumn['name'])) {
80 27
            $tableColumn['name'] = '';
81 27
        }
82
83
        if ($tableColumn['scale'] !== null) {
84 27
            $scale = (int) $tableColumn['scale'];
85 27
        }
86
87
        if ($tableColumn['precision'] !== null) {
88 27
            $precision = (int) $tableColumn['precision'];
89 27
        }
90
91
        if ($tableColumn['default'] !== null) {
92 27
            $default = $this->parseDefaultExpression($tableColumn['default']);
93
        }
94
95
        switch ($dbType) {
96 27
            case 'nchar':
97 2
            case 'nvarchar':
98 2
            case 'ntext':
99 2
                // Unicode data requires 2 bytes per character
100
                $length /= 2;
101
                break;
102
            case 'varchar':
103 2
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
104
                if ($length === -1) {
105
                    $dbType = 'text';
106
                }
107
                break;
108
        }
109
110
        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
111 27
            $fixed = true;
112
        }
113
114
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'])
115 27
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
116 27
117
        $options = [
118
            'length'        => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
119 27
            'fixed'         => $fixed,
120 27
            'default'       => $default,
121 27
            'notnull'       => (bool) $tableColumn['notnull'],
122 27
            'scale'         => $scale,
123 27
            'precision'     => $precision,
124 27
            'autoincrement' => (bool) $tableColumn['autoincrement'],
125 27
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
126 27
        ];
127
128
        if ($fixed !== null) {
0 ignored issues
show
introduced by
The condition $fixed !== null is always true.
Loading history...
129 27
            $options['fixed'] = $fixed;
130
        }
131 27
132 27
        $column = new Column($tableColumn['name'], Type::getType($type), $options);
133
134
        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
135 27
            $column->setPlatformOption('collation', $tableColumn['collation']);
136
        }
137
138
        return $column;
139
    }
140
141
    private function parseDefaultExpression(string $value) : ?string
142
    {
143
        while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
144
            $value = $matches[1];
145
        }
146
147
        if ($value === 'NULL') {
148
            return null;
149
        }
150
151
        if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
152
            $value = str_replace("''", "'", $matches[1]);
153
        }
154
155
        if ($value === 'getdate()') {
156
            return $this->_platform->getCurrentTimestampSQL();
157
        }
158
159
        return $value;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    protected function _getPortableTableForeignKeysList(array $tableForeignKeys) : array
166
    {
167
        $foreignKeys = [];
168
169
        foreach ($tableForeignKeys as $tableForeignKey) {
170
            if (! isset($foreignKeys[$tableForeignKey['ForeignKey']])) {
171
                $foreignKeys[$tableForeignKey['ForeignKey']] = [
172
                    'local_columns' => [$tableForeignKey['ColumnName']],
173
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
174
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
175
                    'name' => $tableForeignKey['ForeignKey'],
176
                    'options' => [
177
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
178
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
179
                    ],
180
                ];
181
            } else {
182
                $foreignKeys[$tableForeignKey['ForeignKey']]['local_columns'][]   = $tableForeignKey['ColumnName'];
183
                $foreignKeys[$tableForeignKey['ForeignKey']]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
184
            }
185
        }
186
187
        return parent::_getPortableTableForeignKeysList($foreignKeys);
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
194
    {
195
        foreach ($tableIndexRows as &$tableIndex) {
196
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
197
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
198
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
199
        }
200
201
        return parent::_getPortableTableIndexesList($tableIndexRows, $tableName);
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey) : ForeignKeyConstraint
208
    {
209
        return new ForeignKeyConstraint(
210
            $tableForeignKey['local_columns'],
211
            $tableForeignKey['foreign_table'],
212
            $tableForeignKey['foreign_columns'],
213
            $tableForeignKey['name'],
214
            $tableForeignKey['options']
215
        );
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    protected function _getPortableTableDefinition($table) : string
222
    {
223
        if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
224
            return $table['schema_name'] . '.' . $table['name'];
225
        }
226
227
        return $table['name'];
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    protected function _getPortableDatabaseDefinition($database)
234
    {
235
        return $database['name'];
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241
    protected function getPortableNamespaceDefinition(array $namespace)
242
    {
243
        return $namespace['name'];
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    protected function _getPortableViewDefinition(array $view) : View
250
    {
251
        // @todo
252
        return new View($view['name'], '');
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function listTableIndexes(string $table) : array
259
    {
260
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());
261
262
        try {
263
            $tableIndexes = $this->_conn->fetchAll($sql);
264
        } catch (PDOException $e) {
265
            if ($e->getCode() === 'IMSSP') {
266
                return [];
267
            }
268
269
            throw $e;
270
        } catch (DBALException $e) {
271
            if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
272
                return [];
273
            }
274
275
            throw $e;
276
        }
277
278
        return $this->_getPortableTableIndexesList($tableIndexes, $table);
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function alterTable(TableDiff $tableDiff) : void
285
    {
286
        if (count($tableDiff->removedColumns) > 0) {
287
            foreach ($tableDiff->removedColumns as $col) {
288
                $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
289
                foreach ($this->_conn->fetchAll($columnConstraintSql) as $constraint) {
290
                    $this->_conn->exec(
291
                        sprintf(
292
                            'ALTER TABLE %s DROP CONSTRAINT %s',
293
                            $tableDiff->name,
294
                            $constraint['Name']
295
                        )
296
                    );
297
                }
298
            }
299
        }
300
301
        parent::alterTable($tableDiff);
302
    }
303
304
    /**
305
     * Returns the SQL to retrieve the constraints for a given column.
306
     */
307
    private function getColumnConstraintSQL(string $table, string $column) : string
308
    {
309
        return "SELECT SysObjects.[Name]
310
            FROM SysObjects INNER JOIN (SELECT [Name],[ID] FROM SysObjects WHERE XType = 'U') AS Tab
311
            ON Tab.[ID] = Sysobjects.[Parent_Obj]
312
            INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = Sysobjects.[ID]
313
            INNER JOIN SysColumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
314
            WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
315
            ORDER BY Col.[Name]';
316
    }
317
318
    /**
319
     * Closes currently active connections on the given database.
320
     *
321
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
322
     *
323
     * @param string $database The name of the database to close currently active connections for.
324
     */
325
    private function closeActiveDatabaseConnections(string $database) : void
326
    {
327
        $database = new Identifier($database);
328
329
        $this->_execSql(
330
            sprintf(
331
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
332
                $database->getQuotedName($this->_platform)
333
            )
334
        );
335
    }
336
}
337