Failed Conditions
Push — master ( 94cec7...7f79d0 )
by Marco
25s queued 13s
created

SQLServerSchemaManager::parseDefaultExpression()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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