dbt-sqlserver 1.11.1__tar.gz → 1.11.2rc1__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.
- {dbt_sqlserver-1.11.1/dbt_sqlserver.egg-info → dbt_sqlserver-1.11.2rc1}/PKG-INFO +103 -1
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/README.md +102 -0
- dbt_sqlserver-1.11.2rc1/dbt/adapters/sqlserver/__version__.py +1 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_adapter.py +210 -32
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental.sql +4 -0
- dbt_sqlserver-1.11.2rc1/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql +79 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/table/table.sql +4 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql +35 -1
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/unit_test/unit_test_create_table_as.sql +5 -1
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/view/view.sql +15 -1
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1/dbt_sqlserver.egg-info}/PKG-INFO +103 -1
- dbt_sqlserver-1.11.1/dbt/adapters/sqlserver/__version__.py +0 -1
- dbt_sqlserver-1.11.1/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql +0 -30
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/LICENSE +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/__init__.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/__init__.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/py.typed +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/__init__.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/index.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/policies.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_auth.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_backend.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_column.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_configs.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_connections.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_constants.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_credentials.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_helpers.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_mask.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_relation.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_runtime.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/__init__.py +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/dbt_project.yml +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/apply_grants.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/apply_masks.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/catalog.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/columns.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/indexes.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/metadata.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/persist_docs.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/relation.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/schema.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/show.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/validate_sql.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/functions/helpers.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/functions/scalar.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/hooks.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/incremental/merge.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/unit_test/get_fixture_sql.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/models/view/create_view_as.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/snapshots/helpers.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot_merge.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/snapshots/strategies.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/tests.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/materializations/unit_tests.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/relations/seeds/helpers.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/relations/table/clone.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/relations/table/create.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/relations/views/create.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/any_value.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/array_construct.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/cast_bool_to_text.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/concat.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/date_trunc.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/dateadd.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/get_tables_by_pattern.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/hash.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/last_day.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/length.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/listagg.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/position.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/safe_cast.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/split_part.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/timestamps.sql +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/profile_template.yml +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt_sqlserver.egg-info/SOURCES.txt +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt_sqlserver.egg-info/dependency_links.txt +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt_sqlserver.egg-info/requires.txt +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt_sqlserver.egg-info/top_level.txt +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/pyproject.toml +0 -0
- {dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbt-sqlserver
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.2rc1
|
|
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
|
|
@@ -283,6 +283,108 @@ You can also set it per model:
|
|
|
283
283
|
{{ config(materialized="table", as_columnstore=false) }}
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
With `table_refresh_method: dml`, a schema change makes the refresh fall back to a rename-swap. On that run — and only that run — the scratch table is rebuilt the way this adapter builds every other table, so it carries the model's columnstore index, and under an enforced contract its `NOT NULL`s and inline constraints, into the swap. That run therefore executes the model's SQL twice — once for the `SELECT … INTO` that probes for the schema change, once for the rebuild — and builds the columnstore index once. Steady-state refreshes are unaffected and keep the single `SELECT … INTO`. A table that lost its columnstore index to this bug before you upgraded is not repaired automatically: its schema still matches, so it stays on the cheap path. To rebuild it, temporarily set `full_refresh_build: prebuilt` and run with `--full-refresh`.
|
|
287
|
+
|
|
288
|
+
### Constraints
|
|
289
|
+
|
|
290
|
+
Constraints declared in a model's yaml are applied when — and only when — the model's [contract](https://docs.getdbt.com/reference/resource-configs/contract) is enforced, which is what every dbt adapter does and keeps their cost opt-in. (dbt-core does not raise if you declare constraints with the contract off — they are simply never emitted.) `not_null`, `check`, `unique`, `primary_key` and `foreign_key` are all supported.
|
|
291
|
+
|
|
292
|
+
**Where a constraint lands depends on whether you name it.**
|
|
293
|
+
|
|
294
|
+
An unnamed constraint is rendered inline in the `CREATE TABLE` column list and SQL Server names it (`PK__my_model__3213E83F…`). It is validated as the table is built, so a violation fails before the new table is swapped in and the previous one is left untouched.
|
|
295
|
+
|
|
296
|
+
A *model-level* constraint carrying `name:` is applied by `ALTER TABLE … ADD CONSTRAINT` after the build swaps the new table into place and drops the old one. SQL Server scopes constraint names per schema, and a table is built alongside the one it replaces, so that is the first moment the name is free to reuse — naming a constraint inline would collide with the outgoing table (`Msg 2714`) on every rebuild after the first. The trade-off is that this runs after the model has committed: if the data violates the constraint, the model fails with the table already in place but unconstrained, and — as with any failure this late in a build — `post_hook`s declared with `transaction: false` do not run.
|
|
297
|
+
|
|
298
|
+
Name a constraint when you want it stable across environments (schema-comparison tools report the generated names as differences) or need to reference it later. A `name:` on a *column-level* constraint is ignored with a warning — declare it under the model's `constraints:` key instead.
|
|
299
|
+
|
|
300
|
+
The full set of rules, with the test that verifies each, is in [docs/constraints.md](docs/constraints.md).
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
models:
|
|
304
|
+
- name: fact_sales
|
|
305
|
+
config:
|
|
306
|
+
contract:
|
|
307
|
+
enforced: true
|
|
308
|
+
constraints:
|
|
309
|
+
# named: applied by ALTER TABLE after the swap
|
|
310
|
+
- type: primary_key
|
|
311
|
+
name: PK_fact_sales
|
|
312
|
+
columns: [sale_id]
|
|
313
|
+
- type: foreign_key
|
|
314
|
+
name: FK_fact_sales_customer
|
|
315
|
+
columns: [customer_id]
|
|
316
|
+
to: ref('dim_customer')
|
|
317
|
+
to_columns: [customer_id]
|
|
318
|
+
# unnamed: rendered into the CREATE TABLE
|
|
319
|
+
- type: check
|
|
320
|
+
expression: amount >= 0
|
|
321
|
+
columns:
|
|
322
|
+
- name: sale_id
|
|
323
|
+
data_type: int
|
|
324
|
+
constraints:
|
|
325
|
+
- type: not_null
|
|
326
|
+
- name: customer_id
|
|
327
|
+
data_type: int
|
|
328
|
+
constraints:
|
|
329
|
+
- type: not_null
|
|
330
|
+
- name: amount
|
|
331
|
+
data_type: decimal(18,2)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Clustering
|
|
335
|
+
|
|
336
|
+
`primary_key` and `unique` are emitted as `NONCLUSTERED` by default so they can coexist with the clustered columnstore index built for [`as_columnstore`](#as_columnstore). Use dbt's own `expression` field to ask for something else:
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
constraints:
|
|
340
|
+
- type: primary_key
|
|
341
|
+
name: PK_fact_sales
|
|
342
|
+
columns: [sale_id]
|
|
343
|
+
expression: clustered # requires as_columnstore: false
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`clustered` and `nonclustered` are the only values understood here. `expression` is free text that dbt splices between the keyword and the column list, which is the one place T-SQL accepts nothing else — index options such as `with (fillfactor = 90)` belong on a separate index, not on the constraint.
|
|
347
|
+
|
|
348
|
+
#### Foreign keys
|
|
349
|
+
|
|
350
|
+
Two things are worth knowing before adding them:
|
|
351
|
+
|
|
352
|
+
- **They are not free at build time.** Every load is validated against them; add them where you want the guarantee, not everywhere the relationship exists.
|
|
353
|
+
- **A foreign key pointing at a model blocks that model's rebuild.** The build renames the outgoing table to a backup and drops it, but the child's foreign key follows the renamed object, so the drop fails with `Msg 3726`. SQL Server has no `DROP TABLE … CASCADE`.
|
|
354
|
+
|
|
355
|
+
The adapter ships a macro for exactly this, meant as a `pre_hook` on the **referenced** (parent) model:
|
|
356
|
+
|
|
357
|
+
```sql
|
|
358
|
+
{{ config(pre_hook="{{ drop_fk_constraints() }}") }}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
It drops the foreign keys in both directions — the inbound ones other tables hold against this model, and this model's own outbound ones — so the rebuild's backup drop succeeds. The trade-off is explicit and worth stating: **the child's foreign key does not exist between the parent's rebuild and the child's next build.** (dbt-postgres makes the same trade silently, by issuing every `drop table` with `cascade`.)
|
|
362
|
+
|
|
363
|
+
Add the hook *before* the first rebuild. Once a rebuild has failed with `Msg 3726`, the new table is already in place but without its own named constraints — those are applied after the backup drop that failed — and the child's key now points at `<model>__dbt_backup`, because a foreign key follows the object, not the name. A plain `dbt run` does not recover: the parent trips over the same backup and the child is skipped behind it. Rebuild the child alone — that run fails too, since the parent has no key to reference, but its swap drops the old child and the stale key with it — and a plain `dbt run` then rebuilds both in order. Dropping the child's key by hand (`ALTER TABLE <child> DROP CONSTRAINT <name>`) does the same.
|
|
364
|
+
|
|
365
|
+
`table_refresh_method: dml` is *not* a workaround. Its steady-state refresh issues `DELETE FROM <parent>`, which fails with `Msg 547` as soon as the child holds referencing rows, and its schema-change path falls back to the same rename-swap, hitting `Msg 3726` anyway.
|
|
366
|
+
|
|
367
|
+
- **SQL Server has no cross-database foreign keys.** `to: ref(...)` resolves to a fully qualified relation, database included, which SQL Server accepts as long as it names the current database. A target in another database fails with `references invalid table`.
|
|
368
|
+
|
|
369
|
+
A foreign key also does not order the build on its own: add an explicit `-- depends_on: {{ ref('dim_customer') }}` to the child model so the parent is built first.
|
|
370
|
+
|
|
371
|
+
#### Changing a constraint after the first build
|
|
372
|
+
|
|
373
|
+
Named constraints are applied by an `ALTER TABLE` whose `ADD` is guarded on the name already being present on the table, so:
|
|
374
|
+
|
|
375
|
+
- **Adding** a constraint to a model that already exists lands on its next run — no `--full-refresh` needed, on `table` and `incremental` alike.
|
|
376
|
+
- **Changing** an existing constraint's definition while keeping its name is *not* detected: a constraint name, unlike a `dbt_idx_` index name, says nothing about what the constraint does. Run `--full-refresh` to apply the new definition; that rebuilds the table, so the constraint is created fresh. (A `table` model rebuilds on every run and needs nothing special.)
|
|
377
|
+
- **Renaming** a constraint on a table that persists across runs adds the new name beside the old one. For a `check`, `unique` or `foreign_key` that means a duplicate; for a `primary_key` the run fails with `Msg 1779` (*table already has a primary key defined on it*) until the old one is dropped. Rename with `--full-refresh`.
|
|
378
|
+
- **Removing** a constraint from the yaml does not drop it from the database. Drop it yourself, or `--full-refresh`.
|
|
379
|
+
|
|
380
|
+
Every bullet above is about *named* constraints. Unnamed ones ride the `CREATE TABLE`, so they follow the table and change only when the table is rebuilt — on a materialization whose table persists across runs (`incremental` in its steady state, `table_refresh_method: dml`), adding an unnamed constraint to an existing model does nothing until a `--full-refresh`, and the run still reports success. Name it, or full-refresh.
|
|
381
|
+
|
|
382
|
+
#### Build-shape notes
|
|
383
|
+
|
|
384
|
+
An *unnamed* `primary_key` or `unique` constraint on a column that also carries a [data mask](#dynamic-data-masking-masked_with--masks) is rejected by the build. The constraint rides the `CREATE TABLE`, so its index already exists by the time `apply_masks` runs, and the adapter refuses to mask any column that an index has as a key: *is configured for masking but is also an index key column*. Declare that constraint at the model level **with a `name:`** instead — named constraints are applied by `ALTER TABLE` after the masks are in place, which the adapter allows.
|
|
385
|
+
|
|
386
|
+
A contract-enforced model is always loaded as `CREATE TABLE` followed by `INSERT … WITH (TABLOCK)`, on every build path. A `primary_key` or `unique` constraint — named or not — puts a nonclustered index on that table *before* the load, so the index is maintained row by row while the rows go in, and that part of the load is fully logged where an index-free heap would have been minimally logged. The cost lands on every rebuild of the model; it is most visible with `full_refresh_build: prebuilt`, whose point is a cheap bulk load. If a model's load time matters, weigh the key constraints against it; `check`, `not_null` and `foreign_key` do not create indexes and do not carry this cost.
|
|
387
|
+
|
|
286
388
|
### Dynamic Data Masking (`masked_with` / `masks`)
|
|
287
389
|
|
|
288
390
|
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+**.
|
|
@@ -246,6 +246,108 @@ You can also set it per model:
|
|
|
246
246
|
{{ config(materialized="table", as_columnstore=false) }}
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
With `table_refresh_method: dml`, a schema change makes the refresh fall back to a rename-swap. On that run — and only that run — the scratch table is rebuilt the way this adapter builds every other table, so it carries the model's columnstore index, and under an enforced contract its `NOT NULL`s and inline constraints, into the swap. That run therefore executes the model's SQL twice — once for the `SELECT … INTO` that probes for the schema change, once for the rebuild — and builds the columnstore index once. Steady-state refreshes are unaffected and keep the single `SELECT … INTO`. A table that lost its columnstore index to this bug before you upgraded is not repaired automatically: its schema still matches, so it stays on the cheap path. To rebuild it, temporarily set `full_refresh_build: prebuilt` and run with `--full-refresh`.
|
|
250
|
+
|
|
251
|
+
### Constraints
|
|
252
|
+
|
|
253
|
+
Constraints declared in a model's yaml are applied when — and only when — the model's [contract](https://docs.getdbt.com/reference/resource-configs/contract) is enforced, which is what every dbt adapter does and keeps their cost opt-in. (dbt-core does not raise if you declare constraints with the contract off — they are simply never emitted.) `not_null`, `check`, `unique`, `primary_key` and `foreign_key` are all supported.
|
|
254
|
+
|
|
255
|
+
**Where a constraint lands depends on whether you name it.**
|
|
256
|
+
|
|
257
|
+
An unnamed constraint is rendered inline in the `CREATE TABLE` column list and SQL Server names it (`PK__my_model__3213E83F…`). It is validated as the table is built, so a violation fails before the new table is swapped in and the previous one is left untouched.
|
|
258
|
+
|
|
259
|
+
A *model-level* constraint carrying `name:` is applied by `ALTER TABLE … ADD CONSTRAINT` after the build swaps the new table into place and drops the old one. SQL Server scopes constraint names per schema, and a table is built alongside the one it replaces, so that is the first moment the name is free to reuse — naming a constraint inline would collide with the outgoing table (`Msg 2714`) on every rebuild after the first. The trade-off is that this runs after the model has committed: if the data violates the constraint, the model fails with the table already in place but unconstrained, and — as with any failure this late in a build — `post_hook`s declared with `transaction: false` do not run.
|
|
260
|
+
|
|
261
|
+
Name a constraint when you want it stable across environments (schema-comparison tools report the generated names as differences) or need to reference it later. A `name:` on a *column-level* constraint is ignored with a warning — declare it under the model's `constraints:` key instead.
|
|
262
|
+
|
|
263
|
+
The full set of rules, with the test that verifies each, is in [docs/constraints.md](docs/constraints.md).
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
models:
|
|
267
|
+
- name: fact_sales
|
|
268
|
+
config:
|
|
269
|
+
contract:
|
|
270
|
+
enforced: true
|
|
271
|
+
constraints:
|
|
272
|
+
# named: applied by ALTER TABLE after the swap
|
|
273
|
+
- type: primary_key
|
|
274
|
+
name: PK_fact_sales
|
|
275
|
+
columns: [sale_id]
|
|
276
|
+
- type: foreign_key
|
|
277
|
+
name: FK_fact_sales_customer
|
|
278
|
+
columns: [customer_id]
|
|
279
|
+
to: ref('dim_customer')
|
|
280
|
+
to_columns: [customer_id]
|
|
281
|
+
# unnamed: rendered into the CREATE TABLE
|
|
282
|
+
- type: check
|
|
283
|
+
expression: amount >= 0
|
|
284
|
+
columns:
|
|
285
|
+
- name: sale_id
|
|
286
|
+
data_type: int
|
|
287
|
+
constraints:
|
|
288
|
+
- type: not_null
|
|
289
|
+
- name: customer_id
|
|
290
|
+
data_type: int
|
|
291
|
+
constraints:
|
|
292
|
+
- type: not_null
|
|
293
|
+
- name: amount
|
|
294
|
+
data_type: decimal(18,2)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Clustering
|
|
298
|
+
|
|
299
|
+
`primary_key` and `unique` are emitted as `NONCLUSTERED` by default so they can coexist with the clustered columnstore index built for [`as_columnstore`](#as_columnstore). Use dbt's own `expression` field to ask for something else:
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
constraints:
|
|
303
|
+
- type: primary_key
|
|
304
|
+
name: PK_fact_sales
|
|
305
|
+
columns: [sale_id]
|
|
306
|
+
expression: clustered # requires as_columnstore: false
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`clustered` and `nonclustered` are the only values understood here. `expression` is free text that dbt splices between the keyword and the column list, which is the one place T-SQL accepts nothing else — index options such as `with (fillfactor = 90)` belong on a separate index, not on the constraint.
|
|
310
|
+
|
|
311
|
+
#### Foreign keys
|
|
312
|
+
|
|
313
|
+
Two things are worth knowing before adding them:
|
|
314
|
+
|
|
315
|
+
- **They are not free at build time.** Every load is validated against them; add them where you want the guarantee, not everywhere the relationship exists.
|
|
316
|
+
- **A foreign key pointing at a model blocks that model's rebuild.** The build renames the outgoing table to a backup and drops it, but the child's foreign key follows the renamed object, so the drop fails with `Msg 3726`. SQL Server has no `DROP TABLE … CASCADE`.
|
|
317
|
+
|
|
318
|
+
The adapter ships a macro for exactly this, meant as a `pre_hook` on the **referenced** (parent) model:
|
|
319
|
+
|
|
320
|
+
```sql
|
|
321
|
+
{{ config(pre_hook="{{ drop_fk_constraints() }}") }}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
It drops the foreign keys in both directions — the inbound ones other tables hold against this model, and this model's own outbound ones — so the rebuild's backup drop succeeds. The trade-off is explicit and worth stating: **the child's foreign key does not exist between the parent's rebuild and the child's next build.** (dbt-postgres makes the same trade silently, by issuing every `drop table` with `cascade`.)
|
|
325
|
+
|
|
326
|
+
Add the hook *before* the first rebuild. Once a rebuild has failed with `Msg 3726`, the new table is already in place but without its own named constraints — those are applied after the backup drop that failed — and the child's key now points at `<model>__dbt_backup`, because a foreign key follows the object, not the name. A plain `dbt run` does not recover: the parent trips over the same backup and the child is skipped behind it. Rebuild the child alone — that run fails too, since the parent has no key to reference, but its swap drops the old child and the stale key with it — and a plain `dbt run` then rebuilds both in order. Dropping the child's key by hand (`ALTER TABLE <child> DROP CONSTRAINT <name>`) does the same.
|
|
327
|
+
|
|
328
|
+
`table_refresh_method: dml` is *not* a workaround. Its steady-state refresh issues `DELETE FROM <parent>`, which fails with `Msg 547` as soon as the child holds referencing rows, and its schema-change path falls back to the same rename-swap, hitting `Msg 3726` anyway.
|
|
329
|
+
|
|
330
|
+
- **SQL Server has no cross-database foreign keys.** `to: ref(...)` resolves to a fully qualified relation, database included, which SQL Server accepts as long as it names the current database. A target in another database fails with `references invalid table`.
|
|
331
|
+
|
|
332
|
+
A foreign key also does not order the build on its own: add an explicit `-- depends_on: {{ ref('dim_customer') }}` to the child model so the parent is built first.
|
|
333
|
+
|
|
334
|
+
#### Changing a constraint after the first build
|
|
335
|
+
|
|
336
|
+
Named constraints are applied by an `ALTER TABLE` whose `ADD` is guarded on the name already being present on the table, so:
|
|
337
|
+
|
|
338
|
+
- **Adding** a constraint to a model that already exists lands on its next run — no `--full-refresh` needed, on `table` and `incremental` alike.
|
|
339
|
+
- **Changing** an existing constraint's definition while keeping its name is *not* detected: a constraint name, unlike a `dbt_idx_` index name, says nothing about what the constraint does. Run `--full-refresh` to apply the new definition; that rebuilds the table, so the constraint is created fresh. (A `table` model rebuilds on every run and needs nothing special.)
|
|
340
|
+
- **Renaming** a constraint on a table that persists across runs adds the new name beside the old one. For a `check`, `unique` or `foreign_key` that means a duplicate; for a `primary_key` the run fails with `Msg 1779` (*table already has a primary key defined on it*) until the old one is dropped. Rename with `--full-refresh`.
|
|
341
|
+
- **Removing** a constraint from the yaml does not drop it from the database. Drop it yourself, or `--full-refresh`.
|
|
342
|
+
|
|
343
|
+
Every bullet above is about *named* constraints. Unnamed ones ride the `CREATE TABLE`, so they follow the table and change only when the table is rebuilt — on a materialization whose table persists across runs (`incremental` in its steady state, `table_refresh_method: dml`), adding an unnamed constraint to an existing model does nothing until a `--full-refresh`, and the run still reports success. Name it, or full-refresh.
|
|
344
|
+
|
|
345
|
+
#### Build-shape notes
|
|
346
|
+
|
|
347
|
+
An *unnamed* `primary_key` or `unique` constraint on a column that also carries a [data mask](#dynamic-data-masking-masked_with--masks) is rejected by the build. The constraint rides the `CREATE TABLE`, so its index already exists by the time `apply_masks` runs, and the adapter refuses to mask any column that an index has as a key: *is configured for masking but is also an index key column*. Declare that constraint at the model level **with a `name:`** instead — named constraints are applied by `ALTER TABLE` after the masks are in place, which the adapter allows.
|
|
348
|
+
|
|
349
|
+
A contract-enforced model is always loaded as `CREATE TABLE` followed by `INSERT … WITH (TABLOCK)`, on every build path. A `primary_key` or `unique` constraint — named or not — puts a nonclustered index on that table *before* the load, so the index is maintained row by row while the rows go in, and that part of the load is fully logged where an index-free heap would have been minimally logged. The cost lands on every rebuild of the model; it is most visible with `full_refresh_build: prebuilt`, whose point is a cheap bulk load. If a model's load time matters, weigh the key constraints against it; `check`, `not_null` and `foreign_key` do not create indexes and do not carry this cost.
|
|
350
|
+
|
|
249
351
|
### Dynamic Data Masking (`masked_with` / `masks`)
|
|
250
352
|
|
|
251
353
|
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+**.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "1.11.2rc1"
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_adapter.py
RENAMED
|
@@ -42,6 +42,13 @@ from dbt.adapters.sqlserver.sqlserver_runtime import _get_pyodbc
|
|
|
42
42
|
|
|
43
43
|
logger = AdapterLogger("SQLServer")
|
|
44
44
|
|
|
45
|
+
# The constraint types this adapter renders itself rather than deferring to
|
|
46
|
+
# dbt-adapters: each needs SQL Server's CLUSTERED/NONCLUSTERED choice or its
|
|
47
|
+
# own column quoting.
|
|
48
|
+
_KEYED_CONSTRAINTS = frozenset(
|
|
49
|
+
{ConstraintType.unique, ConstraintType.primary_key, ConstraintType.foreign_key}
|
|
50
|
+
)
|
|
51
|
+
|
|
45
52
|
# Mirrors sqlserver__select_starts_with_cte
|
|
46
53
|
# (dbt/include/sqlserver/macros/adapters/columns.sql): a query opening with a
|
|
47
54
|
# CTE cannot be neutered as ``select * from (...) where 1 = 0``, so it reaches
|
|
@@ -470,48 +477,219 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
470
477
|
finally:
|
|
471
478
|
conn.transaction_open = False
|
|
472
479
|
|
|
480
|
+
@classmethod
|
|
481
|
+
def _clustering(cls, keyword: str, expression: str) -> str:
|
|
482
|
+
"""Render a PRIMARY KEY / UNIQUE keyword with its index clustering.
|
|
483
|
+
|
|
484
|
+
SQL Server defaults an unqualified PRIMARY KEY to CLUSTERED, which is
|
|
485
|
+
incompatible with the clustered columnstore index the adapter builds
|
|
486
|
+
for ``as_columnstore`` (the default), so NONCLUSTERED is the safe
|
|
487
|
+
default here. dbt's own ``expression`` field is the override - a
|
|
488
|
+
constraint declaring ``expression: clustered`` keeps what it asked for
|
|
489
|
+
- so no adapter-specific yaml key is needed.
|
|
490
|
+
|
|
491
|
+
Those two keywords are the only thing T-SQL accepts in this position,
|
|
492
|
+
so anything else is rejected here rather than emitted as DDL that
|
|
493
|
+
cannot parse.
|
|
494
|
+
"""
|
|
495
|
+
clustering = expression.strip()
|
|
496
|
+
if not clustering:
|
|
497
|
+
return f"{keyword} {SQLServerIndexType.default()}"
|
|
498
|
+
if clustering.lower() not in (
|
|
499
|
+
SQLServerIndexType.clustered,
|
|
500
|
+
SQLServerIndexType.nonclustered,
|
|
501
|
+
):
|
|
502
|
+
raise dbt_common.exceptions.DbtValidationError(
|
|
503
|
+
f"Invalid expression '{expression}' on a {keyword} constraint. "
|
|
504
|
+
f"SQL Server accepts only '{SQLServerIndexType.clustered}' or "
|
|
505
|
+
f"'{SQLServerIndexType.nonclustered}' here; index options belong on a "
|
|
506
|
+
"separate index, not on the constraint."
|
|
507
|
+
)
|
|
508
|
+
return f"{keyword} {clustering}"
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def _render_foreign_key_target(cls, constraint: ColumnLevelConstraint) -> Optional[str]:
|
|
512
|
+
"""The ``references <table> (<columns>)`` tail of a foreign key.
|
|
513
|
+
|
|
514
|
+
Accepts both the ``to`` / ``to_columns`` form and the older free-text
|
|
515
|
+
``expression`` form; returns None when neither is usable.
|
|
516
|
+
|
|
517
|
+
dbt-core resolves ``to: ref(...)`` to a fully rendered relation, which
|
|
518
|
+
for this adapter includes the database. That three-part name is passed
|
|
519
|
+
through as-is: SQL Server resolves it fine while it names the current
|
|
520
|
+
database, and a genuinely cross-database target - which SQL Server does
|
|
521
|
+
not support for foreign keys - then fails with its own clear "references
|
|
522
|
+
invalid table" error rather than being silently rewritten to point at a
|
|
523
|
+
same-named table in this database.
|
|
524
|
+
"""
|
|
525
|
+
if constraint.to and constraint.to_columns:
|
|
526
|
+
columns = ", ".join(cls.quote(column) for column in constraint.to_columns)
|
|
527
|
+
return f"references {constraint.to} ({columns})"
|
|
528
|
+
if constraint.expression:
|
|
529
|
+
return f"references {constraint.expression}"
|
|
530
|
+
logger.warning(
|
|
531
|
+
f"Dropping the {constraint.type.value} constraint"
|
|
532
|
+
+ (f" '{constraint.name}'" if constraint.name else "")
|
|
533
|
+
+ ": it names no target. Declare `to:` together with `to_columns:`, "
|
|
534
|
+
"or the free-text `expression:` form (`<table> (<columns>)`)."
|
|
535
|
+
)
|
|
536
|
+
return None
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def _render_keyed_constraint(
|
|
540
|
+
cls, constraint: ColumnLevelConstraint, column_list: str = ""
|
|
541
|
+
) -> Optional[str]:
|
|
542
|
+
"""The constraint types this adapter renders differently from dbt-adapters.
|
|
543
|
+
|
|
544
|
+
Shared by the column-level and model-level renderers, which differ only
|
|
545
|
+
in whether they name their columns: a column-level constraint is written
|
|
546
|
+
against the column it follows, a model-level one carries its own list.
|
|
547
|
+
"""
|
|
548
|
+
expression = constraint.expression or ""
|
|
549
|
+
suffix = f" ({column_list})" if column_list else ""
|
|
550
|
+
|
|
551
|
+
if constraint.type == ConstraintType.unique:
|
|
552
|
+
return cls._clustering("unique", expression) + suffix
|
|
553
|
+
if constraint.type == ConstraintType.primary_key:
|
|
554
|
+
return cls._clustering("primary key", expression) + suffix
|
|
555
|
+
target = cls._render_foreign_key_target(constraint)
|
|
556
|
+
if target is None:
|
|
557
|
+
return None
|
|
558
|
+
return f"foreign key ({column_list}) {target}" if column_list else target
|
|
559
|
+
|
|
473
560
|
@available
|
|
474
561
|
@classmethod
|
|
475
562
|
def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> Optional[str]:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
563
|
+
"""Render a column-level constraint inline in the CREATE TABLE column list.
|
|
564
|
+
|
|
565
|
+
Column-level constraints are always anonymous: the table is built as
|
|
566
|
+
``<model>__dbt_tmp`` while the previous one still exists, and SQL
|
|
567
|
+
Server scopes constraint names per schema, so a name reused across
|
|
568
|
+
builds collides (Msg 2714). Declare the constraint at the model level
|
|
569
|
+
to name it - those are applied by ALTER after the swap, where the old
|
|
570
|
+
name is already gone.
|
|
571
|
+
|
|
572
|
+
Only UNIQUE, PRIMARY KEY and FOREIGN KEY need SQL Server treatment; NOT
|
|
573
|
+
NULL, CHECK and CUSTOM fall through to dbt-adapters so they stay in step
|
|
574
|
+
with upstream.
|
|
575
|
+
"""
|
|
576
|
+
if constraint.name:
|
|
577
|
+
logger.warning(
|
|
578
|
+
f"Ignoring the name '{constraint.name}' on the column-level "
|
|
579
|
+
f"{constraint.type.value} constraint: SQL Server scopes constraint names "
|
|
580
|
+
"per schema, so naming one inline collides with the table being replaced. "
|
|
581
|
+
"Declare the constraint under the model's `constraints:` key to name it."
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if constraint.type in _KEYED_CONSTRAINTS:
|
|
585
|
+
return cls._render_keyed_constraint(constraint)
|
|
586
|
+
return super().render_column_constraint(constraint)
|
|
481
587
|
|
|
482
|
-
|
|
483
|
-
|
|
588
|
+
@available
|
|
589
|
+
@classmethod
|
|
590
|
+
def render_raw_columns_constraints(
|
|
591
|
+
cls, raw_columns: Dict[str, Dict[str, Any]], only_not_null: bool = False
|
|
592
|
+
) -> List[str]:
|
|
593
|
+
"""Render the column DDL for a CREATE TABLE column list.
|
|
594
|
+
|
|
595
|
+
A fork of the dbt-adapters loop of the same name, for two reasons.
|
|
596
|
+
CHECK constraints are hoisted out of the column definition and returned
|
|
597
|
+
as standalone table-level clauses: SQL Server accepts only one
|
|
598
|
+
column-level CHECK per column ("More than one column CHECK constraint
|
|
599
|
+
specified for column ..."), while the table-level form has no such
|
|
600
|
+
limit. Both are anonymous and both live inside the same CREATE TABLE.
|
|
601
|
+
|
|
602
|
+
``only_not_null`` drops everything but NOT NULL, for the unit-test
|
|
603
|
+
fixture tables: their rows are hand-written stand-ins for the model's
|
|
604
|
+
data, so a UNIQUE, PRIMARY KEY or FOREIGN KEY copied off the contract
|
|
605
|
+
would fail the unit test on data that was never meant to satisfy it.
|
|
606
|
+
"""
|
|
607
|
+
rendered_columns = []
|
|
608
|
+
table_level_clauses = []
|
|
609
|
+
|
|
610
|
+
for column in raw_columns.values():
|
|
611
|
+
name = cls.quote(column["name"]) if column.get("quote") else column["name"]
|
|
612
|
+
parts = [f"{name} {column['data_type']}"]
|
|
613
|
+
for raw_constraint in column.get("constraints") or []:
|
|
614
|
+
constraint = cls._parse_column_constraint(raw_constraint)
|
|
615
|
+
if only_not_null and constraint.type != ConstraintType.not_null:
|
|
616
|
+
continue
|
|
617
|
+
clause = cls.process_parsed_constraint(constraint, cls.render_column_constraint)
|
|
618
|
+
if not clause:
|
|
619
|
+
continue
|
|
620
|
+
if constraint.type == ConstraintType.check:
|
|
621
|
+
table_level_clauses.append(clause)
|
|
622
|
+
else:
|
|
623
|
+
parts.append(clause)
|
|
624
|
+
rendered_columns.append(" ".join(parts))
|
|
484
625
|
|
|
485
|
-
return
|
|
626
|
+
return rendered_columns + table_level_clauses
|
|
486
627
|
|
|
487
628
|
@classmethod
|
|
488
629
|
def render_model_constraint(cls, constraint: ModelLevelConstraint) -> Optional[str]:
|
|
489
|
-
|
|
490
|
-
column_list = ", ".join(constraint.columns)
|
|
491
|
-
|
|
492
|
-
if constraint.name is None:
|
|
493
|
-
raise dbt_common.exceptions.DbtDatabaseError(
|
|
494
|
-
"Constraint name cannot be empty. Provide constraint name - column "
|
|
495
|
-
+ column_list
|
|
496
|
-
+ " and run the project again."
|
|
497
|
-
)
|
|
630
|
+
"""Render an unnamed model-level constraint inline in the column list.
|
|
498
631
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
return f"{constraint_prefix} {constraint.name} check ({constraint.expression})"
|
|
511
|
-
elif constraint.type == ConstraintType.custom and constraint.expression:
|
|
512
|
-
return f"{constraint_prefix} {constraint.name} {constraint.expression}"
|
|
513
|
-
else:
|
|
632
|
+
A named one renders nothing here and is applied afterwards by
|
|
633
|
+
``render_raw_model_alter_constraints`` instead - see
|
|
634
|
+
``sqlserver__build_model_constraints``. Its ``expression`` is still
|
|
635
|
+
validated now: the ALTER runs after the build has committed and swapped
|
|
636
|
+
the table in, and a typo there should fail before any of that happens.
|
|
637
|
+
"""
|
|
638
|
+
if constraint.name:
|
|
639
|
+
if constraint.type in (ConstraintType.unique, ConstraintType.primary_key):
|
|
640
|
+
cls._clustering(
|
|
641
|
+
constraint.type.value.replace("_", " "), constraint.expression or ""
|
|
642
|
+
)
|
|
514
643
|
return None
|
|
644
|
+
return cls._render_model_constraint_body(constraint)
|
|
645
|
+
|
|
646
|
+
@available
|
|
647
|
+
@classmethod
|
|
648
|
+
def render_raw_model_alter_constraints(
|
|
649
|
+
cls, raw_constraints: List[Dict[str, Any]]
|
|
650
|
+
) -> List[Dict[str, str]]:
|
|
651
|
+
"""The *named* model constraints, as ``{name, clause}`` pairs.
|
|
652
|
+
|
|
653
|
+
``clause`` is an ``add constraint <name> ...`` tail for ALTER TABLE;
|
|
654
|
+
``name`` is the bare name, which the macro needs as a string literal to
|
|
655
|
+
test ``sys.objects`` before adding it. These are applied once the build
|
|
656
|
+
has swapped the new table into place and dropped the old one, which is
|
|
657
|
+
the only point at which the name is free to reuse.
|
|
658
|
+
"""
|
|
659
|
+
clauses = []
|
|
660
|
+
for raw_constraint in raw_constraints:
|
|
661
|
+
constraint = cls._parse_model_constraint(raw_constraint)
|
|
662
|
+
if not constraint.name:
|
|
663
|
+
continue
|
|
664
|
+
body = cls.process_parsed_constraint(constraint, cls._render_model_constraint_body)
|
|
665
|
+
if body:
|
|
666
|
+
clauses.append(
|
|
667
|
+
{
|
|
668
|
+
"name": constraint.name,
|
|
669
|
+
"clause": f"add constraint {cls.quote(constraint.name)} {body}",
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
return clauses
|
|
673
|
+
|
|
674
|
+
@classmethod
|
|
675
|
+
def _render_model_constraint_body(cls, constraint: ModelLevelConstraint) -> Optional[str]:
|
|
676
|
+
"""The constraint itself, without any name - valid both in a CREATE
|
|
677
|
+
TABLE column list and after ``ALTER TABLE ... ADD CONSTRAINT <name>``.
|
|
678
|
+
|
|
679
|
+
The base renderer cannot stand in here: it prefixes ``constraint
|
|
680
|
+
<name>`` whenever the constraint is named, which the ALTER form already
|
|
681
|
+
carries.
|
|
682
|
+
"""
|
|
683
|
+
expression = constraint.expression or ""
|
|
684
|
+
|
|
685
|
+
if constraint.type in _KEYED_CONSTRAINTS:
|
|
686
|
+
column_list = ", ".join(cls.quote(column) for column in constraint.columns)
|
|
687
|
+
return cls._render_keyed_constraint(constraint, column_list)
|
|
688
|
+
if constraint.type == ConstraintType.check and expression:
|
|
689
|
+
return f"check ({expression})"
|
|
690
|
+
if constraint.type == ConstraintType.custom and expression:
|
|
691
|
+
return expression
|
|
692
|
+
return None
|
|
515
693
|
|
|
516
694
|
def _get_row_count(self, relation) -> int:
|
|
517
695
|
"""Return the number of rows in the given relation."""
|
|
@@ -189,6 +189,10 @@
|
|
|
189
189
|
{% do adapter.drop_relation(rel) %}
|
|
190
190
|
{% endfor %}
|
|
191
191
|
|
|
192
|
+
{#-- Named model-level constraints. After the to_drop loop, so the backup no
|
|
193
|
+
longer holds the old names. See sqlserver__build_model_constraints. --#}
|
|
194
|
+
{{ build_model_constraints(target_relation) }}
|
|
195
|
+
|
|
192
196
|
{{ run_hooks(post_hooks, inside_transaction=False) }}
|
|
193
197
|
|
|
194
198
|
{{ return({'relations': [target_relation]}) }}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{% macro build_columns_constraints(relation, only_not_null=False) %}
|
|
2
|
+
{{ return(adapter.dispatch('build_columns_constraints', 'dbt')(relation, only_not_null)) }}
|
|
3
|
+
{% endmacro %}
|
|
4
|
+
|
|
5
|
+
{% macro sqlserver__build_columns_constraints(relation, only_not_null=False) %}
|
|
6
|
+
{#- The parenthesised column list for CREATE TABLE. Carries every column-level
|
|
7
|
+
constraint plus the *unnamed* model-level ones: both are anonymous, so
|
|
8
|
+
SQL Server names them itself and nothing collides with the table this
|
|
9
|
+
build is about to replace. Named model-level constraints are applied
|
|
10
|
+
afterwards by build_model_constraints. -#}
|
|
11
|
+
{%- set raw_column_constraints = adapter.render_raw_columns_constraints(
|
|
12
|
+
raw_columns=model['columns'], only_not_null=only_not_null) -%}
|
|
13
|
+
{%- set raw_model_constraints = [] if only_not_null
|
|
14
|
+
else adapter.render_raw_model_constraints(
|
|
15
|
+
raw_constraints=model.get('constraints') or []) -%}
|
|
16
|
+
(
|
|
17
|
+
{% for c in raw_column_constraints + raw_model_constraints -%}
|
|
18
|
+
{{ c }}{{ "," if not loop.last }}
|
|
19
|
+
{% endfor %}
|
|
20
|
+
)
|
|
21
|
+
{% endmacro %}
|
|
22
|
+
|
|
23
|
+
{% macro build_model_constraints(relation) %}
|
|
24
|
+
{{ return(adapter.dispatch('build_model_constraints', 'dbt')(relation)) }}
|
|
25
|
+
{% endmacro %}
|
|
26
|
+
|
|
27
|
+
{% macro sqlserver__build_model_constraints(relation) %}
|
|
28
|
+
{#- Named model-level constraints, applied once the build has swapped the new
|
|
29
|
+
table into place and dropped the old one: SQL Server scopes constraint
|
|
30
|
+
names per schema, so the name is only free to reuse after the table that
|
|
31
|
+
held it is gone.
|
|
32
|
+
|
|
33
|
+
Each ADD is guarded on the name already being present on this table, so
|
|
34
|
+
the macro is safe to call on every build path, including the ones that
|
|
35
|
+
keep the existing table (a plain incremental run, a DML refresh). That
|
|
36
|
+
makes a constraint added to an existing model land on the next run rather
|
|
37
|
+
than waiting for a full refresh.
|
|
38
|
+
|
|
39
|
+
What the guard cannot see is a constraint whose *definition* changed under
|
|
40
|
+
an unchanged name: unlike an index name (a hash of its definition), a
|
|
41
|
+
constraint name says nothing about what the constraint does. Redefining
|
|
42
|
+
one needs --full-refresh, which rebuilds the table and so applies the new
|
|
43
|
+
definition to a table that carries none. This is documented in the README.
|
|
44
|
+
|
|
45
|
+
Emitted as a single batch: one round trip regardless of how many
|
|
46
|
+
constraints the model declares. -#}
|
|
47
|
+
{%- set contract_config = config.get('contract') -%}
|
|
48
|
+
{%- if not contract_config or not contract_config.enforced -%}
|
|
49
|
+
{{ return('') }}
|
|
50
|
+
{%- endif -%}
|
|
51
|
+
|
|
52
|
+
{%- set constraints = adapter.render_raw_model_alter_constraints(
|
|
53
|
+
raw_constraints=model.get('constraints') or []) -%}
|
|
54
|
+
{%- if not constraints -%}
|
|
55
|
+
{{ return('') }}
|
|
56
|
+
{%- endif -%}
|
|
57
|
+
|
|
58
|
+
{%- set object_id_literal = escape_single_quotes(relation.include(database=False)) -%}
|
|
59
|
+
{%- set alter_sql -%}
|
|
60
|
+
{{ get_use_database_sql(relation.database) }}
|
|
61
|
+
{%- for constraint in constraints %}
|
|
62
|
+
if not exists (
|
|
63
|
+
select 1
|
|
64
|
+
from sys.objects {{ information_schema_hints() }}
|
|
65
|
+
where name = '{{ escape_single_quotes(constraint['name']) }}'
|
|
66
|
+
and parent_object_id = OBJECT_ID('{{ object_id_literal }}')
|
|
67
|
+
)
|
|
68
|
+
begin
|
|
69
|
+
alter table {{ relation.include(database=False) }} {{ constraint['clause'] }};
|
|
70
|
+
end
|
|
71
|
+
{%- endfor %}
|
|
72
|
+
{%- endset %}
|
|
73
|
+
|
|
74
|
+
{#- auto_begin=False: this runs after the materialization's adapter.commit(),
|
|
75
|
+
so opening the ambient transaction here would leave one dangling. -#}
|
|
76
|
+
{% call statement('alter_table_add_constraints', auto_begin=False) -%}
|
|
77
|
+
{{ alter_sql }}
|
|
78
|
+
{%- endcall %}
|
|
79
|
+
{% endmacro %}
|
|
@@ -126,6 +126,10 @@
|
|
|
126
126
|
{{ drop_relation_if_exists(backup_relation) }}
|
|
127
127
|
{% endif %}
|
|
128
128
|
|
|
129
|
+
{#-- Named model-level constraints, on every build path; guarded, so a no-op
|
|
130
|
+
where they already exist. See sqlserver__build_model_constraints. --#}
|
|
131
|
+
{{ build_model_constraints(target_relation) }}
|
|
132
|
+
|
|
129
133
|
{{ run_hooks(post_hooks, inside_transaction=False) }}
|
|
130
134
|
|
|
131
135
|
{{ return({'relations': [target_relation]}) }}
|
|
@@ -110,6 +110,40 @@
|
|
|
110
110
|
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
|
|
111
111
|
{{ drop_relation_if_exists(backup_relation) }}
|
|
112
112
|
|
|
113
|
+
{#- The scratch table above came from SELECT * INTO, which is the right
|
|
114
|
+
shape for the schema probe and the wrong one for the object that is
|
|
115
|
+
about to be renamed into position: it copies no constraint and no
|
|
116
|
+
index, and takes nullability from the query rather than from a
|
|
117
|
+
contract. Left as-is it silently strips the model of its clustered
|
|
118
|
+
columnstore index - create_indexes only builds what the `indexes`
|
|
119
|
+
config names, never the as_columnstore CCI - and, under a contract, of
|
|
120
|
+
its NOT NULLs and inline constraints too. None of it came back on a
|
|
121
|
+
later run, because every later run matched the new schema and took the
|
|
122
|
+
DELETE+INSERT path above.
|
|
123
|
+
|
|
124
|
+
So rebuild it the way this adapter builds every other table. The
|
|
125
|
+
rebuild belongs on this branch alone: doing it up front would build,
|
|
126
|
+
and then throw away, a full columnstore index on every steady-state
|
|
127
|
+
refresh, which on a large table dominates the run. A schema change is
|
|
128
|
+
rare, so one extra build here is much the cheaper trade.
|
|
129
|
+
|
|
130
|
+
It is not free, though: the model's SQL runs a second time here, the
|
|
131
|
+
SELECT * INTO above having already run it once as the schema probe.
|
|
132
|
+
Any side effect in that SQL therefore happens twice, and the two runs
|
|
133
|
+
are not interchangeable - the schema decision came from the first, the
|
|
134
|
+
table renamed into position comes from the second. A model whose column
|
|
135
|
+
shape can differ between them lands the second shape unchecked, unless
|
|
136
|
+
a contract is enforced and create_table_as re-asserts it. Probing the
|
|
137
|
+
tmp view instead of the materialized scratch would collapse the two
|
|
138
|
+
back into one, at the cost of changing how the probe behaves - a
|
|
139
|
+
separate change, not this one.
|
|
140
|
+
|
|
141
|
+
create_table_as builds and drops its own __dbt_tmp_vw. -#}
|
|
142
|
+
{{ drop_relation_if_exists(refresh_relation) }}
|
|
143
|
+
{% call statement('dml_refresh_rebuild') -%}
|
|
144
|
+
{{ get_create_table_as_sql(False, refresh_relation, sql) }}
|
|
145
|
+
{%- endcall %}
|
|
146
|
+
|
|
113
147
|
{# Rename scratch table into position #}
|
|
114
148
|
{% set existing_relation = load_cached_relation(target_relation) %}
|
|
115
149
|
{% if existing_relation is not none %}
|
|
@@ -118,7 +152,7 @@
|
|
|
118
152
|
|
|
119
153
|
{{ adapter.rename_relation(refresh_relation, target_relation) }}
|
|
120
154
|
|
|
121
|
-
{#
|
|
155
|
+
{# Freshly rebuilt (no masks carried), so apply masks before
|
|
122
156
|
create_indexes — a mask cannot be added to a column an index depends
|
|
123
157
|
on (documented for all SQL Server versions). #}
|
|
124
158
|
{% do apply_masks(target_relation, mask_config) %}
|
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
{%- elif not is_nested_cte and contract_config.enforced %}
|
|
33
33
|
|
|
34
34
|
CREATE TABLE {{relation}}
|
|
35
|
-
{
|
|
35
|
+
{#- only_not_null: the fixture rows are hand-written stand-ins for the
|
|
36
|
+
model's real data, so a UNIQUE / PRIMARY KEY / FOREIGN KEY copied
|
|
37
|
+
off the contract would fail the unit test on data that was never
|
|
38
|
+
meant to satisfy it. -#}
|
|
39
|
+
{{ build_columns_constraints(relation, only_not_null=True) }}
|
|
36
40
|
{{ get_assert_columns_equivalent(sql) }}
|
|
37
41
|
|
|
38
42
|
{% set listColumns %}
|
|
@@ -69,7 +69,21 @@
|
|
|
69
69
|
{% endif %}
|
|
70
70
|
{% endif %}
|
|
71
71
|
{% if should_skip_view_update %}
|
|
72
|
-
{
|
|
72
|
+
{#- The view's SQL text is unchanged, so the CREATE/ALTER is skipped -
|
|
73
|
+
but a *referenced* table can still have changed shape (columns
|
|
74
|
+
added, dropped, or reordered) since this view was last built. SQL
|
|
75
|
+
Server resolves an unqualified `select *` at CREATE/ALTER time and
|
|
76
|
+
caches the result; skipping that statement here means the cached
|
|
77
|
+
column list silently goes stale relative to the underlying table,
|
|
78
|
+
even though this view's own definition never changed. sp_refreshview
|
|
79
|
+
re-derives that cached metadata from the table's current shape
|
|
80
|
+
without re-running the CREATE, so a skip stays a skip (no DDL, no
|
|
81
|
+
grant/deny churn) while the view keeps reporting the right columns. -#}
|
|
82
|
+
{#- sp_refreshview resolves its argument in the *current* database, so this needs
|
|
83
|
+
the same USE prefix every other name-resolving statement here carries -
|
|
84
|
+
without it a cross-database view model would refresh nothing and error. -#}
|
|
85
|
+
{% set object_name = "quotename('" ~ target_relation.schema ~ "') + '.' + quotename('" ~ target_relation.identifier ~ "')" %}
|
|
86
|
+
{% set build_sql = get_use_database_sql(target_relation.database) ~ " declare @dbt_sqlserver_refresh_target nvarchar(max) = " ~ object_name ~ "; exec sp_refreshview @dbt_sqlserver_refresh_target;" %}
|
|
73
87
|
{% else %}
|
|
74
88
|
{% set build_sql = get_create_view_as_sql(target_relation, sql) %}
|
|
75
89
|
{% endif %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbt-sqlserver
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.2rc1
|
|
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
|
|
@@ -283,6 +283,108 @@ You can also set it per model:
|
|
|
283
283
|
{{ config(materialized="table", as_columnstore=false) }}
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
With `table_refresh_method: dml`, a schema change makes the refresh fall back to a rename-swap. On that run — and only that run — the scratch table is rebuilt the way this adapter builds every other table, so it carries the model's columnstore index, and under an enforced contract its `NOT NULL`s and inline constraints, into the swap. That run therefore executes the model's SQL twice — once for the `SELECT … INTO` that probes for the schema change, once for the rebuild — and builds the columnstore index once. Steady-state refreshes are unaffected and keep the single `SELECT … INTO`. A table that lost its columnstore index to this bug before you upgraded is not repaired automatically: its schema still matches, so it stays on the cheap path. To rebuild it, temporarily set `full_refresh_build: prebuilt` and run with `--full-refresh`.
|
|
287
|
+
|
|
288
|
+
### Constraints
|
|
289
|
+
|
|
290
|
+
Constraints declared in a model's yaml are applied when — and only when — the model's [contract](https://docs.getdbt.com/reference/resource-configs/contract) is enforced, which is what every dbt adapter does and keeps their cost opt-in. (dbt-core does not raise if you declare constraints with the contract off — they are simply never emitted.) `not_null`, `check`, `unique`, `primary_key` and `foreign_key` are all supported.
|
|
291
|
+
|
|
292
|
+
**Where a constraint lands depends on whether you name it.**
|
|
293
|
+
|
|
294
|
+
An unnamed constraint is rendered inline in the `CREATE TABLE` column list and SQL Server names it (`PK__my_model__3213E83F…`). It is validated as the table is built, so a violation fails before the new table is swapped in and the previous one is left untouched.
|
|
295
|
+
|
|
296
|
+
A *model-level* constraint carrying `name:` is applied by `ALTER TABLE … ADD CONSTRAINT` after the build swaps the new table into place and drops the old one. SQL Server scopes constraint names per schema, and a table is built alongside the one it replaces, so that is the first moment the name is free to reuse — naming a constraint inline would collide with the outgoing table (`Msg 2714`) on every rebuild after the first. The trade-off is that this runs after the model has committed: if the data violates the constraint, the model fails with the table already in place but unconstrained, and — as with any failure this late in a build — `post_hook`s declared with `transaction: false` do not run.
|
|
297
|
+
|
|
298
|
+
Name a constraint when you want it stable across environments (schema-comparison tools report the generated names as differences) or need to reference it later. A `name:` on a *column-level* constraint is ignored with a warning — declare it under the model's `constraints:` key instead.
|
|
299
|
+
|
|
300
|
+
The full set of rules, with the test that verifies each, is in [docs/constraints.md](docs/constraints.md).
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
models:
|
|
304
|
+
- name: fact_sales
|
|
305
|
+
config:
|
|
306
|
+
contract:
|
|
307
|
+
enforced: true
|
|
308
|
+
constraints:
|
|
309
|
+
# named: applied by ALTER TABLE after the swap
|
|
310
|
+
- type: primary_key
|
|
311
|
+
name: PK_fact_sales
|
|
312
|
+
columns: [sale_id]
|
|
313
|
+
- type: foreign_key
|
|
314
|
+
name: FK_fact_sales_customer
|
|
315
|
+
columns: [customer_id]
|
|
316
|
+
to: ref('dim_customer')
|
|
317
|
+
to_columns: [customer_id]
|
|
318
|
+
# unnamed: rendered into the CREATE TABLE
|
|
319
|
+
- type: check
|
|
320
|
+
expression: amount >= 0
|
|
321
|
+
columns:
|
|
322
|
+
- name: sale_id
|
|
323
|
+
data_type: int
|
|
324
|
+
constraints:
|
|
325
|
+
- type: not_null
|
|
326
|
+
- name: customer_id
|
|
327
|
+
data_type: int
|
|
328
|
+
constraints:
|
|
329
|
+
- type: not_null
|
|
330
|
+
- name: amount
|
|
331
|
+
data_type: decimal(18,2)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Clustering
|
|
335
|
+
|
|
336
|
+
`primary_key` and `unique` are emitted as `NONCLUSTERED` by default so they can coexist with the clustered columnstore index built for [`as_columnstore`](#as_columnstore). Use dbt's own `expression` field to ask for something else:
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
constraints:
|
|
340
|
+
- type: primary_key
|
|
341
|
+
name: PK_fact_sales
|
|
342
|
+
columns: [sale_id]
|
|
343
|
+
expression: clustered # requires as_columnstore: false
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`clustered` and `nonclustered` are the only values understood here. `expression` is free text that dbt splices between the keyword and the column list, which is the one place T-SQL accepts nothing else — index options such as `with (fillfactor = 90)` belong on a separate index, not on the constraint.
|
|
347
|
+
|
|
348
|
+
#### Foreign keys
|
|
349
|
+
|
|
350
|
+
Two things are worth knowing before adding them:
|
|
351
|
+
|
|
352
|
+
- **They are not free at build time.** Every load is validated against them; add them where you want the guarantee, not everywhere the relationship exists.
|
|
353
|
+
- **A foreign key pointing at a model blocks that model's rebuild.** The build renames the outgoing table to a backup and drops it, but the child's foreign key follows the renamed object, so the drop fails with `Msg 3726`. SQL Server has no `DROP TABLE … CASCADE`.
|
|
354
|
+
|
|
355
|
+
The adapter ships a macro for exactly this, meant as a `pre_hook` on the **referenced** (parent) model:
|
|
356
|
+
|
|
357
|
+
```sql
|
|
358
|
+
{{ config(pre_hook="{{ drop_fk_constraints() }}") }}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
It drops the foreign keys in both directions — the inbound ones other tables hold against this model, and this model's own outbound ones — so the rebuild's backup drop succeeds. The trade-off is explicit and worth stating: **the child's foreign key does not exist between the parent's rebuild and the child's next build.** (dbt-postgres makes the same trade silently, by issuing every `drop table` with `cascade`.)
|
|
362
|
+
|
|
363
|
+
Add the hook *before* the first rebuild. Once a rebuild has failed with `Msg 3726`, the new table is already in place but without its own named constraints — those are applied after the backup drop that failed — and the child's key now points at `<model>__dbt_backup`, because a foreign key follows the object, not the name. A plain `dbt run` does not recover: the parent trips over the same backup and the child is skipped behind it. Rebuild the child alone — that run fails too, since the parent has no key to reference, but its swap drops the old child and the stale key with it — and a plain `dbt run` then rebuilds both in order. Dropping the child's key by hand (`ALTER TABLE <child> DROP CONSTRAINT <name>`) does the same.
|
|
364
|
+
|
|
365
|
+
`table_refresh_method: dml` is *not* a workaround. Its steady-state refresh issues `DELETE FROM <parent>`, which fails with `Msg 547` as soon as the child holds referencing rows, and its schema-change path falls back to the same rename-swap, hitting `Msg 3726` anyway.
|
|
366
|
+
|
|
367
|
+
- **SQL Server has no cross-database foreign keys.** `to: ref(...)` resolves to a fully qualified relation, database included, which SQL Server accepts as long as it names the current database. A target in another database fails with `references invalid table`.
|
|
368
|
+
|
|
369
|
+
A foreign key also does not order the build on its own: add an explicit `-- depends_on: {{ ref('dim_customer') }}` to the child model so the parent is built first.
|
|
370
|
+
|
|
371
|
+
#### Changing a constraint after the first build
|
|
372
|
+
|
|
373
|
+
Named constraints are applied by an `ALTER TABLE` whose `ADD` is guarded on the name already being present on the table, so:
|
|
374
|
+
|
|
375
|
+
- **Adding** a constraint to a model that already exists lands on its next run — no `--full-refresh` needed, on `table` and `incremental` alike.
|
|
376
|
+
- **Changing** an existing constraint's definition while keeping its name is *not* detected: a constraint name, unlike a `dbt_idx_` index name, says nothing about what the constraint does. Run `--full-refresh` to apply the new definition; that rebuilds the table, so the constraint is created fresh. (A `table` model rebuilds on every run and needs nothing special.)
|
|
377
|
+
- **Renaming** a constraint on a table that persists across runs adds the new name beside the old one. For a `check`, `unique` or `foreign_key` that means a duplicate; for a `primary_key` the run fails with `Msg 1779` (*table already has a primary key defined on it*) until the old one is dropped. Rename with `--full-refresh`.
|
|
378
|
+
- **Removing** a constraint from the yaml does not drop it from the database. Drop it yourself, or `--full-refresh`.
|
|
379
|
+
|
|
380
|
+
Every bullet above is about *named* constraints. Unnamed ones ride the `CREATE TABLE`, so they follow the table and change only when the table is rebuilt — on a materialization whose table persists across runs (`incremental` in its steady state, `table_refresh_method: dml`), adding an unnamed constraint to an existing model does nothing until a `--full-refresh`, and the run still reports success. Name it, or full-refresh.
|
|
381
|
+
|
|
382
|
+
#### Build-shape notes
|
|
383
|
+
|
|
384
|
+
An *unnamed* `primary_key` or `unique` constraint on a column that also carries a [data mask](#dynamic-data-masking-masked_with--masks) is rejected by the build. The constraint rides the `CREATE TABLE`, so its index already exists by the time `apply_masks` runs, and the adapter refuses to mask any column that an index has as a key: *is configured for masking but is also an index key column*. Declare that constraint at the model level **with a `name:`** instead — named constraints are applied by `ALTER TABLE` after the masks are in place, which the adapter allows.
|
|
385
|
+
|
|
386
|
+
A contract-enforced model is always loaded as `CREATE TABLE` followed by `INSERT … WITH (TABLOCK)`, on every build path. A `primary_key` or `unique` constraint — named or not — puts a nonclustered index on that table *before* the load, so the index is maintained row by row while the rows go in, and that part of the load is fully logged where an index-free heap would have been minimally logged. The cost lands on every rebuild of the model; it is most visible with `full_refresh_build: prebuilt`, whose point is a cheap bulk load. If a model's load time matters, weigh the key constraints against it; `check`, `not_null` and `foreign_key` do not create indexes and do not carry this cost.
|
|
387
|
+
|
|
286
388
|
### Dynamic Data Masking (`masked_with` / `masks`)
|
|
287
389
|
|
|
288
390
|
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+**.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = "1.11.1"
|
dbt_sqlserver-1.11.1/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{% macro build_columns_constraints(relation) %}
|
|
2
|
-
{{ return(adapter.dispatch('build_columns_constraints', 'dbt')(relation)) }}
|
|
3
|
-
{% endmacro %}
|
|
4
|
-
|
|
5
|
-
{% macro sqlserver__build_columns_constraints(relation) %}
|
|
6
|
-
{# loop through user_provided_columns to create DDL with data types and constraints #}
|
|
7
|
-
{%- set raw_column_constraints = adapter.render_raw_columns_constraints(raw_columns=model['columns']) -%}
|
|
8
|
-
(
|
|
9
|
-
{% for c in raw_column_constraints -%}
|
|
10
|
-
{{ c }}{{ "," if not loop.last }}
|
|
11
|
-
{% endfor %}
|
|
12
|
-
)
|
|
13
|
-
{% endmacro %}
|
|
14
|
-
|
|
15
|
-
{% macro build_model_constraints(relation) %}
|
|
16
|
-
{{ return(adapter.dispatch('build_model_constraints', 'dbt')(relation)) }}
|
|
17
|
-
{% endmacro %}
|
|
18
|
-
|
|
19
|
-
{% macro sqlserver__build_model_constraints(relation) %}
|
|
20
|
-
{# loop through user_provided_columns to create DDL with data types and constraints #}
|
|
21
|
-
{%- set raw_model_constraints = adapter.render_raw_model_constraints(raw_constraints=model['constraints']) -%}
|
|
22
|
-
{% for c in raw_model_constraints -%}
|
|
23
|
-
{% set alter_table_script %}
|
|
24
|
-
alter table {{ relation.include(database=False) }} {{c}};
|
|
25
|
-
{%endset%}
|
|
26
|
-
{% call statement('alter_table_add_constraint') -%}
|
|
27
|
-
{{alter_table_script}}
|
|
28
|
-
{%- endcall %}
|
|
29
|
-
{% endfor -%}
|
|
30
|
-
{% endmacro %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/__init__.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/index.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/relation_configs/policies.py
RENAMED
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_configs.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_connections.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_constants.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_credentials.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_relation.py
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/adapters/sqlserver/sqlserver_runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/catalog.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/columns.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/indexes.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/metadata.sql
RENAMED
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/relation.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/schema.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/adapters/show.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/any_value.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/concat.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/date_trunc.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/dateadd.sql
RENAMED
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/hash.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/last_day.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/length.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/listagg.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/position.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/safe_cast.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/split_part.sql
RENAMED
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt/include/sqlserver/macros/utils/timestamps.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_sqlserver-1.11.1 → dbt_sqlserver-1.11.2rc1}/dbt_sqlserver.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|