Completed
Pull Request — develop (#3533)
by
unknown
16:31 queued 01:26
created

SQLServerSchemaManager::listTableIndexes()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 12.4085

Importance

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