diff --git a/dbt/adapters/sqlserver/sqlserver_connections.py b/dbt/adapters/sqlserver/sqlserver_connections.py index a91baeb1..c4424656 100644 --- a/dbt/adapters/sqlserver/sqlserver_connections.py +++ b/dbt/adapters/sqlserver/sqlserver_connections.py @@ -11,7 +11,6 @@ from dbt.adapters.fabric.fabric_connection_manager import ( AZURE_CREDENTIAL_SCOPE, bool_to_connection_string_arg, - get_pyodbc_attrs_before_accesstoken, get_pyodbc_attrs_before_credentials, ) @@ -136,10 +135,7 @@ def open(cls, connection: Connection) -> Connection: def connect(): logger.debug(f"Using connection string: {con_str_display}") - if credentials.authentication == "ActiveDirectoryAccessToken": - attrs_before = get_pyodbc_attrs_before_accesstoken(credentials.access_token) - else: - attrs_before = get_pyodbc_attrs_before_credentials(credentials) + attrs_before = get_pyodbc_attrs_before_credentials(credentials) handle = pyodbc.connect( con_str_concat, diff --git a/dbt/adapters/sqlserver/sqlserver_relation.py b/dbt/adapters/sqlserver/sqlserver_relation.py index f95bdd7f..508edc60 100644 --- a/dbt/adapters/sqlserver/sqlserver_relation.py +++ b/dbt/adapters/sqlserver/sqlserver_relation.py @@ -30,9 +30,9 @@ def render_limited(self) -> str: if self.limit is None: return rendered elif self.limit == 0: - return f"(select * from {rendered} where 1=0) {self._render_limited_alias()}" + return f"(select * from {rendered} where 1=0) AS {self._render_limited_alias()}" else: - return f"(select TOP {self.limit} * from {rendered}) {self._render_limited_alias()}" + return f"(select TOP {self.limit} * from {rendered}) AS {self._render_limited_alias()}" def __post_init__(self): # Check for length of Redshift table/view names. diff --git a/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_create_table_as.sql b/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_create_table_as.sql new file mode 100644 index 00000000..733e0027 --- /dev/null +++ b/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_create_table_as.sql @@ -0,0 +1,68 @@ +{% macro check_for_nested_cte(sql) %} + {% if execute %} {# Ensure this runs only at execution time #} + {% set cleaned_sql = sql | lower | replace("\n", " ") %} {# Convert to lowercase and remove newlines #} + {% set cte_count = cleaned_sql.count("with ") %} {# Count occurrences of "WITH " #} + {% if cte_count > 1 %} + {{ return(True) }} + {% else %} + {{ return(False) }} {# No nested CTEs found #} + {% endif %} + {% else %} + {{ return(False) }} {# Return False during parsing #} + {% endif %} +{% endmacro %} + +{% macro sqlserver__unit_test_create_table_as(temporary, relation, sql) -%} + {%- set query_label = apply_label() -%} + {%- set contract_config = config.get('contract') -%} + {%- set is_nested_cte = check_for_nested_cte(sql) -%} + + {%- if is_nested_cte -%} + {{ exceptions.warn( + "Nested CTE warning: Nested CTEs do not support CTAS. However, 2-level nested CTEs are supported due to a code bug. Please expect this fix in the future." + ) }} + {%- endif -%} + + {%- if is_nested_cte and contract_config.enforced -%} + + {{ exceptions.raise_compiler_error( + "Unit test Materialization error: Since the contract is enforced and the model contains a nested CTE, unit tests cannot be materialized. Please refactor your model or unenforce model and try again." + ) }} + + {%- elif not is_nested_cte and contract_config.enforced -%} + + {# Build CREATE TABLE + INSERT using a temporary view to avoid CTAS semantics #} + CREATE TABLE {{ relation }} + {{ build_columns_constraints(relation) }} + {{ get_assert_columns_equivalent(sql) }}; + + {%- set listColumns -%} + {%- for column in model['columns'] -%} + {{ "["~column~"]" }}{{ ", " if not loop.last }} + {%- endfor -%} + {%- endset -%} + + {%- set tmp_vw_relation = relation.incorporate(path={"identifier": relation.identifier ~ '__dbt_tmp_vw'}, type='view') -%} + {%- do adapter.drop_relation(tmp_vw_relation) -%} + {{ get_create_view_as_sql(tmp_vw_relation, sql) }} + + INSERT INTO {{ relation }} ({{ listColumns }}) + SELECT {{ listColumns }} FROM {{ tmp_vw_relation }} {{ query_label }}; + + DROP VIEW IF EXISTS {{ tmp_vw_relation.schema }}.{{ tmp_vw_relation.identifier }}; + + {%- else -%} + + {# Default: use SELECT INTO from an intermediate view so CTEs are preserved and labels are placed inside the selectable statement #} + {%- set tmp_vw_relation = relation.incorporate(path={"identifier": relation.identifier ~ '__dbt_tmp_vw'}, type='view') -%} + {%- do adapter.drop_relation(tmp_vw_relation) -%} + + {{ get_create_view_as_sql(tmp_vw_relation, sql) }} + + SELECT * INTO {{ relation }} FROM {{ tmp_vw_relation }} {{ query_label }}; + + DROP VIEW IF EXISTS {{ tmp_vw_relation.schema }}.{{ tmp_vw_relation.identifier }}; + + {%- endif -%} + +{%- endmacro %} diff --git a/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_table.sql b/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_table.sql new file mode 100644 index 00000000..424bd432 --- /dev/null +++ b/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_table.sql @@ -0,0 +1,40 @@ +{%- materialization unit, adapter='sqlserver' -%} + + {% set relations = [] %} + + {% set expected_rows = config.get('expected_rows') %} + {% set expected_sql = config.get('expected_sql') %} + {% set tested_expected_column_names = expected_rows[0].keys() if (expected_rows | length ) > 0 else get_columns_in_query(sql) %} + + {%- set target_relation = this.incorporate(type='table') -%} + {%- set temp_relation = make_temp_relation(target_relation)-%} + {% do run_query(sqlserver__unit_test_create_table_as(True, temp_relation, get_empty_subquery_sql(sql))) %} + {%- set columns_in_relation = adapter.get_columns_in_relation(temp_relation) -%} + {%- set column_name_to_data_types = {} -%} + {%- set column_name_to_quoted = {} -%} + {%- for column in columns_in_relation -%} + {%- do column_name_to_data_types.update({column.name|lower: column.data_type}) -%} + {%- do column_name_to_quoted.update({column.name|lower: column.quoted}) -%} + {%- endfor -%} + + {%- set expected_column_names_quoted = [] -%} + {%- for column_name in tested_expected_column_names -%} + {%- do expected_column_names_quoted.append(column_name_to_quoted[column_name]) -%} + {%- endfor -%} + + {% if not expected_sql %} + {% set expected_sql = get_expected_sql(expected_rows, column_name_to_data_types, column_name_to_quoted) %} + {% endif %} + {% set unit_test_sql = get_unit_test_sql(sql, expected_sql, expected_column_names_quoted) %} + + {% call statement('main', fetch_result=True) -%} + + {{ unit_test_sql }} + + {%- endcall %} + + {% do adapter.drop_relation(temp_relation) %} + + {{ return({'relations': relations}) }} + +{%- endmaterialization -%} diff --git a/dbt/include/sqlserver/macros/unit_test_sql/get_fixture_sql.sql b/dbt/include/sqlserver/macros/unit_test_sql/get_fixture_sql.sql new file mode 100644 index 00000000..17e6e791 --- /dev/null +++ b/dbt/include/sqlserver/macros/unit_test_sql/get_fixture_sql.sql @@ -0,0 +1,17 @@ +{% macro get_expected_sql(rows, column_name_to_data_types, column_name_to_quoted) %} + +{%- if (rows | length) == 0 -%} + select top 0 * from dbt_internal_unit_test_actual where 1=0 +{%- else -%} +{%- for row in rows -%} +{%- set formatted_row = format_row(row, column_name_to_data_types) -%} +select +{%- for column_name, column_value in formatted_row.items() %} {{ column_value }} as {{ column_name_to_quoted[column_name] }}{% if not loop.last -%}, {%- endif %} +{%- endfor %} +{%- if not loop.last %} +union all +{% endif %} +{%- endfor -%} +{%- endif -%} + +{% endmacro %} diff --git a/setup.py b/setup.py index 0a63ce6d..8c37edc4 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ def run(self): packages=find_namespace_packages(include=["dbt", "dbt.*"]), include_package_data=True, install_requires=[ - "dbt-fabric==1.9.3", + "dbt-fabric==1.9.6", "dbt-core>=1.9.0,<2.0", "dbt-common>=1.0,<2.0", "dbt-adapters>=1.11.0,<2.0",