SQLServerSchemaManager.php 9.78 KB
Newer Older
romanb's avatar
romanb committed
1 2
<?php

3
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
4

5 6
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\DriverException;
7
use Doctrine\DBAL\Types\Type;
8
use PDOException;
9 10 11 12 13 14 15 16
use function count;
use function in_array;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strpos;
use function strtok;
use function trim;
17

romanb's avatar
romanb committed
18
/**
Benjamin Morel's avatar
Benjamin Morel committed
19
 * SQL Server Schema Manager.
romanb's avatar
romanb committed
20
 */
21
class SQLServerSchemaManager extends AbstractSchemaManager
22
{
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();

            if (! $exception instanceof DriverException) {
                throw $exception;
            }

            // If we have a error code 3702, the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
            if ($exception->getErrorCode() !== 3702) {
                throw $exception;
            }

            $this->closeActiveDatabaseConnections($database);

            parent::dropDatabase($database);
        }
    }

51 52 53 54 55
    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
56
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
57 58
    }

romanb's avatar
romanb committed
59
    /**
60
     * {@inheritdoc}
romanb's avatar
romanb committed
61
     */
62
    protected function _getPortableTableColumnDefinition($tableColumn)
romanb's avatar
romanb committed
63
    {
64 65 66
        $dbType  = strtok($tableColumn['type'], '(), ');
        $fixed   = null;
        $length  = (int) $tableColumn['length'];
67
        $default = $tableColumn['default'];
romanb's avatar
romanb committed
68

69
        if (! isset($tableColumn['name'])) {
70
            $tableColumn['name'] = '';
romanb's avatar
romanb committed
71
        }
72

73 74 75
        if ($default !== null) {
            while ($default !== ($default2 = preg_replace('/^\((.*)\)$/', '$1', $default))) {
                $default = trim($default2, "'");
76

77 78 79
                if ($default !== 'getdate()') {
                    continue;
                }
80

81 82
                $default = $this->_platform->getCurrentTimestampSQL();
            }
83
        }
84

85 86 87 88 89
        switch ($dbType) {
            case 'nchar':
            case 'nvarchar':
            case 'ntext':
                // Unicode data requires 2 bytes per character
90
                $length /= 2;
91 92 93
                break;
            case 'varchar':
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
94
                if ($length === -1) {
95 96 97 98
                    $dbType = 'text';
                }
                break;
        }
99

100
        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
Steve Müller's avatar
Steve Müller committed
101 102 103
            $fixed = true;
        }

104 105 106
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
107

108
        $options = [
109
            'length'        => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
110 111 112 113 114 115
            'unsigned'      => false,
            'fixed'         => (bool) $fixed,
            'default'       => $default !== 'NULL' ? $default : null,
            'notnull'       => (bool) $tableColumn['notnull'],
            'scale'         => $tableColumn['scale'],
            'precision'     => $tableColumn['precision'],
116
            'autoincrement' => (bool) $tableColumn['autoincrement'],
117
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
118
        ];
119

120
        $column = new Column($tableColumn['name'], Type::getType($type), $options);
121 122 123 124

        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
            $column->setPlatformOption('collation', $tableColumn['collation']);
        }
125 126

        return $column;
romanb's avatar
romanb committed
127 128
    }

129 130 131 132 133
    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
134
        $foreignKeys = [];
135 136

        foreach ($tableForeignKeys as $tableForeignKey) {
137
            if (! isset($foreignKeys[$tableForeignKey['ForeignKey']])) {
138 139
                $foreignKeys[$tableForeignKey['ForeignKey']] = [
                    'local_columns' => [$tableForeignKey['ColumnName']],
140
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
141
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
142
                    'name' => $tableForeignKey['ForeignKey'],
143
                    'options' => [
144
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
145 146
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
                    ],
147
                ];
148
            } else {
149
                $foreignKeys[$tableForeignKey['ForeignKey']]['local_columns'][]   = $tableForeignKey['ColumnName'];
150 151 152 153 154 155 156
                $foreignKeys[$tableForeignKey['ForeignKey']]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
            }
        }

        return parent::_getPortableTableForeignKeysList($foreignKeys);
    }

romanb's avatar
romanb committed
157
    /**
158
     * {@inheritdoc}
romanb's avatar
romanb committed
159
     */
160
    protected function _getPortableTableIndexesList($tableIndexRows, $tableName = null)
