From 703862b68734b806c42cf8fc8d86fd248633f8aa Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 24 Jun 2026 19:58:04 +0000 Subject: [PATCH 1/6] feat: add SQL Server 2025 support Cover SQL Server 2025 in the integration-test matrix (pyodbc and mssql-python on Python 3.13 / ODBC Driver 18) and add it to the published server CI images. Rename the server Dockerfile build arg SQLServer_VERSION to MSSQL_VERSION to match the build-arg the workflow already passes; with the old name the matrix version was ignored and every image used the 2022 default. Document 2025 as a supported version and refresh the stale dbt-core 0.14 compatibility note to 1.10. --- .../workflows/integration-tests-sqlserver.yml | 14 ++++++++++++- .github/workflows/publish-docker.yml | 2 +- CHANGELOG.md | 1 + README.md | 20 ++++++++++++++++--- devops/server.Dockerfile | 4 ++-- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests-sqlserver.yml b/.github/workflows/integration-tests-sqlserver.yml index c31c55f6a..62b9a2b21 100644 --- a/.github/workflows/integration-tests-sqlserver.yml +++ b/.github/workflows/integration-tests-sqlserver.yml @@ -82,7 +82,19 @@ jobs: sqlserver_version: "2019" msodbc_version: "17" collation: SQL_Latin1_General_CP1_CI_AS - # Add the case-sensitive collation only on the latest SQL Server + # SQL Server 2025 (newest): cover both backends on the latest + # Python and ODBC driver (18). + - backend: pyodbc + python_version: "3.13" + sqlserver_version: "2025" + msodbc_version: "18" + collation: SQL_Latin1_General_CP1_CI_AS + - backend: mssql-python + python_version: "3.13" + sqlserver_version: "2025" + msodbc_version: "18" + collation: SQL_Latin1_General_CP1_CI_AS + # Add the case-sensitive collation on the SQL Server 2022 baseline # and latest Python/backend rows. - backend: pyodbc python_version: "3.13" diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 0a7bdb8fd..ce9a1e9b1 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -44,7 +44,7 @@ jobs: publish-docker-server: strategy: matrix: - mssql_version: ["2017", "2019", "2022"] + mssql_version: ["2017", "2019", "2022", "2025"] runs-on: ubuntu-latest permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6a2e9b4..9fe487b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Add `drop_unmanaged_indexes` config (`false` (default) / `warn` / `true`) for indexes dbt didn't create. - Validate cross-index config conflicts (multiple clustered indexes, clustered vs `as_columnstore`). - Document the minimum supported SQL Server version (2017). Partitioning, `XML_COMPRESSION` and ordered columnstore are not yet expressible in the `indexes` config. +- Add SQL Server 2025 to the integration-test matrix (pyodbc and `mssql-python` backends, ODBC Driver 18) and document it as a supported version. ### v1.10.0 diff --git a/README.md b/README.md index 34f25d944..5bf677052 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,24 @@ [dbt](https://www.getdbt.com) adapter for Microsoft SQL Server and Azure SQL services. -The adapter supports dbt-core 0.14 or newer and follows the same versioning scheme. -E.g. version 1.1.x of the adapter will be compatible with dbt-core 1.1.x. +The adapter supports dbt-core 1.10 or newer and follows the same versioning scheme. +E.g. version 1.10.x of the adapter will be compatible with dbt-core 1.10.x. -The minimum supported SQL Server version is SQL Server 2017. +## Supported SQL Server versions + +The adapter is tested against the following SQL Server versions: + +| SQL Server version | Supported | +|---|---| +| SQL Server 2017 | ✅ (minimum supported version) | +| SQL Server 2019 | ✅ | +| SQL Server 2022 | ✅ | +| SQL Server 2025 | ✅ | +| Azure SQL Database | ✅ | +| Azure SQL Managed Instance | ✅ | + +The minimum supported SQL Server version is SQL Server 2017; older versions are not supported. +SQL Server 2017, 2019, 2022, and 2025 are covered by the integration test suite, and Azure SQL services are tested separately. ## Documentation diff --git a/devops/server.Dockerfile b/devops/server.Dockerfile index d5743136d..a7a3fe082 100644 --- a/devops/server.Dockerfile +++ b/devops/server.Dockerfile @@ -1,5 +1,5 @@ -ARG SQLServer_VERSION="2022" -FROM mcr.microsoft.com/mssql/server:${SQLServer_VERSION}-latest +ARG MSSQL_VERSION="2022" +FROM mcr.microsoft.com/mssql/server:${MSSQL_VERSION}-latest ENV COLLATION="SQL_Latin1_General_CP1_CI_AS" From 391080c38043f277d530a70cab4a7c111222a053 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 24 Jun 2026 19:58:04 +0000 Subject: [PATCH 2/6] docs: document the as_columnstore table config Explain that table materializations build a clustered columnstore index by default, and that as_columnstore: false is required for tables with (n)varchar(max)/LOB columns such as dbt store_failures audit tables. --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 5bf677052..155d02129 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,29 @@ flags: **Compatibility notes:** Enabling `dbt_sqlserver_use_dbt_transactions: true` may expose transaction-state assumptions hidden by autocommit-only mode. Explicit transaction macros may interact with dbt-managed transactions, and cleanup after failed DDL/DML may differ. Review pre/post hooks for in-transaction vs out-of-transaction semantics. +### `as_columnstore` + +*(default: `true`)* When building a table, the adapter creates a [clustered columnstore index](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/columnstore-indexes-overview) (CCI) on it. Set `as_columnstore: false` to build a plain rowstore table instead. + +This matters for any table containing a `(n)varchar(max)` or other LOB column, because SQL Server does not allow those data types to participate in a columnstore index. The table build fails with: + +> Column '...' has a data type that cannot participate in a columnstore index. + +A common case is dbt's [test failure storage](https://docs.getdbt.com/reference/resource-configs/store_failures): the audit tables can contain `VARCHAR(MAX)` columns (dbt's `STRING` type maps to `VARCHAR(MAX)`), so disable the CCI on those resources: + +```yaml +# dbt_project.yml +data_tests: + +store_failures: true + +as_columnstore: false # avoids CCI on (n)varchar(max) audit columns +``` + +You can also set it per model: + +```sql +{{ config(materialized="table", as_columnstore=false) }} +``` + ## Contributing [![Unit tests](https://github.com/dbt-msft/dbt-sqlserver/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/dbt-msft/dbt-sqlserver/actions/workflows/unit-tests.yml) From 7d8e90b0fa0f43cb6184aaf9500f6338763d3b78 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 24 Jun 2026 19:58:04 +0000 Subject: [PATCH 3/6] test: keep empty audit table on passing store-failures run (#601) A passing test run with --store-failures must replace prior failures with an empty audit table rather than drop it. Adds a functional regression test for dbt-msft/dbt-sqlserver#601. --- .../mssql/test_store_failures_passing.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/functional/adapter/mssql/test_store_failures_passing.py diff --git a/tests/functional/adapter/mssql/test_store_failures_passing.py b/tests/functional/adapter/mssql/test_store_failures_passing.py new file mode 100644 index 000000000..728e08a0d --- /dev/null +++ b/tests/functional/adapter/mssql/test_store_failures_passing.py @@ -0,0 +1,101 @@ +# flake8: noqa: E501 +"""Regression test for dbt-msft/dbt-sqlserver#601. + +With ``--store-failures``, a test that *passes* must leave behind an empty +audit relation, not drop it. dbt's contract: "A test's results will always +replace previous failures for the same test, even if that test results in no +failures." The SQL Server adapter was reported to ``DROP`` the audit table on a +passing test instead of replacing it with an empty table (Postgres creates the +empty table). + +This exercises the exact reported scenario: a passing test configured with +``store_failures`` materialized as a ``table``. It asserts the audit relation +exists, is a base table (not a view), is empty, and survives idempotent re-runs. +""" +import pytest + +from dbt.tests.util import run_dbt + +# the default audit schema (_dbt_test__audit) plus the test schema can exceed +# identifier limits; use a short suffix as the rest of the suite does. +TEST_AUDIT_SCHEMA_SUFFIX = "dbt_test__aud" + +model__chipmunks = """ +select 1 as id, 'alvin' as name +union all +select 2 as id, 'simon' as name +""" + +# returns zero rows -> the test passes +test__passing_601 = """ +{{ config(store_failures=true, store_failures_as='table') }} +select * from {{ ref('chipmunks') }} +where 1 = 2 +""" + + +class TestStoreFailuresPassingKeepsEmptyTable: + @pytest.fixture(scope="class") + def models(self): + return {"chipmunks.sql": model__chipmunks} + + @pytest.fixture(scope="class") + def tests(self): + return {"passing_601.sql": test__passing_601} + + @pytest.fixture(scope="class") + def project_config_update(self): + return { + "vars": {"dbt_sqlserver_use_default_schema_concat": True}, + "data_tests": {"+schema": TEST_AUDIT_SCHEMA_SUFFIX}, + } + + @pytest.fixture(scope="function", autouse=True) + def setup(self, project): + self.audit_schema = f"{project.test_schema}_{TEST_AUDIT_SCHEMA_SUFFIX}" + run_dbt(["run"]) + yield + with project.adapter.connection_named("__test"): + relation = project.adapter.Relation.create( + database=project.database, schema=self.audit_schema + ) + project.adapter.drop_schema(relation) + + def _assert_empty_audit_table(self, project): + # type_desc proves the relation exists AND is a user table (not a view). + # On the #601 bug the relation is dropped, so this returns no rows. + # Queried via sys catalog (lowercase column names) so it is safe under a + # case-sensitive database collation. + rows = project.run_sql( + f""" + select o.type_desc + from sys.objects o + join sys.schemas s on o.schema_id = s.schema_id + where s.name = '{self.audit_schema}' + and o.name = 'passing_601' + """, + fetch="all", + ) + assert len(rows) == 1 and rows[0][0] == "USER_TABLE", ( + f"audit relation [{self.audit_schema}].[passing_601] should be a user " + f"table that persists after a passing store-failures run, got: " + f"{[tuple(r) for r in rows]}" + ) + # and it must be empty (the failures were replaced with nothing) + count = project.run_sql( + f"select count(*) from [{self.audit_schema}].[passing_601]", + fetch="one", + ) + assert count[0] == 0, f"audit table should be empty, has {count[0]} rows" + + def test_passing_test_keeps_empty_audit_table(self, project): + results = run_dbt(["test", "--store-failures"], expect_pass=True) + assert len(results) == 1 + assert results[0].status == "pass" + assert results[0].failures == 0 + self._assert_empty_audit_table(project) + + # idempotency: a second run must still leave the empty table in place + results = run_dbt(["test", "--store-failures"], expect_pass=True) + assert results[0].status == "pass" + self._assert_empty_audit_table(project) From 66634a1c6f2e9b8638c7e0508f565520312feca1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:30:43 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/functional/adapter/mssql/test_store_failures_passing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/adapter/mssql/test_store_failures_passing.py b/tests/functional/adapter/mssql/test_store_failures_passing.py index 728e08a0d..7aa44f180 100644 --- a/tests/functional/adapter/mssql/test_store_failures_passing.py +++ b/tests/functional/adapter/mssql/test_store_failures_passing.py @@ -12,6 +12,7 @@ ``store_failures`` materialized as a ``table``. It asserts the audit relation exists, is a base table (not a view), is empty, and survives idempotent re-runs. """ + import pytest from dbt.tests.util import run_dbt From c137d8053e7fcdc40aec19b08a1d8d599827861f Mon Sep 17 00:00:00 2001 From: joshmarkovic <52184130+joshmarkovic@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:26:57 -0400 Subject: [PATCH 5/6] Update README.md Co-authored-by: Axell Padilla <68310020+axellpadilla@users.noreply.github.com> --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 155d02129..839130cc4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ The adapter is tested against the following SQL Server versions: | Azure SQL Managed Instance | ✅ | The minimum supported SQL Server version is SQL Server 2017; older versions are not supported. -SQL Server 2017, 2019, 2022, and 2025 are covered by the integration test suite, and Azure SQL services are tested separately. +## Supported SQL Server versions + +SQL Server 2017, 2019, 2022, and 2025 are covered by the integration test suite. Azure SQL Database and Azure SQL Managed Instance are not covered by the integration test suite, but are expected to be compatible. ## Documentation From ad899620908439470bd182095dfec68ed5a3c52e Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Thu, 25 Jun 2026 14:25:20 +0000 Subject: [PATCH 6/6] ci: promote SQL Server 2025 to baseline and fix README support section Addresses @axellpadilla's review on #721. - integration tests: make 2025 the matrix baseline, replacing 2022 as the latest tier. Keep 2017, 2019 and 2022 as single legacy rows so all four versions stay covered by CI. - README: remove the duplicate "Supported SQL Server versions" heading and the Azure SQL Database / Managed Instance rows that were marked tested. Azure is now described as not covered by CI but expected to be compatible. --- .../workflows/integration-tests-sqlserver.yml | 31 +++++++------------ README.md | 3 -- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.github/workflows/integration-tests-sqlserver.yml b/.github/workflows/integration-tests-sqlserver.yml index 62b9a2b21..0aa6f2f34 100644 --- a/.github/workflows/integration-tests-sqlserver.yml +++ b/.github/workflows/integration-tests-sqlserver.yml @@ -43,31 +43,31 @@ jobs: matrix: python_version: ["3.10", "3.11", "3.12", "3.13"] backend: [pyodbc, mssql-python] - # Baseline on 2022 - sqlserver_version: ["2022"] + # Baseline on 2025 (newest) + sqlserver_version: ["2025"] msodbc_version: ["18"] collation: [SQL_Latin1_General_CP1_CI_AS] exclude: - backend: mssql-python python_version: "3.10" - sqlserver_version: "2022" + sqlserver_version: "2025" - backend: mssql-python python_version: "3.11" - sqlserver_version: "2022" + sqlserver_version: "2025" - backend: mssql-python python_version: "3.12" - sqlserver_version: "2022" + sqlserver_version: "2025" include: # Keep pyodbc on every supported Python version, but retain # SQL Server ODBC 17 coverage for the oldest and newest Python. - backend: pyodbc python_version: "3.10" - sqlserver_version: "2022" + sqlserver_version: "2025" msodbc_version: "17" collation: SQL_Latin1_General_CP1_CI_AS - backend: pyodbc python_version: "3.13" - sqlserver_version: "2022" + sqlserver_version: "2025" msodbc_version: "17" collation: SQL_Latin1_General_CP1_CI_AS # Older SQL Server versions stay on pyodbc only, with a single @@ -82,29 +82,22 @@ jobs: sqlserver_version: "2019" msodbc_version: "17" collation: SQL_Latin1_General_CP1_CI_AS - # SQL Server 2025 (newest): cover both backends on the latest - # Python and ODBC driver (18). - backend: pyodbc python_version: "3.13" - sqlserver_version: "2025" - msodbc_version: "18" - collation: SQL_Latin1_General_CP1_CI_AS - - backend: mssql-python - python_version: "3.13" - sqlserver_version: "2025" - msodbc_version: "18" + sqlserver_version: "2022" + msodbc_version: "17" collation: SQL_Latin1_General_CP1_CI_AS - # Add the case-sensitive collation on the SQL Server 2022 baseline + # Add the case-sensitive collation on the SQL Server 2025 baseline # and latest Python/backend rows. - backend: pyodbc python_version: "3.13" - sqlserver_version: "2022" + sqlserver_version: "2025" msodbc_version: "17" collation: SQL_Latin1_General_CP1_CS_AS # mssql-python stays on the latest Python only. - backend: mssql-python python_version: "3.13" - sqlserver_version: "2022" + sqlserver_version: "2025" msodbc_version: "18" collation: SQL_Latin1_General_CP1_CS_AS runs-on: ubuntu-latest diff --git a/README.md b/README.md index 7d3aba732..5ce591896 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,8 @@ The adapter is tested against the following SQL Server versions: | SQL Server 2019 | ✅ | | SQL Server 2022 | ✅ | | SQL Server 2025 | ✅ | -| Azure SQL Database | ✅ | -| Azure SQL Managed Instance | ✅ | The minimum supported SQL Server version is SQL Server 2017; older versions are not supported. -## Supported SQL Server versions SQL Server 2017, 2019, 2022, and 2025 are covered by the integration test suite. Azure SQL Database and Azure SQL Managed Instance are not covered by the integration test suite, but are expected to be compatible.