Failed Conditions
Pull Request — develop (#3581)
by Jonathan
12:44
created

closeActiveDatabaseConnections()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1.0046

Importance

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