Skip to content

Commit

Permalink
Introspect default constraint name
Browse files Browse the repository at this point in the history
  • Loading branch information
morozov committed Sep 11, 2022
1 parent 2f562d6 commit c8d2c3d
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 116 deletions.
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ The following classes have been converted to enums:

The corresponding class constants are now instances of their enum type.

## BC BREAK: dropped naming convention for default constraints on SQL Server

The DBAL no longer generates default constraint names using the table name and column name. The name is now generated
by the database.

## BC BREAK: renamed SQLite platform classes

1. `SqlitePlatform` => `SQLitePlatform`
Expand Down
88 changes: 28 additions & 60 deletions src/Platforms/SQLServerPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
*/
class SQLServerPlatform extends AbstractPlatform
{
/** @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. */
public const OPTION_DEFAULT_CONSTRAINT_NAME = 'default_constraint_name';

public function getCurrentDateSQL(): string
{
return $this->getConvertExpression('date', 'GETDATE()');
Expand Down Expand Up @@ -182,7 +185,7 @@ protected function _getCreateTableSQL(string $name, array $columns, array $optio
// Build default constraints SQL statements.
if (isset($column['default'])) {
$defaultConstraintsSql[] = 'ALTER TABLE ' . $name .
' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column);
' ADD' . $this->getDefaultConstraintDeclarationSQL($column);
}

if (empty($column['comment']) && ! is_numeric($column['comment'])) {
Expand Down Expand Up @@ -288,23 +291,19 @@ protected function getCreateColumnCommentSQL(string $tableName, string $columnNa
/**
* Returns the SQL snippet for declaring a default constraint.
*
* @param string $table Name of the table to return the default constraint declaration for.
* @param mixed[] $column Column definition.
*
* @throws InvalidArgumentException
*/
protected function getDefaultConstraintDeclarationSQL(string $table, array $column): string
protected function getDefaultConstraintDeclarationSQL(array $column): string
{
if (! isset($column['default'])) {
throw new InvalidArgumentException('Incomplete column definition. "default" required.');
}

$columnName = new Identifier($column['name']);

return ' CONSTRAINT ' .
$this->generateDefaultConstraintName($table, $column['name']) .
$this->getDefaultValueDeclarationSQL($column) .
' FOR ' . $columnName->getQuotedName($this);
return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this);
}

public function getCreateIndexSQL(Index $index, string $table): string
Expand Down Expand Up @@ -368,9 +367,7 @@ public function getAlterTableSQL(TableDiff $diff): array
$addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties);

if (isset($columnProperties['default'])) {
$addColumnSql .= ' CONSTRAINT ' .
$this->generateDefaultConstraintName($diff->name, $column->getQuotedName($this)) .
$this->getDefaultValueDeclarationSQL($columnProperties);
$addColumnSql .= $this->getDefaultValueDeclarationSQL($columnProperties);
}

$queryParts[] = $addColumnSql;
Expand All @@ -393,6 +390,10 @@ public function getAlterTableSQL(TableDiff $diff): array
continue;
}

if ($column->getDefault() !== null) {
$queryParts[] = $this->getAlterTableDropDefaultConstraintClause($column);
}

$queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
}

Expand Down Expand Up @@ -440,10 +441,7 @@ public function getAlterTableSQL(TableDiff $diff): array
$requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);

if ($requireDropDefaultConstraint) {
$queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
$diff->name,
$oldColumn->getName(),
);
$queryParts[] = $this->getAlterTableDropDefaultConstraintClause($oldColumn);
}

if ($declarationSQLChanged) {
Expand All @@ -467,20 +465,12 @@ public function getAlterTableSQL(TableDiff $diff): array

$oldColumnName = new Identifier($oldColumnName);

$sql[] = "sp_rename '" .
$diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
"', '" . $newColumn->getQuotedName($this) . "', 'COLUMN'";

// Recreate default constraint with new column name if necessary (for future reference).
if ($newColumn->getDefault() === null) {
continue;
}

$queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
$diff->name,
$sql[] = sprintf(
"sp_rename '%s.%s', '%s', 'COLUMN'",
$diff->getName($this)->getQuotedName($this),
$oldColumnName->getQuotedName($this),
$newColumn->getQuotedName($this)
);
$queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $newColumn);
}

$tableSql = [];
Expand All @@ -499,23 +489,6 @@ public function getAlterTableSQL(TableDiff $diff): array

if ($newName !== null) {
$sql[] = "sp_rename '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";

/**
* Rename table's default constraints names
* to match the new table name.
* This is necessary to ensure that the default
* constraints can be referenced in future table
* alterations as the table name is encoded in
* default constraints' names.
*/
$sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
"SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
"+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
"'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
'FROM sys.default_constraints dc ' .
'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
"WHERE tbl.name = '" . $newName->getName() . "';" .
'EXEC sp_executesql @sql';
}

