dbt-sqlserver 1.10.1rc2__tar.gz → 1.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/PKG-INFO +82 -5
  2. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/README.md +79 -2
  3. dbt_sqlserver-1.11.0/dbt/adapters/sqlserver/__version__.py +1 -0
  4. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/relation_configs/policies.py +1 -0
  5. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_adapter.py +113 -9
  6. dbt_sqlserver-1.11.0/dbt/adapters/sqlserver/sqlserver_configs.py +27 -0
  7. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_connections.py +62 -0
  8. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_credentials.py +2 -0
  9. dbt_sqlserver-1.11.0/dbt/adapters/sqlserver/sqlserver_mask.py +218 -0
  10. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_relation.py +2 -4
  11. dbt_sqlserver-1.11.0/dbt/include/sqlserver/macros/adapters/apply_masks.sql +170 -0
  12. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/indexes.sql +107 -42
  13. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/metadata.sql +2 -2
  14. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/relation.sql +10 -4
  15. dbt_sqlserver-1.11.0/dbt/include/sqlserver/macros/materializations/functions/helpers.sql +19 -0
  16. dbt_sqlserver-1.11.0/dbt/include/sqlserver/macros/materializations/functions/scalar.sql +42 -0
  17. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/hooks.sql +5 -1
  18. dbt_sqlserver-1.11.0/dbt/include/sqlserver/macros/materializations/models/incremental/incremental.sql +196 -0
  19. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/incremental/merge.sql +6 -0
  20. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/table/table.sql +49 -0
  21. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql +31 -2
  22. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot.sql +7 -1
  23. dbt_sqlserver-1.11.0/dbt/include/sqlserver/macros/relations/table/create.sql +264 -0
  24. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt_sqlserver.egg-info/PKG-INFO +82 -5
  25. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt_sqlserver.egg-info/SOURCES.txt +4 -0
  26. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt_sqlserver.egg-info/requires.txt +2 -2
  27. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/pyproject.toml +5 -4
  28. dbt_sqlserver-1.10.1rc2/dbt/adapters/sqlserver/__version__.py +0 -1
  29. dbt_sqlserver-1.10.1rc2/dbt/adapters/sqlserver/sqlserver_configs.py +0 -16
  30. dbt_sqlserver-1.10.1rc2/dbt/include/sqlserver/macros/materializations/models/incremental/incremental.sql +0 -102
  31. dbt_sqlserver-1.10.1rc2/dbt/include/sqlserver/macros/relations/table/create.sql +0 -50
  32. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/LICENSE +0 -0
  33. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/__init__.py +0 -0
  34. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/__init__.py +0 -0
  35. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/py.typed +0 -0
  36. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/relation_configs/__init__.py +0 -0
  37. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/relation_configs/index.py +0 -0
  38. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_auth.py +0 -0
  39. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_backend.py +0 -0
  40. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_column.py +0 -0
  41. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_constants.py +0 -0
  42. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_helpers.py +0 -0
  43. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/adapters/sqlserver/sqlserver_runtime.py +0 -0
  44. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/__init__.py +0 -0
  45. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/dbt_project.yml +0 -0
  46. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/apply_grants.sql +0 -0
  47. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/catalog.sql +0 -0
  48. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/columns.sql +0 -0
  49. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/persist_docs.sql +0 -0
  50. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/schema.sql +0 -0
  51. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/show.sql +0 -0
  52. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/adapters/validate_sql.sql +0 -0
  53. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql +0 -0
  54. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql +0 -0
  55. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/unit_test/get_fixture_sql.sql +0 -0
  56. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/unit_test/unit_test_create_table_as.sql +0 -0
  57. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/view/create_view_as.sql +0 -0
  58. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/models/view/view.sql +0 -0
  59. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/snapshots/helpers.sql +0 -0
  60. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot_merge.sql +0 -0
  61. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/snapshots/strategies.sql +0 -0
  62. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/tests.sql +0 -0
  63. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/materializations/unit_tests.sql +0 -0
  64. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/relations/seeds/helpers.sql +0 -0
  65. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/relations/table/clone.sql +0 -0
  66. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/relations/views/create.sql +0 -0
  67. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/any_value.sql +0 -0
  68. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/array_construct.sql +0 -0
  69. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/cast_bool_to_text.sql +0 -0
  70. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/concat.sql +0 -0
  71. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/date_trunc.sql +0 -0
  72. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/dateadd.sql +0 -0
  73. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/get_tables_by_pattern.sql +0 -0
  74. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/hash.sql +0 -0
  75. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/last_day.sql +0 -0
  76. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/length.sql +0 -0
  77. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/listagg.sql +0 -0
  78. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/position.sql +0 -0
  79. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/safe_cast.sql +0 -0
  80. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/split_part.sql +0 -0
  81. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/macros/utils/timestamps.sql +0 -0
  82. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt/include/sqlserver/profile_template.yml +0 -0
  83. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt_sqlserver.egg-info/dependency_links.txt +0 -0
  84. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/dbt_sqlserver.egg-info/top_level.txt +0 -0
  85. {dbt_sqlserver-1.10.1rc2 → dbt_sqlserver-1.11.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbt-sqlserver
3
- Version: 1.10.1rc2
3
+ Version: 1.11.0
4
4
  Summary: A Microsoft SQL Server adapter plugin for dbt
5
5
  Author: Mikael Ene, Anders Swanson, Sam Debruyn, Cor Zuurmond, Cody Scott
6
6
  License: MIT
@@ -21,9 +21,9 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: dbt-core<2.0,>=1.10.0
24
+ Requires-Dist: dbt-core<2.0,>=1.11.0
25
25
  Requires-Dist: dbt-common<2.0,>=1.22.0
26
- Requires-Dist: dbt-adapters<2.0,>=1.15.2
26
+ Requires-Dist: dbt-adapters<2.0,>=1.24.1
27
27
  Requires-Dist: pyodbc>=5.2.0
28
28
  Provides-Extra: azure
29
29
  Requires-Dist: azure-identity>=1.12.0; extra == "azure"
@@ -39,8 +39,19 @@ Dynamic: license-file
39
39
 
40
40
  [dbt](https://www.getdbt.com) adapter for Microsoft SQL Server and Azure SQL services.
41
41
 
42
- The adapter supports dbt-core 1.10 or newer and follows the same versioning scheme.
43
- E.g. version 1.10.x of the adapter is compatible with dbt-core 1.10.x.
42
+ The adapter supports dbt-core 1.11 or newer and follows the same versioning scheme.
43
+ E.g. version 1.11.x of the adapter is compatible with dbt-core 1.11.x.
44
+
45
+ ## Supported Python versions
46
+
47
+ The adapter is tested against:
48
+
49
+ | Python version | Status |
50
+ |---|---|
51
+ | 3.10 | Installable (not tested in CI) |
52
+ | 3.11 | Officially supported |
53
+ | 3.12 | Officially supported |
54
+ | 3.13 | Officially supported |
44
55
 
45
56
  ## Supported SQL Server versions
46
57
 
@@ -208,6 +219,25 @@ flags:
208
219
  dbt_sqlserver_use_dbt_transactions: true # <-- opt-in; default is false
209
220
  ```
210
221
 
222
+ ### `xact_abort`
223
+
224
+ *(default: `true`)* Profile/connection field. When enabled, the adapter runs `SET XACT_ABORT ON;` once per connection, right after it opens. With `XACT_ABORT ON`, a run-time error partway through a multi-statement batch (e.g. a `NOT NULL`/constraint violation during the DML table refresh's DELETE+INSERT swap) aborts the whole batch and rolls back any open transaction, instead of only aborting the failing statement and letting a trailing `COMMIT` persist a partial result. See [#718](https://github.com/dbt-msft/dbt-sqlserver/issues/718).
225
+
226
+ This is independent of `dbt_sqlserver_use_dbt_transactions` above: that flag decides who owns the transaction boundary (dbt vs. the driver's autocommit), while `xact_abort` decides how the server reacts to a run-time error mid-batch. `XACT_ABORT ON` matters even when there is no explicit transaction at all, which is exactly the configuration `dbt_sqlserver_use_dbt_transactions` offers no protection in — so the two settings are not derived from one another and both need to be considered independently.
227
+
228
+ Turn it off only if a project intentionally relies on continue-on-error batch semantics (e.g. a hook that expects one failing statement in a batch not to abort the rest):
229
+
230
+ ```yaml
231
+ # profiles.yml
232
+ your_profile:
233
+ target: dev
234
+ outputs:
235
+ dev:
236
+ type: sqlserver
237
+ # ...
238
+ xact_abort: false # <-- opt-out; default is true
239
+ ```
240
+
211
241
  ### `column_type_expansion_max_rows`
212
242
 
213
243
  *(default: `1000000`)* Per-model config that limits when safe type expansion runs. When the target table exceeds this row count, safe type expansion is skipped (basic same-family string resizes still proceed). Set to `-1` to disable the check entirely.
@@ -253,6 +283,53 @@ You can also set it per model:
253
283
  {{ config(materialized="table", as_columnstore=false) }}
254
284
  ```
255
285
 
286
+ ### Dynamic Data Masking (`masked_with` / `masks`)
287
+
288
+ The adapter can apply SQL Server [Dynamic Data Masking](https://learn.microsoft.com/en-us/sql/relational-databases/security/dynamic-data-masking) (DDM) to columns as part of the materialization, so masks are re-applied on every build and survive dbt's drop-and-recreate on a full refresh. A principal granted `SELECT` but not `UNMASK` then sees masked values instead of real data (dbt's own build principal, being `db_owner`, keeps `UNMASK` and reads real data). Requires **SQL Server 2016+**.
289
+
290
+ There are two config surfaces, and you can use either or both:
291
+
292
+ **Column-level `masked_with:`** — a first-class column property in schema YAML (like `data_type:` or `constraints:`), whose value is the masking-function string:
293
+
294
+ ```yaml
295
+ # models/schema.yml
296
+ version: 2
297
+ models:
298
+ - name: core_patients
299
+ columns:
300
+ - name: surname
301
+ masked_with: "default()"
302
+ - name: nhs_number
303
+ masked_with: 'partial(0,"XXXXXXXXXX",0)'
304
+ ```
305
+
306
+ **Model-level `masks`** — a `{column: function}` dict, settable in the in-file `{{ config() }}`, the model's `.yml` `config:` block, or a directory-wide default in `dbt_project.yml`. It merges key-wise across those levels (like `meta`), so a directory default and a per-model tweak combine rather than clobber:
307
+
308
+ ```sql
309
+ {{ config(masks={'surname': "default()", 'nhs_number': 'partial(0,"XXXXXXXXXX",0)'}) }}
310
+ ```
311
+
312
+ ```yaml
313
+ # dbt_project.yml — mask nhs_number on every model under datasets/ that has it
314
+ models:
315
+ my_project:
316
+ datasets:
317
+ +masks: { nhs_number: "default()" }
318
+ ```
319
+
320
+ Behaviour:
321
+
322
+ - **Precedence:** when both surfaces target the same column, the column-level `masked_with` wins, and a warning naming the model, column and both functions is emitted (even when they agree). This is not something dbt itself ranks, so the rule is the adapter's: a column is more specific than a model.
323
+ - **Opt out of an inherited default:** set `masked_with: null` on the column to remove a mask inherited from a directory/model-level `masks` entry.
324
+ - **Validation:** a `masks` (or `masked_with`) entry naming a column that is not in the built relation is skipped with a warning (a likely typo or stale rename); the run does not fail.
325
+ - **Unmaskable columns:** computed, `FILESTREAM`, sparse `COLUMN_SET` and `Always Encrypted` columns cannot carry a mask, and the run errors listing them rather than emitting DDL that fails.
326
+ - **Views/ephemeral/seeds:** masks apply to base tables only (`table`, `incremental`, `snapshot`). Views inherit masking from their base tables and cannot carry a mask; **seeds are not currently masked**.
327
+ - **Idempotent:** the adapter diffs the desired masks against `sys.masked_columns` and emits only the `ADD` / change / `DROP MASKED` statements that changed, so a persisted (incremental) re-run with no config change issues no DDL.
328
+
329
+ **Indexes and masking.** SQL Server cannot *add* a mask to a column an index depends on (documented for all versions: `ALTER TABLE ALTER COLUMN … failed because one or more objects access this column`). The adapter avoids this on fresh builds by applying masks **before** it creates (rowstore) indexes — which is exactly Microsoft's documented workaround order (mask, then create the index). The default clustered columnstore index is unaffected (its columns are included, not key columns). On a **persisted** table (incremental/snapshot without full refresh), adding a *new* mask to a column that is already an index key errors with a message pointing to the drop-index → mask → recreate-index workaround.
330
+
331
+ **Version notes.** All masking DDL the adapter emits (`ADD MASKED`, `MASKED WITH`, `DROP MASKED`) and the functions `default()`, `email()`, `random(a,b)` and `partial(...)` work on 2016+. The `datetime()` partial-date function and granular column/schema/table-scoped `UNMASK` are SQL Server 2022+ only; the adapter never emits them, but mask-function strings are passed through verbatim, so using a 2022-only function on an older server will be rejected by SQL Server.
332
+
256
333
  ## Contributing
257
334
 
258
335
  [![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)
@@ -2,8 +2,19 @@
2
2
 
3
3
  [dbt](https://www.getdbt.com) adapter for Microsoft SQL Server and Azure SQL services.
4
4
 
5
- The adapter supports dbt-core 1.10 or newer and follows the same versioning scheme.
6
- E.g. version 1.10.x of the adapter is compatible with dbt-core 1.10.x.
5
+ The adapter supports dbt-core 1.11 or newer and follows the same versioning scheme.
6
+ E.g. version 1.11.x of the adapter is compatible with dbt-core 1.11.x.
7
+
8
+ ## Supported Python versions
9
+
10
+ The adapter is tested against:
11
+
12
+ | Python version | Status |
13
+ |---|---|
14
+ | 3.10 | Installable (not tested in CI) |
15
+ | 3.11 | Officially supported |
16
+ | 3.12 | Officially supported |
17
+ | 3.13 | Officially supported |
7
18
 
8
19
  ## Supported SQL Server versions
9
20
 
@@ -171,6 +182,25 @@ flags:
171
182
  dbt_sqlserver_use_dbt_transactions: true # <-- opt-in; default is false
172
183
  ```
173
184
 
185
+ ### `xact_abort`
186
+
187
+ *(default: `true`)* Profile/connection field. When enabled, the adapter runs `SET XACT_ABORT ON;` once per connection, right after it opens. With `XACT_ABORT ON`, a run-time error partway through a multi-statement batch (e.g. a `NOT NULL`/constraint violation during the DML table refresh's DELETE+INSERT swap) aborts the whole batch and rolls back any open transaction, instead of only aborting the failing statement and letting a trailing `COMMIT` persist a partial result. See [#718](https://github.com/dbt-msft/dbt-sqlserver/issues/718).
188
+
189
+ This is independent of `dbt_sqlserver_use_dbt_transactions` above: that flag decides who owns the transaction boundary (dbt vs. the driver's autocommit), while `xact_abort` decides how the server reacts to a run-time error mid-batch. `XACT_ABORT ON` matters even when there is no explicit transaction at all, which is exactly the configuration `dbt_sqlserver_use_dbt_transactions` offers no protection in — so the two settings are not derived from one another and both need to be considered independently.
190
+
191
+ Turn it off only if a project intentionally relies on continue-on-error batch semantics (e.g. a hook that expects one failing statement in a batch not to abort the rest):
192
+
193
+ ```yaml
194
+ # profiles.yml
195
+ your_profile:
196
+ target: dev
197
+ outputs:
198
+ dev:
199
+ type: sqlserver
200
+ # ...
201
+ xact_abort: false # <-- opt-out; default is true
202
+ ```
203
+
174
204
  ### `column_type_expansion_max_rows`
175
205
 
176
206
  *(default: `1000000`)* Per-model config that limits when safe type expansion runs. When the target table exceeds this row count, safe type expansion is skipped (basic same-family string resizes still proceed). Set to `-1` to disable the check entirely.
@@ -216,6 +246,53 @@ You can also set it per model:
216
246
  {{ config(materialized="table", as_columnstore=false) }}
217
247
  ```
218
248
 
249
+ ### Dynamic Data Masking (`masked_with` / `masks`)
250
+
251
+ The adapter can apply SQL Server [Dynamic Data Masking](https://learn.microsoft.com/en-us/sql/relational-databases/security/dynamic-data-masking) (DDM) to columns as part of the materialization, so masks are re-applied on every build and survive dbt's drop-and-recreate on a full refresh. A principal granted `SELECT` but not `UNMASK` then sees masked values instead of real data (dbt's own build principal, being `db_owner`, keeps `UNMASK` and reads real data). Requires **SQL Server 2016+**.
252
+
253
+ There are two config surfaces, and you can use either or both:
254
+
255
+ **Column-level `masked_with:`** — a first-class column property in schema YAML (like `data_type:` or `constraints:`), whose value is the masking-function string:
256
+
257
+ ```yaml
258
+ # models/schema.yml
259
+ version: 2
260
+ models:
261
+ - name: core_patients
262
+ columns:
263
+ - name: surname
264
+ masked_with: "default()"
265
+ - name: nhs_number
266
+ masked_with: 'partial(0,"XXXXXXXXXX",0)'
267
+ ```
268
+
269
+ **Model-level `masks`** — a `{column: function}` dict, settable in the in-file `{{ config() }}`, the model's `.yml` `config:` block, or a directory-wide default in `dbt_project.yml`. It merges key-wise across those levels (like `meta`), so a directory default and a per-model tweak combine rather than clobber:
270
+
271
+ ```sql
272
+ {{ config(masks={'surname': "default()", 'nhs_number': 'partial(0,"XXXXXXXXXX",0)'}) }}
273
+ ```
274
+
275
+ ```yaml
276
+ # dbt_project.yml — mask nhs_number on every model under datasets/ that has it
277
+ models:
278
+ my_project:
279
+ datasets:
280
+ +masks: { nhs_number: "default()" }
281
+ ```
282
+
283
+ Behaviour:
284
+
285
+ - **Precedence:** when both surfaces target the same column, the column-level `masked_with` wins, and a warning naming the model, column and both functions is emitted (even when they agree). This is not something dbt itself ranks, so the rule is the adapter's: a column is more specific than a model.
286
+ - **Opt out of an inherited default:** set `masked_with: null` on the column to remove a mask inherited from a directory/model-level `masks` entry.
287
+ - **Validation:** a `masks` (or `masked_with`) entry naming a column that is not in the built relation is skipped with a warning (a likely typo or stale rename); the run does not fail.
288
+ - **Unmaskable columns:** computed, `FILESTREAM`, sparse `COLUMN_SET` and `Always Encrypted` columns cannot carry a mask, and the run errors listing them rather than emitting DDL that fails.
289
+ - **Views/ephemeral/seeds:** masks apply to base tables only (`table`, `incremental`, `snapshot`). Views inherit masking from their base tables and cannot carry a mask; **seeds are not currently masked**.
290
+ - **Idempotent:** the adapter diffs the desired masks against `sys.masked_columns` and emits only the `ADD` / change / `DROP MASKED` statements that changed, so a persisted (incremental) re-run with no config change issues no DDL.
291
+
292
+ **Indexes and masking.** SQL Server cannot *add* a mask to a column an index depends on (documented for all versions: `ALTER TABLE ALTER COLUMN … failed because one or more objects access this column`). The adapter avoids this on fresh builds by applying masks **before** it creates (rowstore) indexes — which is exactly Microsoft's documented workaround order (mask, then create the index). The default clustered columnstore index is unaffected (its columns are included, not key columns). On a **persisted** table (incremental/snapshot without full refresh), adding a *new* mask to a column that is already an index key errors with a message pointing to the drop-index → mask → recreate-index workaround.
293
+
294
+ **Version notes.** All masking DDL the adapter emits (`ADD MASKED`, `MASKED WITH`, `DROP MASKED`) and the functions `default()`, `email()`, `random(a,b)` and `partial(...)` work on 2016+. The `datetime()` partial-date function and granular column/schema/table-scoped `UNMASK` are SQL Server 2022+ only; the adapter never emits them, but mask-function strings are passed through verbatim, so using a 2022-only function on an older server will be rejected by SQL Server.
295
+
219
296
  ## Contributing
220
297
 
221
298
  [![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)
@@ -0,0 +1 @@
1
+ version = "1.11.0"
@@ -11,6 +11,7 @@ class SQLServerRelationType(StrEnum):
11
11
  Table = "table"
12
12
  View = "view"
13
13
  CTE = "cte"
14
+ Function = "function"
14
15
 
15
16
 
16
17
  class SQLServerIncludePolicy(Policy):
@@ -1,4 +1,4 @@
1
- from typing import Any, List, Optional
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
3
  import agate
4
4
  import dbt_common.exceptions
@@ -29,6 +29,9 @@ from dbt.adapters.sqlserver.relation_configs.index import (
29
29
  from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn, SQLServerColumnNative
30
30
  from dbt.adapters.sqlserver.sqlserver_configs import SQLServerConfigs
31
31
  from dbt.adapters.sqlserver.sqlserver_connections import SQLServerConnectionManager
32
+ from dbt.adapters.sqlserver.sqlserver_mask import ColumnMask
33
+ from dbt.adapters.sqlserver.sqlserver_mask import mask_changes as _mask_changes
34
+ from dbt.adapters.sqlserver.sqlserver_mask import resolve_masks as _resolve_masks
32
35
  from dbt.adapters.sqlserver.sqlserver_relation import SQLServerRelation
33
36
 
34
37
  logger = AdapterLogger("SQLServer")
@@ -74,14 +77,6 @@ class SQLServerAdapter(SQLAdapter):
74
77
  @property
75
78
  def _behavior_flags(self) -> List[BehaviorFlag]:
76
79
  return [
77
- {
78
- "name": "empty",
79
- "default": False,
80
- "description": (
81
- "When enabled, table and view materializations will be created as empty "
82
- "structures (no data)."
83
- ),
84
- },
85
80
  {
86
81
  "name": "dbt_sqlserver_use_default_schema_concat",
87
82
  "default": False,
@@ -424,6 +419,57 @@ class SQLServerAdapter(SQLAdapter):
424
419
  def parse_index(self, raw_index: Any) -> Optional[SQLServerIndexConfig]:
425
420
  return SQLServerIndexConfig.parse(raw_index)
426
421
 
422
+ @available
423
+ def index_needs_own_batch(self, raw_index: Any) -> bool:
424
+ """True when raw_index's build_options (ONLINE / RESUMABLE) force its
425
+ CREATE INDEX to run outside any transaction: SQL Server rejects
426
+ RESUMABLE inside a user transaction (error 574), and an ONLINE build
427
+ wrapped in one holds its locks until commit, negating the point."""
428
+ parsed = self.parse_index(raw_index)
429
+ if not parsed:
430
+ return False
431
+ return create_needs_own_batch(parsed.build_options)
432
+
433
+ @available
434
+ def commit_if_open(self) -> None:
435
+ """Commit the current transaction if one is open - a no-op otherwise.
436
+
437
+ dbt_sqlserver_use_dbt_transactions on (default) wraps a
438
+ materialization's whole build, from its first statement through its
439
+ own trailing ``adapter.commit()``, in one continuous ambient
440
+ transaction. Some statements must not share that transaction with
441
+ whatever runs after them: an ONLINE/RESUMABLE index build (SQL Server
442
+ rejects RESUMABLE inside a user transaction outright, and ONLINE
443
+ holds its locks until commit either way), or a full-refresh-in-
444
+ progress marker, which exists specifically to survive a later
445
+ failure and so must not roll back with it. This makes the prior
446
+ statement durable on its own; pair with begin_if_closed once the
447
+ statement(s) that must run outside a transaction are done, to leave
448
+ later code (more such statements, or the materialization's own
449
+ trailing ``adapter.commit()``, which raises if it finds nothing open)
450
+ working as if this call had never happened.
451
+
452
+ A no-op when no transaction is open: the caller's statement already
453
+ ran autocommitted on its own (e.g. via ``run_query``'s
454
+ ``auto_begin=false``, before anything else began one), so there is
455
+ nothing to flush. Also a no-op, at the SQL level, whenever
456
+ dbt_sqlserver_use_dbt_transactions is off: begin/commit still flip
457
+ dbt-core's bookkeeping (see
458
+ SQLServerConnectionManager.add_begin_query/add_commit_query), but
459
+ emit no real T-SQL, matching the driver's own autocommit.
460
+ """
461
+ connection = self.connections.get_thread_connection()
462
+ if connection is not None and connection.transaction_open:
463
+ self.connections.commit()
464
+
465
+ @available
466
+ def begin_if_closed(self) -> None:
467
+ """Begin a transaction if none is open - a no-op otherwise. See
468
+ commit_if_open, which this pairs with."""
469
+ connection = self.connections.get_thread_connection()
470
+ if connection is not None and not connection.transaction_open:
471
+ self.connections.begin()
472
+
427
473
  @available
428
474
  def validate_indexes(
429
475
  self, raw_indexes: Any, as_columnstore: Any = False, drop_unmanaged: Any = False
@@ -502,6 +548,64 @@ class SQLServerAdapter(SQLAdapter):
502
548
  "warnings": warnings,
503
549
  }
504
550
 
551
+ @available
552
+ def resolve_masks(self, model: Any, model_masks: Optional[dict] = None) -> Dict[str, str]:
553
+ """Merge the column-level `masked_with` and model-level `masks` surfaces
554
+ into one `{column: function}` map for `apply_masks`.
555
+
556
+ `model` is the Jinja `model` dict (`node.to_dict()`), whose `columns`
557
+ carry any `masked_with` as a flattened key (an explicit `masked_with:
558
+ null` survives serialization as a present `None`, signalling opt-out).
559
+ `model_masks` is `config.get('masks')` — already surface-merged by dbt.
560
+ Precedence and conflict warnings are handled here; key existence is
561
+ validated later in the macro against the real relation.
562
+ """
563
+ model = model or {}
564
+ columns = model.get("columns") or {}
565
+ column_masks = []
566
+ for name, col in columns.items():
567
+ col = col or {}
568
+ column_masks.append(
569
+ ColumnMask(
570
+ name=col.get("name", name),
571
+ masked_with_present=("masked_with" in col),
572
+ masked_with=col.get("masked_with"),
573
+ )
574
+ )
575
+ model_name = model.get("name") or model.get("alias") or "<unknown>"
576
+ mask_map, warnings = _resolve_masks(column_masks, model_masks, model_name)
577
+ for warning in warnings:
578
+ logger.warning(warning)
579
+ return mask_map
580
+
581
+ @available
582
+ def mask_changes(
583
+ self,
584
+ existing_masks: Any,
585
+ mask_config: Optional[dict],
586
+ index_key_columns: Any = None,
587
+ existing_columns: Any = None,
588
+ ) -> dict:
589
+ """Diff a resolved mask map against current `sys.masked_columns` state.
590
+
591
+ `existing_masks` is the agate table from `get_show_mask_sql` (columns
592
+ `name`, `masking_function`). Returns plain lists for jinja: `adds` /
593
+ `changes` (each `[column, function]`), `drops` (column names), `skipped`
594
+ (warnings for columns absent from the relation) and `errors` (an ADD onto
595
+ a current index-key column, which SQL Server rejects). The macro emits
596
+ DDL for adds/changes/drops, logs `skipped`, and raises on `errors`."""
597
+ rows = []
598
+ if existing_masks is not None:
599
+ column_names = existing_masks.column_names
600
+ for row in existing_masks.rows:
601
+ rows.append(dict(zip(column_names, row)))
602
+ return _mask_changes(
603
+ rows,
604
+ mask_config or {},
605
+ set(index_key_columns or []),
606
+ existing_columns=(list(existing_columns) if existing_columns is not None else None),
607
+ )
608
+
505
609
 
506
610
  COLUMNS_EQUAL_SQL = """
507
611
  with diff_count as (
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict, Optional, Tuple
3
+
4
+ from dbt_common.contracts.config.base import MergeBehavior
5
+
6
+ from dbt.adapters.protocol import AdapterConfig
7
+ from dbt.adapters.sqlserver.relation_configs import SQLServerIndexConfig
8
+
9
+
10
+ @dataclass
11
+ class SQLServerConfigs(AdapterConfig):
12
+ auto_provision_aad_principals: Optional[bool] = False
13
+ prefer_single_alter_column: Optional[bool] = False
14
+ column_type_expansion_max_rows: int = 1000000
15
+ indexes: Optional[Tuple[SQLServerIndexConfig, ...]] = None
16
+ # false (default) | warn | true - how index reconciliation treats
17
+ # droppable indexes dbt didn't create (YAML may supply bool or str)
18
+ drop_unmanaged_indexes: Optional[Any] = False
19
+ # column-name -> DDM masking-function map for the model-level `masks`
20
+ # surface. MergeBehavior.Update makes it merge key-wise across the config
21
+ # chain (dbt_project.yml +masks defaults, .yml config, in-file config())
22
+ # the same way `meta` composes, rather than the default clobber — so a
23
+ # directory-level default and a per-model tweak combine instead of one
24
+ # replacing the whole dict.
25
+ masks: Optional[Dict[str, Any]] = field(
26
+ default_factory=dict, metadata=MergeBehavior.Update.meta()
27
+ )
@@ -65,6 +65,10 @@ logger = AdapterLogger("sqlserver")
65
65
  # Connection so cancel() can reach it from another thread. See cancel().
66
66
  _IN_FLIGHT_CURSOR_ATTR = "_dbt_sqlserver_in_flight_cursor"
67
67
 
68
+ # Fires once per process (not per connection/thread) when xact_abort is
69
+ # disabled. See SQLServerConnectionManager._warn_xact_abort_disabled_once.
70
+ _xact_abort_warning_logged = False
71
+
68
72
 
69
73
  class SQLServerConnectionManager(SQLConnectionManager):
70
74
  TYPE = "sqlserver"
@@ -144,8 +148,66 @@ class SQLServerConnectionManager(SQLConnectionManager):
144
148
  retryable_exceptions=retryable_exceptions,
145
149
  )
146
150
 
151
+ if conn.state == ConnectionState.OPEN:
152
+ if credentials.xact_abort:
153
+ cls._apply_session_settings(conn)
154
+ else:
155
+ cls._warn_xact_abort_disabled_once()
156
+
147
157
  return conn
148
158
 
159
+ @classmethod
160
+ def _apply_session_settings(cls, connection: Connection) -> None:
161
+ """Set session-level defaults that must hold for every batch this
162
+ connection ever runs. Currently: XACT_ABORT, so a run-time error
163
+ mid-batch (e.g. a NOT NULL violation in a DELETE+INSERT swap) kills
164
+ the batch and rolls back any open transaction instead of falling
165
+ through to a trailing COMMIT. See dbt-msft/dbt-sqlserver#718.
166
+
167
+ A connection that fails this setup is not usable: it is closed and
168
+ the error is re-raised rather than handed back to dbt.
169
+ """
170
+ try:
171
+ cursor = connection.handle.cursor()
172
+ try:
173
+ cursor.execute("SET XACT_ABORT ON;")
174
+ finally:
175
+ cursor.close()
176
+
177
+ if not connection.handle.autocommit:
178
+ connection.handle.commit()
179
+ except Exception:
180
+ connection.handle.close()
181
+ connection.handle = None
182
+ connection.state = ConnectionState.FAIL
183
+ raise
184
+
185
+ @classmethod
186
+ def _warn_xact_abort_disabled_once(cls) -> None:
187
+ global _xact_abort_warning_logged
188
+ if _xact_abort_warning_logged:
189
+ return
190
+ _xact_abort_warning_logged = True
191
+
192
+ use_dbt_transactions = cls._dbt_sqlserver_use_dbt_transactions
193
+ msg = (
194
+ "xact_abort is disabled (xact_abort: false in the profile). Without "
195
+ "SET XACT_ABORT ON, a run-time error partway through a multi-statement "
196
+ "batch (e.g. the DELETE+INSERT swap in the DML refresh materialization) "
197
+ "aborts only the failing statement, not the batch, so a trailing COMMIT "
198
+ "can still commit a partial result. "
199
+ f"dbt_sqlserver_use_dbt_transactions is currently {use_dbt_transactions}"
200
+ )
201
+ if not use_dbt_transactions:
202
+ msg += (
203
+ " (dbt-managed transactions are off): the DML refresh materialization "
204
+ "emits its own in-batch BEGIN/COMMIT, and that in-batch swap can commit "
205
+ "a partial result in this configuration."
206
+ )
207
+ else:
208
+ msg += "."
209
+ logger.warning(msg)
210
+
149
211
  def cancel(self, connection: Connection) -> None:
150
212
  """Cancel the in-flight query on ``connection``, if any.
151
213
 
@@ -56,6 +56,7 @@ class SQLServerCredentials(Credentials):
56
56
  schema_authorization: Optional[str] = None
57
57
  login_timeout: Optional[int] = 0
58
58
  query_timeout: Optional[int] = 0
59
+ xact_abort: bool = True
59
60
 
60
61
  _ALIASES = {
61
62
  "user": "UID",
@@ -104,6 +105,7 @@ class SQLServerCredentials(Credentials):
104
105
  "encrypt",
105
106
  "trust_cert",
106
107
  "backend",
108
+ "xact_abort",
107
109
  )
108
110
 
109
111
  if self.backend == SQLServerBackend.pyodbc: