Failed Conditions
Push — master ( 01c22b...e42c1f )
by Marco
79:13 queued 10s
created

SQLServerSchemaManager   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Test Coverage

Coverage 91.3%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 54
eloc 130
dl 0
loc 321
ccs 126
cts 138
cp 0.913
rs 6.4799
c 1
b 0
f 0

16 Methods

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

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