$sql = array_merge(
Expand All @@ -538,18 +511,24 @@ private function getAlterTableAddDefaultConstraintClause(string $tableName, Colu
$columnDef = $column->toArray();
$columnDef['name'] = $column->getQuotedName($this);

return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef);
}

/**
* Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
*
* @param string $tableName The name of the table to generate the clause for.
* @param string $columnName The name of the column to generate the clause for.
*/
private function getAlterTableDropDefaultConstraintClause(string $tableName, string $columnName): string
private function getAlterTableDropDefaultConstraintClause(Column $column): string
{
return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
if (! $column->hasPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME)) {
throw new InvalidArgumentException(
'Column ' . $column->getName() . ' was not properly introspected as it has a default value'
. ' but does not have the default constraint name.',
);
}

return 'DROP CONSTRAINT ' . $this->quoteIdentifier(
$column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME),
);
}

/**
Expand Down Expand Up @@ -1226,17 +1205,6 @@ protected function getLikeWildcardCharacters(): string
return parent::getLikeWildcardCharacters() . '[]^';
}

/**
* Returns a unique default constraint name for a table and column.
*
* @param string $table Name of the table to generate the unique default constraint name for.
* @param string $column Name of the column in the table to generate the unique default constraint name for.
*/
private function generateDefaultConstraintName(string $table, string $column): string
{
return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
}

/**
* Returns a hash value for a given identifier.
*
Expand Down
68 changes: 12 additions & 56 deletions src/Schema/SQLServerSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

use function array_change_key_case;
use function assert;
use function count;
use function explode;
use function implode;
use function is_string;
Expand Down Expand Up @@ -65,7 +64,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column

$length = (int) $tableColumn['length'];

$precision = $default = null;
$precision = null;

$scale = 0;
$fixed = false;
Expand All @@ -82,10 +81,6 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
$precision = (int) $tableColumn['precision'];
}

if ($tableColumn['default'] !== null) {
$default = $this->parseDefaultExpression($tableColumn['default']);
}

switch ($dbType) {
case 'nchar':
case 'nvarchar':
Expand Down Expand Up @@ -118,7 +113,6 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column

$options = [
'fixed' => $fixed,
'default' => $default,
'notnull' => (bool) $tableColumn['notnull'],
'scale' => $scale,
'precision' => $precision,
Expand All @@ -135,6 +129,16 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column

$column = new Column($tableColumn['name'], Type::getType($type), $options);

if ($tableColumn['default'] !== null) {
$default = $this->parseDefaultExpression($tableColumn['default']);

$column->setDefault($default);
$column->setPlatformOption(
SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME,
$tableColumn['df_name'],
);
}

if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
$column->setPlatformOption('collation', $tableColumn['collation']);
}
Expand Down Expand Up @@ -246,58 +250,9 @@ protected function _getPortableDatabaseDefinition(array $database): string
*/
protected function _getPortableViewDefinition(array $view): View
{
// @todo
return new View($view['name'], $view['definition']);
}

public function alterTable(TableDiff $tableDiff): void
{
if (count($tableDiff->removedColumns) > 0) {
foreach ($tableDiff->removedColumns as $col) {
foreach ($this->getColumnConstraints($tableDiff->name, $col->getName()) as $constraint) {
$this->connection->executeStatement(
sprintf(
'ALTER TABLE %s DROP CONSTRAINT %s',
$tableDiff->name,
$constraint,
),
);
}
}
}

parent::alterTable($tableDiff);
}

/**
* Returns the names of the constraints for a given column.
*
* @return iterable<string>
*
* @throws Exception
*/
private function getColumnConstraints(string $table, string $column): iterable
{
return $this->connection->iterateColumn(
<<<'SQL'
SELECT o.name
FROM sys.objects o
INNER JOIN sys.objects t
ON t.object_id = o.parent_object_id
AND t.type = 'U'
INNER JOIN sys.default_constraints dc
ON dc.object_id = o.object_id
INNER JOIN sys.columns c
ON c.column_id = dc.parent_column_id
AND c.object_id = t.object_id
WHERE t.name = ?
AND c.name = ?
SQL
,
[$table, $column],
);
}

/** @throws Exception */
public function createComparator(): Comparator
{
Expand Down Expand Up @@ -351,6 +306,7 @@ protected function selectTableColumns(string $databaseName, ?string $tableName =
col.max_length AS length,
~col.is_nullable AS notnull,
def.definition AS [default],
def.name AS df_name,
col.scale,
col.precision,
col.is_identity AS autoincrement,
Expand Down

0 comments on commit c8d2c3d

Please sign in to comment.