romanb's avatar
romanb committed
161
    {
162
        foreach ($tableIndexRows as &$tableIndex) {
163 164 165
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
166
        }
romanb's avatar
romanb committed
167

168
        return parent::_getPortableTableIndexesList($tableIndexRows, $tableName);
romanb's avatar
romanb committed
169 170 171
    }

    /**
172
     * {@inheritdoc}
romanb's avatar
romanb committed
173
     */
174
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
romanb's avatar
romanb committed
175
    {
176
        return new ForeignKeyConstraint(
177 178 179 180 181
            $tableForeignKey['local_columns'],
            $tableForeignKey['foreign_table'],
            $tableForeignKey['foreign_columns'],
            $tableForeignKey['name'],
            $tableForeignKey['options']
182
        );
romanb's avatar
romanb committed
183 184 185
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
186
     * {@inheritdoc}
romanb's avatar
romanb committed
187
     */
188
    protected function _getPortableTableDefinition($table)
romanb's avatar
romanb committed
189
    {
190 191 192 193
        if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
            return $table['schema_name'] . '.' . $table['name'];
        }

194
        return $table['name'];
romanb's avatar
romanb committed
195
    }
196 197

    /**
Benjamin Morel's avatar
Benjamin Morel committed
198
     * {@inheritdoc}
199
     */
200
    protected function _getPortableDatabaseDefinition($database)
201 202 203
    {
        return $database['name'];
    }
204

205 206 207 208 209 210 211 212
    /**
     * {@inheritdoc}
     */
    protected function getPortableNamespaceDefinition(array $namespace)
    {
        return $namespace['name'];
    }

213
    /**
Benjamin Morel's avatar
Benjamin Morel committed
214
     * {@inheritdoc}
215
     */
216
    protected function _getPortableViewDefinition($view)
217
    {
218
        // @todo
219 220
        return new View($view['name'], null);
    }
221

222
    /**
Benjamin Morel's avatar
Benjamin Morel committed
223
     * {@inheritdoc}
224 225 226 227 228 229 230
     */
    public function listTableIndexes($table)
    {
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());

        try {
            $tableIndexes = $this->_conn->fetchAll($sql);
231 232
        } catch (PDOException $e) {
            if ($e->getCode() === 'IMSSP') {
233
                return [];
234
            }
235 236

            throw $e;
237
        } catch (DBALException $e) {
238
            if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
239
                return [];
240
            }
241 242

            throw $e;
243 244 245 246
        }

        return $this->_getPortableTableIndexesList($tableIndexes, $table);
    }
247 248

    /**
Benjamin Morel's avatar
Benjamin Morel committed
249
     * {@inheritdoc}
250 251 252
     */
    public function alterTable(TableDiff $tableDiff)
    {
Steve Müller's avatar
Steve Müller committed
253 254
        if (count($tableDiff->removedColumns) > 0) {
            foreach ($tableDiff->removedColumns as $col) {
255 256
                $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
                foreach ($this->_conn->fetchAll($columnConstraintSql) as $constraint) {
257 258 259 260 261 262 263
                    $this->_conn->exec(
                        sprintf(
                            'ALTER TABLE %s DROP CONSTRAINT %s',
                            $tableDiff->name,
                            $constraint['Name']
                        )
                    );
264 265 266 267
                }
            }
        }

Benjamin Morel's avatar
Benjamin Morel committed
268
        parent::alterTable($tableDiff);
269 270 271
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
272 273 274 275 276 277
     * Returns the SQL to retrieve the constraints for a given column.
     *
     * @param string $table
     * @param string $column
     *
     * @return string
278 279 280 281 282 283 284 285
     */
    private function getColumnConstraintSQL($table, $column)
    {
        return "SELECT SysObjects.[Name]
            FROM SysObjects INNER JOIN (SELECT [Name],[ID] FROM SysObjects WHERE XType = 'U') AS Tab
            ON Tab.[ID] = Sysobjects.[Parent_Obj]
            INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = Sysobjects.[ID]
            INNER JOIN SysColumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
286 287
            WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
            ORDER BY Col.[Name]';
288
    }
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

    /**
     * Closes currently active connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @param string $database The name of the database to close currently active connections for.
     *
     * @return void
     */
    private function closeActiveDatabaseConnections($database)
    {
        $database = new Identifier($database);

        $this->_execSql(
            sprintf(
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
                $database->getQuotedName($this->_platform)
            )
        );
    }
310
}