Completed
Pull Request — master (#3512)
by David
61:26
created

SQLServerSchemaManager   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 91.34%

Importance

Changes 0
Metric Value
wmc 50
eloc 122
dl 0
loc 316
ccs 116
cts 127
cp 0.9134
rs 8.4
c 0
b 0
f 0

16 Methods

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