Failed Conditions
Push — 3.0.x ( 655b6b...430dce )
by Grégoire
16:57 queued 12:22
created

SQLServerSchemaManager   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Test Coverage

Coverage 91.55%

Importance

Changes 0
Metric Value
eloc 129
dl 0
loc 329
ccs 130
cts 142
cp 0.9155
rs 6.96
c 0
b 0
f 0
wmc 53

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