Failed Conditions
Push — develop ( 152bc9...e39bc0 )
by Sergei
102:42 queued 37:39
created

SQLServerSchemaManager::listTableIndexes()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7.3471

Importance

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