_getPortableViewDefinition()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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