From b1130be0ff4f8cedb37a31ca4fb264928e1b9ee0 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 10 Jun 2026 15:40:19 +0000 Subject: [PATCH 1/9] fix: SET XACT_ABORT ON in dml refresh swap transaction Without XACT_ABORT, a statement-aborting error (e.g. a constraint violation on the INSERT) does not stop the swap batch: execution continues to the trailing COMMIT, which persists the DELETE and leaves the target committed-empty. Verified live against SQL Server 2022; with XACT_ABORT ON the whole transaction rolls back and the target keeps its pre-run rows. --- .../materializations/models/table/table_dml_refresh.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql b/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql index 4a5d095c4..33fffc04c 100644 --- a/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql +++ b/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql @@ -56,7 +56,13 @@ {# Atomic DML swap — RCSI protects concurrent readers #} {# dbt-sqlserver uses autocommit=True and add_begin_query/add_commit_query #} {# are no-ops, so this creates a simple (non-nested) transaction. #} + {# SET XACT_ABORT ON makes the whole transaction roll back if any statement #} + {# fails. Without it, statement-aborting errors on the INSERT (e.g. NULL or #} + {# constraint violations) do not stop the batch: the DELETE stands and the #} + {# trailing COMMIT still executes, leaving the target committed-empty — #} + {# silent data loss. (Verified against SQL Server 2022.) #} {% call statement('dml_refresh_swap') -%} + SET XACT_ABORT ON; BEGIN TRANSACTION; DELETE FROM {{ target_relation }}; INSERT INTO {{ target_relation }} ({{ column_list }}) From c0961b52a2aed5f7db65807491b12a1209caf500 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 10 Jun 2026 15:43:50 +0000 Subject: [PATCH 2/9] fix: default port 1433 (not Postgres 5432) in dbt init profile template --- dbt/include/sqlserver/profile_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt/include/sqlserver/profile_template.yml b/dbt/include/sqlserver/profile_template.yml index 32c807119..abe07dc40 100644 --- a/dbt/include/sqlserver/profile_template.yml +++ b/dbt/include/sqlserver/profile_template.yml @@ -4,7 +4,7 @@ prompts: host: hint: "your host name" port: - default: 5432 + default: 1433 type: "int" user: hint: "dev username" From 26406038865681b8548d5f4c433aed0585249705 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 10 Jun 2026 15:43:50 +0000 Subject: [PATCH 3/9] fix: map Python float to SQL Server float, not bigint --- dbt/adapters/sqlserver/sqlserver_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt/adapters/sqlserver/sqlserver_constants.py b/dbt/adapters/sqlserver/sqlserver_constants.py index ecae4e8c0..9ee5fdcb6 100644 --- a/dbt/adapters/sqlserver/sqlserver_constants.py +++ b/dbt/adapters/sqlserver/sqlserver_constants.py @@ -102,7 +102,7 @@ "str": "varchar", "uuid.UUID": "uniqueidentifier", "uuid": "uniqueidentifier", - "float": "bigint", + "float": "float", "int": "int", "bytes": "varbinary", "bytearray": "varbinary", From 3a4b626a5406bb298bae8eda2995dcad35294b10 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Wed, 10 Jun 2026 15:43:50 +0000 Subject: [PATCH 4/9] fix: escape single quotes in query_tag before building OPTION (LABEL) A query_tag containing a single quote broke every query the adapter emitted, and allowed injection into the OPTION clause. Escape via dbt's cross-adapter escape_single_quotes() macro (quote doubling on this adapter), the same helper the EXEC('...') wrappers here already use. --- dbt/include/sqlserver/macros/adapters/metadata.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dbt/include/sqlserver/macros/adapters/metadata.sql b/dbt/include/sqlserver/macros/adapters/metadata.sql index 062a0c4f5..a72377087 100644 --- a/dbt/include/sqlserver/macros/adapters/metadata.sql +++ b/dbt/include/sqlserver/macros/adapters/metadata.sql @@ -1,6 +1,7 @@ {% macro get_query_options(parse_options=False) %} {{ log (config.get('query_tag','dbt-sqlserver'))}} - {%- set query_label = config.get('query_tag','dbt-sqlserver') -%} + {#- Escape single quotes so a query_tag like "it's" can't break out of the LABEL literal. -#} + {%- set query_label = escape_single_quotes(config.get('query_tag','dbt-sqlserver')) -%} {%- set query_options = config.get('query_options', {}) -%} {%- set query_options_raw = config.get('query_options_raw', []) -%} @@ -77,7 +78,7 @@ incremental, snapshot, unit_test), override get_query_options instead. -#} {% macro apply_label() %} {{ log (config.get('query_tag','dbt-sqlserver'))}} - {%- set query_label = config.get('query_tag','dbt-sqlserver') -%} + {%- set query_label = escape_single_quotes(config.get('query_tag','dbt-sqlserver')) -%} OPTION (LABEL = '{{query_label}}'); {% endmacro %} From 4452a574b012e8e3eaba36cc5bdaa624fd7b346e Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Fri, 12 Jun 2026 13:13:49 +0000 Subject: [PATCH 5/9] fix: columnstore IF EXISTS guard checked object_id('schema_table') The underscore-joined name never resolves, so the existence check was always false and the DROP never ran. Use the relation's own quoted rendering (relation.include(database=False)), which OBJECT_ID resolves. --- dbt/include/sqlserver/macros/adapters/indexes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt/include/sqlserver/macros/adapters/indexes.sql b/dbt/include/sqlserver/macros/adapters/indexes.sql index 2fd3733ae..278d93ac5 100644 --- a/dbt/include/sqlserver/macros/adapters/indexes.sql +++ b/dbt/include/sqlserver/macros/adapters/indexes.sql @@ -1,6 +1,6 @@ {% macro sqlserver__create_clustered_columnstore_index(relation) -%} {%- set cci_name = (relation.schema ~ '_' ~ relation.identifier ~ '_cci') | replace(".", "") | replace(" ", "") -%} - {%- set relation_name = relation.schema ~ '_' ~ relation.identifier -%} + {%- set relation_name = relation.include(database=False) -%} {%- set full_relation = '"' ~ relation.schema ~ '"."' ~ relation.identifier ~ '"' -%} use [{{ relation.database }}]; if EXISTS ( From ef80b28bea8c720a38aa26d16ea504f37d44c2c4 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Fri, 12 Jun 2026 13:13:49 +0000 Subject: [PATCH 6/9] docs: correct incremental default-strategy comment to MERGE With a unique_key the default strategy emits a MERGE via get_incremental_merge_sql, not delete+insert as the comment claimed. --- .../models/incremental/incremental_strategies.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql b/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql index 393d70205..2adb1f343 100644 --- a/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql +++ b/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql @@ -1,7 +1,7 @@ {% macro sqlserver__get_incremental_default_sql(arg_dict) %} {% if arg_dict["unique_key"] %} - -- Delete + Insert Strategy, calls get_delete_insert_merge_sql + -- Merge strategy: emits a MERGE statement via get_incremental_merge_sql {% do return(get_incremental_merge_sql(arg_dict)) %} {% else %} -- Incremental Append will insert data into target table. From f7593eaf5b186837d07e2af91890193ecf3d1599 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Fri, 12 Jun 2026 13:13:58 +0000 Subject: [PATCH 7/9] docs: de-dupe README schema-concat section README documented dbt_sqlserver_use_default_schema_concat twice with conflicting flags-vs-vars guidance; merged into one section matching the code (behavior flag primary, vars fallback). --- README.md | 49 ++++++++++++++----------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 3c431ec0a..f3a9fe22f 100644 --- a/README.md +++ b/README.md @@ -98,51 +98,30 @@ See [the changelog](CHANGELOG.md) ## Configuration -- `dbt_sqlserver_use_default_schema_concat`: *(default: `false`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model. - - | Flag value | `custom_schema_name` | Result | - |---|---|---| - | `false` (default, legacy) | *(none)* | `target.schema` | - | `false` (default, legacy) | `"reporting"` | `reporting` | - | `true` (dbt-core standard) | *(none)* | `target.schema` | - | `true` (dbt-core standard) | `"reporting"` | `target.schema_reporting` | - - When `false` (the default), the adapter uses its legacy behaviour: `custom_schema_name` is used **as-is** without being prefixed by `target.schema`. - When `true`, the adapter delegates to dbt-core's `default__generate_schema_name`, which concatenates `target.schema` + `_` + `custom_schema_name`. - - **Example usage in `dbt_project.yml`:** - - ```yaml - flags: - dbt_sqlserver_use_default_schema_concat: true # Enable standard schema concatenation - ``` - - This adapter also supports the same setting via `vars:` for backwards compatibility, so either method works in the current release. - - > **Note:** If you want to permanently customise schema generation and avoid any future changes, override the `sqlserver__generate_schema_name` macro directly in your project instead. - - ### `dbt_sqlserver_use_default_schema_concat` *(default: `false`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model. -| Value | `custom_schema_name` | Result | +| Flag value | `custom_schema_name` | Result | |---|---|---| -| `false` (default) | *(none)* | `target.schema` | -| `false` (default) | `"reporting"` | `reporting` | -| `true` | *(none)* | `target.schema` | -| `true` | `"reporting"` | `target.schema_reporting` | +| `false` (default, legacy) | *(none)* | `target.schema` | +| `false` (default, legacy) | `"reporting"` | `reporting` | +| `true` (dbt-core standard) | *(none)* | `target.schema` | +| `true` (dbt-core standard) | `"reporting"` | `target.schema_reporting` | -When `false`, `custom_schema_name` is used as-is without being prefixed by `target.schema`. -When `true`, the adapter delegates to dbt-core's `default__generate_schema_name`. +When `false` (the default), the adapter uses its legacy behaviour: `custom_schema_name` is used **as-is** without being prefixed by `target.schema`. +When `true`, the adapter delegates to dbt-core's `default__generate_schema_name`, which concatenates `target.schema` + `_` + `custom_schema_name`. + +**Example usage in `dbt_project.yml`:** ```yaml -# dbt_project.yml -vars: - dbt_sqlserver_use_default_schema_concat: true +flags: + dbt_sqlserver_use_default_schema_concat: true # Enable standard schema concatenation ``` -> **Note:** To permanently customise schema generation without a flag dependency, override the `sqlserver__generate_schema_name` macro directly in your project. +The same setting is also honoured via `vars:` for backwards compatibility; the behavior flag under `flags:` takes precedence when both are set. + +> **Note:** If you want to permanently customise schema generation and avoid any future changes, override the `sqlserver__generate_schema_name` macro directly in your project instead. ### `backend` From 6fde61861d47aa13b6096ef5b2d508f3503ae8d6 Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Fri, 12 Jun 2026 13:13:58 +0000 Subject: [PATCH 8/9] chore: target py310 in black/isort pre-commit args black/isort pre-commit hooks targeted py39 while requires-python is >=3.10 and the manual black-check already used py310; aligned both. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 402e67158..c45d116c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,14 +48,14 @@ repos: - '--line-length' - '99' - '--python-version' - - '39' + - '310' - repo: 'https://github.com/psf/black' rev: 26.5.1 hooks: - id: black args: - '--line-length=99' - - '--target-version=py39' + - '--target-version=py310' - id: black alias: black-check stages: From 8da387827aeef8d2779d87ff80b87e73994fe99c Mon Sep 17 00:00:00 2001 From: Josh Markovic Date: Fri, 12 Jun 2026 13:13:58 +0000 Subject: [PATCH 9/9] fix: restore missing Makefile clean rule line The clean target had a .PHONY declaration and recipe lines but no 'clean:' rule line, so 'make clean' did nothing. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index defc31e1c..cb1425928 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,7 @@ server: ## Spins up a local MS SQL Server instance for development. Docker-compo docker compose up -d .PHONY: clean +clean: ## Removes ignored files and build artifacts from the repo. @echo "cleaning repo" @git clean -f -X