diff --git a/.github/workflows/integration-tests-sqlserver.yml b/.github/workflows/integration-tests-sqlserver.yml index 5c7694a6..d74274b4 100644 --- a/.github/workflows/integration-tests-sqlserver.yml +++ b/.github/workflows/integration-tests-sqlserver.yml @@ -18,7 +18,7 @@ jobs: name: Regular strategy: matrix: - python_version: ["3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] msodbc_version: ["17", "18"] sqlserver_version: ["2017", "2019", "2022"] collation: ["SQL_Latin1_General_CP1_CS_AS", "SQL_Latin1_General_CP1_CI_AS"] diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 63b1f5c4..9c96f15a 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -12,7 +12,7 @@ jobs: publish-docker-client: strategy: matrix: - python_version: ["3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] docker_target: ["msodbc17", "msodbc18"] runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 5acae556..a2f09804 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,7 +18,7 @@ jobs: name: Unit tests strategy: matrix: - python_version: ["3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest permissions: contents: read 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..7c21db16 --- /dev/null +++ b/dbt/include/sqlserver/macros/materializations/unit_test/unit_test_table.sql @@ -0,0 +1,41 @@ +{%- 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 can be added once supported by get_expected_sql #} + {% 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/setup.py b/setup.py index 0a63ce6d..af97049a 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", @@ -86,6 +86,7 @@ def run(self): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], project_urls={ "Setup & configuration": "https://docs.getdbt.com/reference/warehouse-profiles/mssql-profile", # noqa: E501