Skip to content

Commit a8e9301

Browse files
committed
feat(sqlserver): IF EXISTS support for DROP COLUMN/CONSTRAINT
Add SQLServerAlterColumnModifierMixin so SQL Server renders DROP COLUMN IF EXISTS and DROP CONSTRAINT IF EXISTS (2016+) and raises UnsupportedFeatureError for ADD COLUMN IF NOT EXISTS. ADD omits the COLUMN literal per SQL Server syntax. Add unit tests.
1 parent d738418 commit a8e9301

3 files changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# src/rhosocial/activerecord/backend/impl/sqlserver/alter_table_modifier.py
2+
"""SQL Server ALTER TABLE column IF EXISTS handling.
3+
4+
Kept as a sibling module of ``dialect.py`` (like ``collation.py``) to avoid
5+
the circular import triggered by the eager ``mixins/__init__.py`` chain
6+
(``mixins/backend_mixin`` -> ``dialect``).
7+
"""
8+
9+
from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError
10+
11+
12+
class SQLServerAlterColumnModifierMixin:
13+
"""SQL Server ALTER TABLE column modifiers.
14+
15+
SQL Server supports ``DROP COLUMN IF EXISTS`` and
16+
``DROP CONSTRAINT IF EXISTS`` since 2016, but **not**
17+
``ADD COLUMN IF NOT EXISTS``. Applications opt in via
18+
``if_exists`` / ``if_not_exists``; ``None`` (default) renders the
19+
plain form.
20+
"""
21+
22+
def supports_add_column_if_not_exists(self) -> bool:
23+
"""SQL Server does not support ``ADD COLUMN IF NOT EXISTS``."""
24+
return False
25+
26+
def supports_drop_column_if_exists(self) -> bool:
27+
"""``DROP COLUMN IF EXISTS`` is supported since SQL Server 2016."""
28+
return True
29+
30+
def supports_drop_constraint_if_exists(self) -> bool:
31+
"""``DROP CONSTRAINT IF EXISTS`` is supported since SQL Server 2016."""
32+
return True
33+
34+
def format_add_column_action(self, action):
35+
if getattr(action, "if_not_exists", None) is True:
36+
raise UnsupportedFeatureError(
37+
self.name,
38+
"ADD COLUMN IF NOT EXISTS",
39+
suggestion="SQL Server does not support ADD COLUMN IF NOT EXISTS. "
40+
"Pre-check sys.columns / information_schema.COLUMNS.",
41+
)
42+
column_sql, column_params = self.format_column_definition(action.column)
43+
# SQL Server ALTER TABLE ... ADD takes the column without a COLUMN literal.
44+
return f"ADD {column_sql}", column_params
45+
46+
def format_drop_column_action(self, action):
47+
name = self.format_identifier(action.column_name)
48+
if getattr(action, "if_exists", None) is True:
49+
return f"DROP COLUMN IF EXISTS {name}", ()
50+
return f"DROP COLUMN {name}", ()
51+
52+
def format_drop_table_constraint_action(self, action):
53+
name = self.format_identifier(action.constraint_name)
54+
if getattr(action, "if_exists", None) is True:
55+
result = f"DROP CONSTRAINT IF EXISTS {name}"
56+
else:
57+
result = f"DROP CONSTRAINT {name}"
58+
if getattr(action, "cascade", None):
59+
result += " CASCADE"
60+
return result, ()

‎src/rhosocial/activerecord/backend/impl/sqlserver/dialect.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
from rhosocial.activerecord.backend.dialect.protocols import PartitionSupport
9696
from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError
9797
from .collation import validate_sqlserver_collation_name
98+
from .alter_table_modifier import SQLServerAlterColumnModifierMixin
9899
# SQLServerPartitionMixin is registered lazily in _register_partition_formatters()
99100

100101
if TYPE_CHECKING:
@@ -167,6 +168,7 @@ class SQLServerDialect(
167168
DateTimeMixin,
168169
DQLMixin,
169170
DMLMixin,
171+
SQLServerAlterColumnModifierMixin, # Before DDLColumnMixin to override format_*_action
170172
DDLColumnMixin,
171173
DDLTypeMixin,
172174
# Feature mixins
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_alter_table_if_exists.py
2+
"""Tests for SQL Server ALTER TABLE IF [NOT] EXISTS handling.
3+
4+
SQL Server supports ``DROP COLUMN IF EXISTS`` and ``DROP CONSTRAINT
5+
IF EXISTS`` since 2016 but **not** ``ADD COLUMN IF NOT EXISTS``. The
6+
``ADD`` form omits the ``COLUMN`` literal (SQL Server syntax).
7+
"""
8+
9+
import pytest
10+
11+
from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError
12+
from rhosocial.activerecord.backend.expression.statements import ColumnDefinition
13+
from rhosocial.activerecord.backend.expression.statements.ddl_alter import (
14+
AddColumn,
15+
DropColumn,
16+
DropTableConstraint,
17+
)
18+
from rhosocial.activerecord.backend.expression.types import TextType
19+
from rhosocial.activerecord.backend.impl.sqlserver.dialect import SQLServerDialect
20+
21+
22+
@pytest.fixture
23+
def dialect():
24+
return SQLServerDialect(version=(15, 0, 0))
25+
26+
27+
class TestSQLServerAlterTableModifierCapabilities:
28+
def test_supports_switches(self, dialect):
29+
assert dialect.supports_add_column_if_not_exists() is False
30+
assert dialect.supports_drop_column_if_exists() is True
31+
assert dialect.supports_drop_constraint_if_exists() is True
32+
33+
34+
class TestSQLServerAddColumn:
35+
def test_add_column_omits_column_literal(self, dialect):
36+
action = AddColumn(dialect, ColumnDefinition("content", TextType()))
37+
sql, params = action.to_sql()
38+
assert sql.startswith("ADD [content]")
39+
assert "ADD COLUMN" not in sql
40+
assert params == ()
41+
42+
def test_add_column_if_not_exists_raises(self, dialect):
43+
action = AddColumn(
44+
dialect,
45+
ColumnDefinition("content", TextType()),
46+
if_not_exists=True,
47+
)
48+
with pytest.raises(UnsupportedFeatureError):
49+
action.to_sql()
50+
51+
52+
class TestSQLServerDropColumnIfExists:
53+
def test_if_exists_renders_qualifier(self, dialect):
54+
action = DropColumn(dialect, column_name="x", if_exists=True)
55+
sql, params = action.to_sql()
56+
assert "DROP COLUMN IF EXISTS [x]" == sql
57+
assert params == ()
58+
59+
def test_none_renders_plain_form(self, dialect):
60+
action = DropColumn(dialect, column_name="x")
61+
sql, params = action.to_sql()
62+
assert "DROP COLUMN [x]" == sql
63+
assert "IF EXISTS" not in sql
64+
assert params == ()
65+
66+
67+
class TestSQLServerDropConstraintIfExists:
68+
def test_if_exists_renders_qualifier(self, dialect):
69+
action = DropTableConstraint(
70+
dialect, constraint_name="fk", if_exists=True
71+
)
72+
sql, params = action.to_sql()
73+
assert "DROP CONSTRAINT IF EXISTS [fk]" == sql
74+
assert params == ()
75+
76+
def test_none_renders_plain_form(self, dialect):
77+
action = DropTableConstraint(dialect, constraint_name="fk")
78+
sql, params = action.to_sql()
79+
assert "DROP CONSTRAINT [fk]" == sql
80+
assert "IF EXISTS" not in sql
81+
assert params == ()

0 commit comments

Comments
 (0)