Failed Conditions
Push — master ( 4d0d2a...06d1d8 )
by Marco
22s queued 16s
created

SQLServerSchemaManager   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Test Coverage

Coverage 90.77%

Importance

Changes 0
Metric Value
wmc 50
eloc 120
dl 0
loc 302
ccs 118
cts 130
cp 0.9077
rs 8.4
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getColumnConstraintSQL() 0 8 1
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 59 16
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

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