Completed
Pull Request — master (#3429)
by Gabriel
13:29
created

SQLServerSchemaManager   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Test Coverage

Coverage 23.77%

Importance

Changes 0
Metric Value
wmc 48
eloc 113
dl 0
loc 286
ccs 29
cts 122
cp 0.2377
rs 8.5599
c 0
b 0
f 0

14 Methods

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