99from rhosocial .activerecord .backend .dialect .exceptions import UnsupportedFeatureError
1010
1111
12+ _SQL_SERVER_SPARSE_COLUMN_VERSION = (10 , 0 , 0 )
13+ _SQL_SERVER_MASKED_COLUMN_VERSION = (13 , 0 , 0 )
14+
15+
1216class SQLServerAlterColumnModifierMixin :
1317 """SQL Server ALTER TABLE column modifiers.
1418
@@ -57,4 +61,124 @@ def format_drop_table_constraint_action(self, action):
5761 result = f"DROP CONSTRAINT { name } "
5862 if getattr (action , "cascade" , None ):
5963 result += " CASCADE"
60- return result , ()
64+ return result , ()
65+
66+ def format_alter_column_action (self , action ):
67+ """Dispatch an ALTER COLUMN action to a SQL Server-specific formatter.
68+
69+ SQL Server re-specifies the full column rather than applying the
70+ standard SET/DROP subclauses. The ``operation`` value selects the
71+ variant:
72+
73+ - ``SET DATA TYPE`` (or ``dialect_options["data_type"]``) renders the
74+ T-SQL ``ALTER COLUMN col type [COLLATE ...] [NULL | NOT NULL] [SPARSE]``
75+ form via ``format_alter_column_type_action``.
76+ - ``ADD MASKED`` renders dynamic data masking via
77+ ``format_add_masked_action``.
78+ - ``DROP MASKED`` renders ``format_drop_masked_action``.
79+ """
80+ operation = action .operation
81+ op_str = operation .value if hasattr (operation , "value" ) else str (operation )
82+ dialect_options = getattr (action , "dialect_options" , None ) or {}
83+
84+ if op_str == "ADD MASKED" or "masked_function" in dialect_options :
85+ return self .format_add_masked_action (action )
86+ if op_str == "DROP MASKED" or dialect_options .get ("drop_masked" ):
87+ return self .format_drop_masked_action (action )
88+ if op_str == "SET DATA TYPE" or "data_type" in dialect_options :
89+ return self .format_alter_column_type_action (action )
90+
91+ raise UnsupportedFeatureError (
92+ self .name ,
93+ f"ALTER COLUMN { op_str } " ,
94+ "SQL Server requires re-specifying the full column. Use "
95+ "operation='SET DATA TYPE' with dialect_options to add "
96+ "NULL/NOT NULL, COLLATE, SPARSE, or MASKED." ,
97+ )
98+
99+ def format_alter_column_type_action (self , action ):
100+ """Format a T-SQL ALTER COLUMN type change.
101+
102+ SQL Syntax:
103+ ALTER COLUMN column data_type [COLLATE collation]
104+ [NULL | NOT NULL] [SPARSE]
105+
106+ The data type comes from ``dialect_options["data_type"]`` or the
107+ action's ``new_value``. ``not_null``, ``collate``, and ``sparse``
108+ are carried in ``dialect_options``.
109+ """
110+ dialect_options = getattr (action , "dialect_options" , None ) or {}
111+ data_type = dialect_options .get ("data_type" ) or action .new_value
112+ if data_type is None :
113+ raise ValueError ("data type is required for ALTER COLUMN" )
114+
115+ parts = [
116+ "ALTER COLUMN" ,
117+ self .format_identifier (action .column_name ),
118+ str (data_type ),
119+ ]
120+
121+ collate = dialect_options .get ("collate" )
122+ if collate :
123+ parts .append (f"COLLATE { collate } " )
124+
125+ not_null = dialect_options .get ("not_null" )
126+ if not_null is not None :
127+ parts .append ("NOT NULL" if not_null else "NULL" )
128+
129+ if dialect_options .get ("sparse" ):
130+ if self .version < _SQL_SERVER_SPARSE_COLUMN_VERSION : # type: ignore[attr-defined]
131+ raise UnsupportedFeatureError (
132+ self .name ,
133+ "SPARSE column" ,
134+ suggestion = "requires SQL Server 2008+." ,
135+ )
136+ parts .append ("SPARSE" )
137+
138+ return " " .join (parts ), ()
139+
140+ def format_add_masked_action (self , action ):
141+ """Format ALTER COLUMN ... ADD MASKED (dynamic data masking, 2016+).
142+
143+ SQL Syntax:
144+ ALTER COLUMN column ADD MASKED WITH (FUNCTION = 'mask_function')
145+
146+ The mask function is carried in ``dialect_options["masked_function"]``
147+ or the action's ``new_value``.
148+ """
149+ if self .version < _SQL_SERVER_MASKED_COLUMN_VERSION : # type: ignore[attr-defined]
150+ raise UnsupportedFeatureError (
151+ self .name ,
152+ "ADD MASKED (dynamic data masking)" ,
153+ suggestion = "requires SQL Server 2016+." ,
154+ )
155+
156+ dialect_options = getattr (action , "dialect_options" , None ) or {}
157+ function = dialect_options .get ("masked_function" ) or action .new_value
158+ if function is None :
159+ raise ValueError ("masked_function is required for ADD MASKED" )
160+
161+ escaped = str (function ).replace ("'" , "''" )
162+ return (
163+ f"ALTER COLUMN { self .format_identifier (action .column_name )} "
164+ f"ADD MASKED WITH (FUNCTION = '{ escaped } ')" ,
165+ (),
166+ )
167+
168+ def format_drop_masked_action (self , action ):
169+ """Format ALTER COLUMN ... DROP MASKED (dynamic data masking, 2016+).
170+
171+ SQL Syntax:
172+ ALTER COLUMN column DROP MASKED
173+ """
174+ if self .version < _SQL_SERVER_MASKED_COLUMN_VERSION : # type: ignore[attr-defined]
175+ raise UnsupportedFeatureError (
176+ self .name ,
177+ "DROP MASKED (dynamic data masking)" ,
178+ suggestion = "requires SQL Server 2016+." ,
179+ )
180+
181+ return (
182+ f"ALTER COLUMN { self .format_identifier (action .column_name )} DROP MASKED" ,
183+ (),
184+ )
0 commit comments