|
15 | 15 | from dbt.adapters.base.meta import available |
16 | 16 | from dbt.adapters.base.relation import BaseRelation |
17 | 17 | from dbt.adapters.capability import Capability, CapabilityDict, CapabilitySupport, Support |
18 | | -from dbt.adapters.events.types import SchemaCreation |
| 18 | +from dbt.adapters.events.logging import AdapterLogger |
| 19 | +from dbt.adapters.events.types import ColTypeChange, SchemaCreation |
19 | 20 | from dbt.adapters.reference_keys import _make_ref_key_dict |
20 | 21 | from dbt.adapters.relation_configs import RelationConfigChangeAction |
21 | 22 | from dbt.adapters.sql.impl import CREATE_SCHEMA_MACRO_NAME, SQLAdapter |
|
30 | 31 | from dbt.adapters.sqlserver.sqlserver_connections import SQLServerConnectionManager |
31 | 32 | from dbt.adapters.sqlserver.sqlserver_relation import SQLServerRelation |
32 | 33 |
|
| 34 | +logger = AdapterLogger("SQLServer") |
| 35 | + |
33 | 36 |
|
34 | 37 | class SQLServerAdapter(SQLAdapter): |
35 | 38 | """ |
@@ -111,6 +114,16 @@ def _behavior_flags(self) -> List[BehaviorFlag]: |
111 | 114 | "The new behaviour is intended to become the default in a future release." |
112 | 115 | ), |
113 | 116 | }, |
| 117 | + { |
| 118 | + "name": "dbt_sqlserver_enable_safe_type_expansion", |
| 119 | + "default": False, |
| 120 | + "description": ( |
| 121 | + "Allow the SQL Server adapter to widen column types during schema expansion. " |
| 122 | + "This enables promotions like varchar -> nvarchar, " |
| 123 | + "bit -> tinyint -> smallint -> int -> bigint, " |
| 124 | + "and numeric(p,s) -> numeric(p2,s2) using alter column." |
| 125 | + ), |
| 126 | + }, |
114 | 127 | { |
115 | 128 | "name": "dbt_sqlserver_use_dbt_transactions", |
116 | 129 | "default": False, |
@@ -312,6 +325,101 @@ def render_model_constraint(cls, constraint: ModelLevelConstraint) -> Optional[s |
312 | 325 | else: |
313 | 326 | return None |
314 | 327 |
|
| 328 | + def _get_row_count(self, relation) -> int: |
| 329 | + """Return the number of rows in the given relation.""" |
| 330 | + sql = f"SELECT COUNT_BIG(*) FROM {relation}" |
| 331 | + _, cursor = self.connections.add_select_query(sql) |
| 332 | + row = cursor.fetchone() |
| 333 | + return int(row[0]) if row else 0 |
| 334 | + |
| 335 | + def expand_column_types(self, goal, current, max_rows: int = 1000000): |
| 336 | + """Override to ensure we preserve nvarchar/nchar type family during |
| 337 | + column expansion. Necessary same-family resizes (e.g. varchar size) |
| 338 | + always proceed. Safe type expansions (cross-family promotions like |
| 339 | + varchar -> nvarchar) are guarded by column_type_expansion_max_rows. |
| 340 | + enable_safe_type_expansion is the future approach for widening.""" |
| 341 | + |
| 342 | + reference_columns = {c.name: c for c in self.get_columns_in_relation(goal)} |
| 343 | + target_columns = {c.name: c for c in self.get_columns_in_relation(current)} |
| 344 | + |
| 345 | + enable_safe = self.behavior.dbt_sqlserver_enable_safe_type_expansion |
| 346 | + |
| 347 | + row_count_exceeds = False |
| 348 | + if enable_safe and max_rows != -1: |
| 349 | + if max_rows == 0: |
| 350 | + row_count_exceeds = True |
| 351 | + logger.info( |
| 352 | + "Safe type expansion skipped for %s: column_type_expansion_max_rows is 0.", |
| 353 | + current, |
| 354 | + ) |
| 355 | + else: |
| 356 | + row_count = self._get_row_count(current) |
| 357 | + if row_count > max_rows: |
| 358 | + row_count_exceeds = True |
| 359 | + logger.warning( |
| 360 | + "Safe type expansion skipped for %s: " |
| 361 | + "%s rows exceeds column_type_expansion_max_rows (%s). " |
| 362 | + "Set column_type_expansion_max_rows=-1 to disable " |
| 363 | + "this check, or increase the limit.", |
| 364 | + current, |
| 365 | + row_count, |
| 366 | + max_rows, |
| 367 | + ) |
| 368 | + |
| 369 | + for column_name, reference_column in reference_columns.items(): |
| 370 | + target_column = target_columns.get(column_name) |
| 371 | + if target_column is None: |
| 372 | + continue |
| 373 | + |
| 374 | + if target_column.can_expand_to(reference_column): |
| 375 | + pass |
| 376 | + elif ( |
| 377 | + enable_safe |
| 378 | + and not row_count_exceeds |
| 379 | + and target_column.can_expand_safe(reference_column) |
| 380 | + ): |
| 381 | + pass |
| 382 | + else: |
| 383 | + continue |
| 384 | + |
| 385 | + if reference_column.is_string(): |
| 386 | + col_string_size = reference_column.string_size() |
| 387 | + new_type = reference_column.string_type_instance(col_string_size) |
| 388 | + else: |
| 389 | + new_type = reference_column.data_type |
| 390 | + fire_event( |
| 391 | + ColTypeChange( |
| 392 | + orig_type=target_column.data_type, |
| 393 | + new_type=new_type, |
| 394 | + table=_make_ref_key_dict(current), |
| 395 | + ) |
| 396 | + ) |
| 397 | + self.alter_column_type(current, column_name, new_type) |
| 398 | + |
| 399 | + @available.parse_none |
| 400 | + def expand_target_column_types( |
| 401 | + self, from_relation: BaseRelation, to_relation: BaseRelation, max_rows: int = 1000000 |
| 402 | + ) -> None: |
| 403 | + if not isinstance(from_relation, self.Relation): |
| 404 | + from dbt.adapters.base.impl import MacroArgTypeError |
| 405 | + |
| 406 | + raise MacroArgTypeError( |
| 407 | + method_name="expand_target_column_types", |
| 408 | + arg_name="from_relation", |
| 409 | + got_value=from_relation, |
| 410 | + expected_type=self.Relation, |
| 411 | + ) |
| 412 | + if not isinstance(to_relation, self.Relation): |
| 413 | + from dbt.adapters.base.impl import MacroArgTypeError |
| 414 | + |
| 415 | + raise MacroArgTypeError( |
| 416 | + method_name="expand_target_column_types", |
| 417 | + arg_name="to_relation", |
| 418 | + got_value=to_relation, |
| 419 | + expected_type=self.Relation, |
| 420 | + ) |
| 421 | + self.expand_column_types(from_relation, to_relation, max_rows) |
| 422 | + |
315 | 423 | @available |
316 | 424 | def parse_index(self, raw_index: Any) -> Optional[SQLServerIndexConfig]: |
317 | 425 | return SQLServerIndexConfig.parse(raw_index) |
|
0 commit comments