dbt-sqlserver 1.12.0rc2__tar.gz → 1.12.0rc3__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.12.0rc2 → dbt_sqlserver-1.12.0rc3}/PKG-INFO +42 -15
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/README.md +41 -14
- dbt_sqlserver-1.12.0rc3/dbt/adapters/sqlserver/__version__.py +1 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/relation_configs/index.py +8 -2
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_adapter.py +279 -23
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_configs.py +8 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_connections.py +98 -19
- dbt_sqlserver-1.12.0rc3/dbt/adapters/sqlserver/sqlserver_deny.py +141 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_relation.py +1 -1
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_runtime.py +5 -0
- dbt_sqlserver-1.12.0rc3/dbt/include/sqlserver/macros/adapters/apply_denies.sql +116 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/schema.sql +8 -7
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental.sql +5 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/table/table.sql +17 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/view/view.sql +31 -4
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot.sql +4 -0
- dbt_sqlserver-1.12.0rc3/dbt/include/sqlserver/macros/utils/openquery.sql +21 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt_sqlserver.egg-info/PKG-INFO +42 -15
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt_sqlserver.egg-info/SOURCES.txt +3 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/pyproject.toml +18 -2
- dbt_sqlserver-1.12.0rc2/dbt/adapters/sqlserver/__version__.py +0 -1
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/LICENSE +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/__init__.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/__init__.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/py.typed +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/relation_configs/__init__.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/relation_configs/policies.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_auth.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_backend.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_column.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_constants.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_credentials.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_helpers.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_mask.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/__init__.py +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/dbt_project.yml +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/apply_grants.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/apply_masks.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/catalog.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/columns.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/indexes.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/metadata.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/persist_docs.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/relation.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/show.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/adapters/validate_sql.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/functions/helpers.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/functions/scalar.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/hooks.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/incremental/incremental_strategies.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/incremental/merge.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/table/columns_spec_ddl.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/table/table_dml_refresh.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/unit_test/get_fixture_sql.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/unit_test/unit_test_create_table_as.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/models/view/create_view_as.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/snapshots/helpers.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/snapshots/snapshot_merge.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/snapshots/strategies.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/tests.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/materializations/unit_tests.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/relations/seeds/helpers.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/relations/table/clone.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/relations/table/create.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/relations/views/create.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/any_value.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/array_construct.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/cast_bool_to_text.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/concat.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/date_trunc.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/dateadd.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/get_tables_by_pattern.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/hash.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/last_day.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/length.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/listagg.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/position.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/safe_cast.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/split_part.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/macros/utils/timestamps.sql +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/include/sqlserver/profile_template.yml +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt_sqlserver.egg-info/dependency_links.txt +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt_sqlserver.egg-info/requires.txt +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt_sqlserver.egg-info/top_level.txt +0 -0
- {dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbt-sqlserver
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.0rc3
|
|
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
|
|
@@ -40,8 +40,8 @@ Dynamic: license-file
|
|
|
40
40
|
|
|
41
41
|
[dbt](https://www.getdbt.com) adapter for Microsoft SQL Server and Azure SQL services.
|
|
42
42
|
|
|
43
|
-
The adapter supports dbt-core 1.
|
|
44
|
-
E.g. version 1.
|
|
43
|
+
The adapter supports dbt-core 1.12 or newer and follows the same versioning scheme.
|
|
44
|
+
E.g. version 1.12.x of the adapter is compatible with dbt-core 1.12.x.
|
|
45
45
|
|
|
46
46
|
## Supported Python versions
|
|
47
47
|
|
|
@@ -53,6 +53,7 @@ The adapter is tested against:
|
|
|
53
53
|
| 3.11 | Officially supported |
|
|
54
54
|
| 3.12 | Officially supported |
|
|
55
55
|
| 3.13 | Officially supported |
|
|
56
|
+
| 3.14 | Officially supported |
|
|
56
57
|
|
|
57
58
|
## Supported SQL Server versions
|
|
58
59
|
|
|
@@ -175,23 +176,23 @@ See [the changelog](CHANGELOG.md)
|
|
|
175
176
|
|
|
176
177
|
### `dbt_sqlserver_use_default_schema_concat`
|
|
177
178
|
|
|
178
|
-
*(default: `
|
|
179
|
+
*(default: `true`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model.
|
|
179
180
|
|
|
180
181
|
| Flag value | `custom_schema_name` | Result |
|
|
181
182
|
|---|---|---|
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
183
|
+
| `true` (default, dbt-core standard) | *(none)* | `target.schema` |
|
|
184
|
+
| `true` (default, dbt-core standard) | `"reporting"` | `target.schema_reporting` |
|
|
185
|
+
| `false` (legacy, deprecated) | *(none)* | `target.schema` |
|
|
186
|
+
| `false` (legacy, deprecated) | `"reporting"` | `reporting` |
|
|
186
187
|
|
|
187
|
-
When `
|
|
188
|
-
When `
|
|
188
|
+
When `true` (the default), the adapter delegates to dbt-core's `default__generate_schema_name`, which concatenates `target.schema` + `_` + `custom_schema_name`. This also matches the behavior dbt-core v2 (Fusion) ships unconditionally.
|
|
189
|
+
When `false`, the adapter uses its legacy behaviour: `custom_schema_name` is used **as-is** without being prefixed by `target.schema`. This legacy behavior is deprecated and will be removed in a future release.
|
|
189
190
|
|
|
190
191
|
**Example usage in `dbt_project.yml`:**
|
|
191
192
|
|
|
192
193
|
```yaml
|
|
193
194
|
flags:
|
|
194
|
-
dbt_sqlserver_use_default_schema_concat:
|
|
195
|
+
dbt_sqlserver_use_default_schema_concat: false # Opt back into the deprecated legacy behavior
|
|
195
196
|
```
|
|
196
197
|
|
|
197
198
|
The same setting is also honoured via `vars:` for backwards compatibility; the behavior flag under `flags:` takes precedence when both are set.
|
|
@@ -216,19 +217,28 @@ Safe expansions are further gated by `column_type_expansion_max_rows` (default 1
|
|
|
216
217
|
|
|
217
218
|
### `dbt_sqlserver_use_dbt_transactions`
|
|
218
219
|
|
|
219
|
-
_(default: `
|
|
220
|
+
_(default: `true`)_ Makes dbt's transaction hooks real at the SQL Server level by emitting `BEGIN TRANSACTION` / `COMMIT TRANSACTION` through the adapter's `add_begin_query` and `add_commit_query` methods.
|
|
220
221
|
|
|
221
|
-
The default is `
|
|
222
|
+
The default is `true`, so dbt-managed transaction hooks emit real T-SQL transaction statements and rollback uses `IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION`. Set it to `false` to opt back into the deprecated legacy behavior where `begin`/`commit` hooks are logical no-ops and the driver auto-commits each statement.
|
|
222
223
|
|
|
223
224
|
The driver connection remains in autocommit mode (`autocommit=true`) in both modes.
|
|
224
225
|
|
|
225
|
-
This
|
|
226
|
+
This is now the default and should be tested carefully with project-specific materializations and hooks. Projects that depend on autocommit-only behavior should set the flag to `false` during migration.
|
|
226
227
|
|
|
227
228
|
```yaml
|
|
228
229
|
# dbt_project.yml
|
|
229
230
|
flags:
|
|
230
231
|
dbt_sqlserver_enable_safe_type_expansion: true
|
|
231
|
-
dbt_sqlserver_use_dbt_transactions: true #
|
|
232
|
+
dbt_sqlserver_use_dbt_transactions: true # default
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `dbt_sqlserver_use_native_string_types`
|
|
236
|
+
|
|
237
|
+
*(default: `true`)* Controls the SQL Server-native mappings used for dbt string types. With the default enabled, `STRING` maps to `VARCHAR(MAX)`, `NCHAR` maps to `NCHAR(1)`, and `NVARCHAR` maps to `NVARCHAR(4000)`. Set it to `false` to opt back into the deprecated legacy mappings: `STRING` and `NVARCHAR` map to `VARCHAR(8000)`, while `NCHAR` maps to `CHAR(1)`.
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
flags:
|
|
241
|
+
dbt_sqlserver_use_native_string_types: false # deprecated legacy behavior
|
|
232
242
|
```
|
|
233
243
|
|
|
234
244
|
### `xact_abort`
|
|
@@ -342,6 +352,23 @@ Behaviour:
|
|
|
342
352
|
|
|
343
353
|
**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.
|
|
344
354
|
|
|
355
|
+
### Object-level DENY (`denies`)
|
|
356
|
+
|
|
357
|
+
In SQL Server, an object-level `DENY` is the only way to carve an exception out of a schema-level `GRANT` — grant a principal `SELECT` on a whole schema, then `DENY SELECT` on the individual PII-bearing models. But a `DENY` is stored against the object's `object_id`, so dbt destroys it every time it drops and recreates the relation (which is *every* run for a view). The schema grant survives; its exceptions silently evaporate, leaving a **fail-open** posture that no run reports.
|
|
358
|
+
|
|
359
|
+
The `denies` config re-applies object-level DENYs after each build, diffed against `sys.database_permissions`, the same way `masks` re-applies Dynamic Data Masking. Its shape mirrors `grants` — a `{privilege: [principals]}` map — and it is settable in the in-file `{{ config() }}`, the model's `.yml` `config:` block, or a directory-wide default in `dbt_project.yml` (merging key-wise across those levels, like `masks`):
|
|
360
|
+
|
|
361
|
+
```sql
|
|
362
|
+
{{ config(denies={'select': ['Restricted_Read_Only']}) }}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- **Survives rebuilds:** the DENY is present after `dbt run` and *still present after the next run*, for `table`, `view`, `incremental` (append and full-refresh) and `snapshot`. Unlike masks, this **includes views** — a view is a valid securable and is recreated on every run, which is where a DENY is lost most often.
|
|
366
|
+
- **Reconciled:** the adapter diffs the config against the live DENY state and emits only what changed — `DENY` for configured-not-present, `REVOKE` for present-not-configured (a `REVOKE` removes a `DENY`). Removing a principal from `denies` revokes its DENY on the next run; a converged, persisted relation issues no DDL.
|
|
367
|
+
- **Scope:** object-level DENY for the table privileges `select`, `insert`, `update`, `delete`, `references`. An unsupported privilege is warned and skipped (the warning surfaces the likely typo without taking down the build). Column-, database- and server-scoped permissions are out of scope (the latter survive a rebuild already).
|
|
368
|
+
- **Absent principal:** a `DENY` to a non-existent database principal is warned-and-skipped (the run still succeeds), so one config runs unchanged across dev, CI and prod where the principal set differs.
|
|
369
|
+
- **Ordering vs `grants`:** grants are applied first, then denies, so the final state is unambiguous. A `(privilege, principal)` pair appearing in both `grants` and `denies` is warned as a likely mistake (a DENY overrides a GRANT in SQL Server).
|
|
370
|
+
- **Other adapters:** `denies` is a no-op on non-SQL-Server adapters, not an error. This is SQL-Server-specific by design — `DENY` is absent from the SQL standard and does not port.
|
|
371
|
+
|
|
345
372
|
## Contributing
|
|
346
373
|
|
|
347
374
|
[](https://github.com/dbt-msft/dbt-sqlserver/actions/workflows/unit-tests.yml)
|
|
@@ -2,8 +2,8 @@
|
|
|
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.
|
|
6
|
-
E.g. version 1.
|
|
5
|
+
The adapter supports dbt-core 1.12 or newer and follows the same versioning scheme.
|
|
6
|
+
E.g. version 1.12.x of the adapter is compatible with dbt-core 1.12.x.
|
|
7
7
|
|
|
8
8
|
## Supported Python versions
|
|
9
9
|
|
|
@@ -15,6 +15,7 @@ The adapter is tested against:
|
|
|
15
15
|
| 3.11 | Officially supported |
|
|
16
16
|
| 3.12 | Officially supported |
|
|
17
17
|
| 3.13 | Officially supported |
|
|
18
|
+
| 3.14 | Officially supported |
|
|
18
19
|
|
|
19
20
|
## Supported SQL Server versions
|
|
20
21
|
|
|
@@ -137,23 +138,23 @@ See [the changelog](CHANGELOG.md)
|
|
|
137
138
|
|
|
138
139
|
### `dbt_sqlserver_use_default_schema_concat`
|
|
139
140
|
|
|
140
|
-
*(default: `
|
|
141
|
+
*(default: `true`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model.
|
|
141
142
|
|
|
142
143
|
| Flag value | `custom_schema_name` | Result |
|
|
143
144
|
|---|---|---|
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
145
|
+
| `true` (default, dbt-core standard) | *(none)* | `target.schema` |
|
|
146
|
+
| `true` (default, dbt-core standard) | `"reporting"` | `target.schema_reporting` |
|
|
147
|
+
| `false` (legacy, deprecated) | *(none)* | `target.schema` |
|
|
148
|
+
| `false` (legacy, deprecated) | `"reporting"` | `reporting` |
|
|
148
149
|
|
|
149
|
-
When `
|
|
150
|
-
When `
|
|
150
|
+
When `true` (the default), the adapter delegates to dbt-core's `default__generate_schema_name`, which concatenates `target.schema` + `_` + `custom_schema_name`. This also matches the behavior dbt-core v2 (Fusion) ships unconditionally.
|
|
151
|
+
When `false`, the adapter uses its legacy behaviour: `custom_schema_name` is used **as-is** without being prefixed by `target.schema`. This legacy behavior is deprecated and will be removed in a future release.
|
|
151
152
|
|
|
152
153
|
**Example usage in `dbt_project.yml`:**
|
|
153
154
|
|
|
154
155
|
```yaml
|
|
155
156
|
flags:
|
|
156
|
-
dbt_sqlserver_use_default_schema_concat:
|
|
157
|
+
dbt_sqlserver_use_default_schema_concat: false # Opt back into the deprecated legacy behavior
|
|
157
158
|
```
|
|
158
159
|
|
|
159
160
|
The same setting is also honoured via `vars:` for backwards compatibility; the behavior flag under `flags:` takes precedence when both are set.
|
|
@@ -178,19 +179,28 @@ Safe expansions are further gated by `column_type_expansion_max_rows` (default 1
|
|
|
178
179
|
|
|
179
180
|
### `dbt_sqlserver_use_dbt_transactions`
|
|
180
181
|
|
|
181
|
-
_(default: `
|
|
182
|
+
_(default: `true`)_ Makes dbt's transaction hooks real at the SQL Server level by emitting `BEGIN TRANSACTION` / `COMMIT TRANSACTION` through the adapter's `add_begin_query` and `add_commit_query` methods.
|
|
182
183
|
|
|
183
|
-
The default is `
|
|
184
|
+
The default is `true`, so dbt-managed transaction hooks emit real T-SQL transaction statements and rollback uses `IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION`. Set it to `false` to opt back into the deprecated legacy behavior where `begin`/`commit` hooks are logical no-ops and the driver auto-commits each statement.
|
|
184
185
|
|
|
185
186
|
The driver connection remains in autocommit mode (`autocommit=true`) in both modes.
|
|
186
187
|
|
|
187
|
-
This
|
|
188
|
+
This is now the default and should be tested carefully with project-specific materializations and hooks. Projects that depend on autocommit-only behavior should set the flag to `false` during migration.
|
|
188
189
|
|
|
189
190
|
```yaml
|
|
190
191
|
# dbt_project.yml
|
|
191
192
|
flags:
|
|
192
193
|
dbt_sqlserver_enable_safe_type_expansion: true
|
|
193
|
-
dbt_sqlserver_use_dbt_transactions: true #
|
|
194
|
+
dbt_sqlserver_use_dbt_transactions: true # default
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `dbt_sqlserver_use_native_string_types`
|
|
198
|
+
|
|
199
|
+
*(default: `true`)* Controls the SQL Server-native mappings used for dbt string types. With the default enabled, `STRING` maps to `VARCHAR(MAX)`, `NCHAR` maps to `NCHAR(1)`, and `NVARCHAR` maps to `NVARCHAR(4000)`. Set it to `false` to opt back into the deprecated legacy mappings: `STRING` and `NVARCHAR` map to `VARCHAR(8000)`, while `NCHAR` maps to `CHAR(1)`.
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
flags:
|
|
203
|
+
dbt_sqlserver_use_native_string_types: false # deprecated legacy behavior
|
|
194
204
|
```
|
|
195
205
|
|
|
196
206
|
### `xact_abort`
|
|
@@ -304,6 +314,23 @@ Behaviour:
|
|
|
304
314
|
|
|
305
315
|
**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.
|
|
306
316
|
|
|
317
|
+
### Object-level DENY (`denies`)
|
|
318
|
+
|
|
319
|
+
In SQL Server, an object-level `DENY` is the only way to carve an exception out of a schema-level `GRANT` — grant a principal `SELECT` on a whole schema, then `DENY SELECT` on the individual PII-bearing models. But a `DENY` is stored against the object's `object_id`, so dbt destroys it every time it drops and recreates the relation (which is *every* run for a view). The schema grant survives; its exceptions silently evaporate, leaving a **fail-open** posture that no run reports.
|
|
320
|
+
|
|
321
|
+
The `denies` config re-applies object-level DENYs after each build, diffed against `sys.database_permissions`, the same way `masks` re-applies Dynamic Data Masking. Its shape mirrors `grants` — a `{privilege: [principals]}` map — and it is settable in the in-file `{{ config() }}`, the model's `.yml` `config:` block, or a directory-wide default in `dbt_project.yml` (merging key-wise across those levels, like `masks`):
|
|
322
|
+
|
|
323
|
+
```sql
|
|
324
|
+
{{ config(denies={'select': ['Restricted_Read_Only']}) }}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- **Survives rebuilds:** the DENY is present after `dbt run` and *still present after the next run*, for `table`, `view`, `incremental` (append and full-refresh) and `snapshot`. Unlike masks, this **includes views** — a view is a valid securable and is recreated on every run, which is where a DENY is lost most often.
|
|
328
|
+
- **Reconciled:** the adapter diffs the config against the live DENY state and emits only what changed — `DENY` for configured-not-present, `REVOKE` for present-not-configured (a `REVOKE` removes a `DENY`). Removing a principal from `denies` revokes its DENY on the next run; a converged, persisted relation issues no DDL.
|
|
329
|
+
- **Scope:** object-level DENY for the table privileges `select`, `insert`, `update`, `delete`, `references`. An unsupported privilege is warned and skipped (the warning surfaces the likely typo without taking down the build). Column-, database- and server-scoped permissions are out of scope (the latter survive a rebuild already).
|
|
330
|
+
- **Absent principal:** a `DENY` to a non-existent database principal is warned-and-skipped (the run still succeeds), so one config runs unchanged across dev, CI and prod where the principal set differs.
|
|
331
|
+
- **Ordering vs `grants`:** grants are applied first, then denies, so the final state is unambiguous. A `(privilege, principal)` pair appearing in both `grants` and `denies` is warned as a likely mistake (a DENY overrides a GRANT in SQL Server).
|
|
332
|
+
- **Other adapters:** `denies` is a no-op on non-SQL-Server adapters, not an error. This is SQL-Server-specific by design — `DENY` is absent from the SQL standard and does not port.
|
|
333
|
+
|
|
307
334
|
## Contributing
|
|
308
335
|
|
|
309
336
|
[](https://github.com/dbt-msft/dbt-sqlserver/actions/workflows/unit-tests.yml)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "1.12.0rc3"
|
{dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/relation_configs/index.py
RENAMED
|
@@ -593,13 +593,19 @@ def index_config_changes(
|
|
|
593
593
|
SQLServerIndexConfig.parse_relation_results(rows_by_name[name])
|
|
594
594
|
)
|
|
595
595
|
changes.append(
|
|
596
|
-
SQLServerIndexConfigChange(
|
|
596
|
+
SQLServerIndexConfigChange(
|
|
597
|
+
# `action` is inherited from RelationConfigChange, a dataclass a
|
|
598
|
+
# type checker cannot see; see [tool.ty.rules] in pyproject.toml.
|
|
599
|
+
action=RelationConfigChangeAction.drop, # ty: ignore[unknown-argument]
|
|
600
|
+
context=context,
|
|
601
|
+
)
|
|
597
602
|
)
|
|
598
603
|
for name, config in expected_by_name.items():
|
|
599
604
|
if name not in existing_names:
|
|
600
605
|
changes.append(
|
|
601
606
|
SQLServerIndexConfigChange(
|
|
602
|
-
action=RelationConfigChangeAction.create,
|
|
607
|
+
action=RelationConfigChangeAction.create, # ty: ignore[unknown-argument]
|
|
608
|
+
context=config,
|
|
603
609
|
)
|
|
604
610
|
)
|
|
605
611
|
return changes, warnings
|
{dbt_sqlserver-1.12.0rc2 → dbt_sqlserver-1.12.0rc3}/dbt/adapters/sqlserver/sqlserver_adapter.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime as _dt
|
|
2
|
-
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
3
4
|
|
|
4
5
|
import agate
|
|
5
6
|
import dbt_common.exceptions
|
|
@@ -27,16 +28,120 @@ from dbt.adapters.sqlserver.relation_configs.index import (
|
|
|
27
28
|
index_config_changes,
|
|
28
29
|
normalize_drop_unmanaged,
|
|
29
30
|
)
|
|
31
|
+
from dbt.adapters.sqlserver.sqlserver_auth import is_adbc_backend, is_mssql_python_backend
|
|
30
32
|
from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn, SQLServerColumnNative
|
|
31
33
|
from dbt.adapters.sqlserver.sqlserver_configs import SQLServerConfigs
|
|
32
|
-
from dbt.adapters.sqlserver.sqlserver_connections import
|
|
34
|
+
from dbt.adapters.sqlserver.sqlserver_connections import (
|
|
35
|
+
SQLServerConnectionManager,
|
|
36
|
+
_discard_pending_results,
|
|
37
|
+
)
|
|
38
|
+
from dbt.adapters.sqlserver.sqlserver_deny import deny_changes as _deny_changes
|
|
39
|
+
from dbt.adapters.sqlserver.sqlserver_deny import resolve_denies as _resolve_denies
|
|
33
40
|
from dbt.adapters.sqlserver.sqlserver_mask import ColumnMask
|
|
34
41
|
from dbt.adapters.sqlserver.sqlserver_mask import mask_changes as _mask_changes
|
|
35
42
|
from dbt.adapters.sqlserver.sqlserver_mask import resolve_masks as _resolve_masks
|
|
36
43
|
from dbt.adapters.sqlserver.sqlserver_relation import SQLServerRelation
|
|
44
|
+
from dbt.adapters.sqlserver.sqlserver_runtime import _get_pyodbc
|
|
37
45
|
|
|
38
46
|
logger = AdapterLogger("SQLServer")
|
|
39
47
|
|
|
48
|
+
# Mirrors sqlserver__select_starts_with_cte
|
|
49
|
+
# (dbt/include/sqlserver/macros/adapters/columns.sql): a query opening with a
|
|
50
|
+
# CTE cannot be neutered as ``select * from (...) where 1 = 0``, so it reaches
|
|
51
|
+
# get_column_schema_from_query unwrapped and would otherwise be executed in
|
|
52
|
+
# full just to read its column names.
|
|
53
|
+
_SQL_COMMENT = re.compile(r"(?s)/\*.*?\*/|--[^\n]*\n")
|
|
54
|
+
|
|
55
|
+
# sp_describe_first_result_set reports true SQL Server types; reading
|
|
56
|
+
# ``cursor.description`` reports Python classes, which collapse whole families
|
|
57
|
+
# (every integer width arrives as ``int``, every string type as ``varchar``).
|
|
58
|
+
# Contract comparison comes through this method either way, so the describe
|
|
59
|
+
# path is mapped back onto exactly the names the execute path yields via
|
|
60
|
+
# ``data_type_code_to_name``. TestCteProbeAvoidsExecution pins the two
|
|
61
|
+
# together; a type missing from this map falls back to executing rather than
|
|
62
|
+
# guessing.
|
|
63
|
+
#
|
|
64
|
+
# The names below are pyodbc's. The class a driver picks for a column is its
|
|
65
|
+
# own choice, not SQL Server's, and mssql-python decodes three of these
|
|
66
|
+
# differently -- see ``_MSSQL_PYTHON_TYPE_OVERRIDES``.
|
|
67
|
+
_SYSTEM_TYPE_TO_EXECUTED_NAME = {
|
|
68
|
+
"bigint": "int",
|
|
69
|
+
"int": "int",
|
|
70
|
+
"smallint": "int",
|
|
71
|
+
"tinyint": "int",
|
|
72
|
+
"bit": "bit",
|
|
73
|
+
"decimal": "decimal",
|
|
74
|
+
"numeric": "decimal",
|
|
75
|
+
"money": "decimal",
|
|
76
|
+
"smallmoney": "decimal",
|
|
77
|
+
"float": "float",
|
|
78
|
+
"real": "float",
|
|
79
|
+
"date": "date",
|
|
80
|
+
"time": "time",
|
|
81
|
+
"datetime": "datetime2(6)",
|
|
82
|
+
"smalldatetime": "datetime2(6)",
|
|
83
|
+
"datetime2": "datetime2(6)",
|
|
84
|
+
"char": "varchar",
|
|
85
|
+
"nchar": "varchar",
|
|
86
|
+
"varchar": "varchar",
|
|
87
|
+
"nvarchar": "varchar",
|
|
88
|
+
"text": "varchar",
|
|
89
|
+
"ntext": "varchar",
|
|
90
|
+
"xml": "varchar",
|
|
91
|
+
"uniqueidentifier": "varchar",
|
|
92
|
+
"binary": "varbinary",
|
|
93
|
+
"varbinary": "varbinary",
|
|
94
|
+
"image": "varbinary",
|
|
95
|
+
"timestamp": "varbinary",
|
|
96
|
+
"rowversion": "varbinary",
|
|
97
|
+
# datetimeoffset is deliberately absent *from this map*: add_query
|
|
98
|
+
# registers the -155 output converter after the execute that needed it,
|
|
99
|
+
# so pyodbc reports the column as bytearray on a connection's first query
|
|
100
|
+
# and as str on every one after. No fixed name mirrors that, so on pyodbc
|
|
101
|
+
# describing gives up and the query is executed -- which agrees with
|
|
102
|
+
# itself by construction. mssql-python needs no converter and is pinned
|
|
103
|
+
# in _MSSQL_PYTHON_TYPE_OVERRIDES.
|
|
104
|
+
"sql_variant": "varbinary",
|
|
105
|
+
"hierarchyid": "varbinary",
|
|
106
|
+
"geography": "varbinary",
|
|
107
|
+
"geometry": "varbinary",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# mssql-python decodes three types into richer Python objects than pyodbc
|
|
111
|
+
# does: uniqueidentifier as ``uuid.UUID`` (pyodbc: ``str``), datetimeoffset
|
|
112
|
+
# as ``datetime`` and sql_variant as ``str`` (pyodbc: ``bytearray`` for
|
|
113
|
+
# both). Executing reports those classes, so describing has to agree.
|
|
114
|
+
_MSSQL_PYTHON_TYPE_OVERRIDES = {
|
|
115
|
+
"uniqueidentifier": "uniqueidentifier",
|
|
116
|
+
"datetimeoffset": "datetime2(6)",
|
|
117
|
+
"sql_variant": "varchar",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _executed_name_for_system_type(base_type: str, backend: Any) -> Optional[str]:
|
|
122
|
+
"""The name the executed probe would report for a described column type.
|
|
123
|
+
|
|
124
|
+
None means "no confident answer" -- the caller then executes the query,
|
|
125
|
+
which is slower but cannot disagree with itself.
|
|
126
|
+
"""
|
|
127
|
+
if is_mssql_python_backend(backend):
|
|
128
|
+
overridden = _MSSQL_PYTHON_TYPE_OVERRIDES.get(base_type)
|
|
129
|
+
if overridden is not None:
|
|
130
|
+
return overridden
|
|
131
|
+
|
|
132
|
+
elif base_type == "uniqueidentifier":
|
|
133
|
+
# pyodbc yields uuid.UUID or str for a GUID depending on its
|
|
134
|
+
# module-level ``native_uuid`` flag -- process-global state anything
|
|
135
|
+
# in the process can flip, so read it rather than assume a default.
|
|
136
|
+
try:
|
|
137
|
+
native_uuid = bool(_get_pyodbc().native_uuid)
|
|
138
|
+
except Exception as e: # pragma: no cover - pyodbc is present if in use
|
|
139
|
+
logger.debug(f"Could not read pyodbc.native_uuid, executing the query: {e}")
|
|
140
|
+
return None
|
|
141
|
+
return "uniqueidentifier" if native_uuid else "varchar"
|
|
142
|
+
|
|
143
|
+
return _SYSTEM_TYPE_TO_EXECUTED_NAME.get(base_type)
|
|
144
|
+
|
|
40
145
|
|
|
41
146
|
def _normalize_result_datetimes(
|
|
42
147
|
result: Union[Tuple, List[Tuple], None],
|
|
@@ -82,7 +187,8 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
82
187
|
"""
|
|
83
188
|
|
|
84
189
|
ConnectionManager = SQLServerConnectionManager
|
|
85
|
-
|
|
190
|
+
# Annotated because __init__ swaps in the SQLServerColumnNative subclass.
|
|
191
|
+
Column: Type[SQLServerColumn] = SQLServerColumn
|
|
86
192
|
AdapterSpecificConfigs = SQLServerConfigs
|
|
87
193
|
Relation = SQLServerRelation
|
|
88
194
|
|
|
@@ -115,20 +221,25 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
115
221
|
|
|
116
222
|
@property
|
|
117
223
|
def _behavior_flags(self) -> List[BehaviorFlag]:
|
|
118
|
-
|
|
119
|
-
|
|
224
|
+
# dbt-common declares BehaviorFlag's optional keys with a NotRequired
|
|
225
|
+
# that falls back to Optional under a try/except ImportError shim, so a
|
|
226
|
+
# type checker reads `source` and `docs_url` as required and rejects
|
|
227
|
+
# every flag below. The suppressions go stale once that shim is dropped.
|
|
228
|
+
return [ # ty: ignore[invalid-return-type]
|
|
229
|
+
{ # ty: ignore[missing-typed-dict-key]
|
|
120
230
|
"name": "dbt_sqlserver_use_default_schema_concat",
|
|
121
|
-
"default":
|
|
231
|
+
"default": True,
|
|
122
232
|
"description": (
|
|
123
|
-
"When True, uses dbt-core's standard schema concatenation "
|
|
233
|
+
"When True (default), uses dbt-core's standard schema concatenation "
|
|
124
234
|
"(`target.schema` + `_` + `custom_schema_name`). "
|
|
125
|
-
"When False
|
|
235
|
+
"When False, uses the legacy adapter behaviour: "
|
|
126
236
|
"`custom_schema_name` is used directly without prefixing `target.schema`. "
|
|
127
|
-
"
|
|
128
|
-
"
|
|
237
|
+
"The legacy behaviour is deprecated and this override will be removed in a "
|
|
238
|
+
"future release. For a permanent solution, override the "
|
|
239
|
+
"`sqlserver__generate_schema_name` macro in your project instead."
|
|
129
240
|
),
|
|
130
241
|
},
|
|
131
|
-
{
|
|
242
|
+
{ # ty: ignore[missing-typed-dict-key]
|
|
132
243
|
"name": "dbt_sqlserver_disable_empty_relation_aliases",
|
|
133
244
|
"default": True,
|
|
134
245
|
"description": (
|
|
@@ -137,7 +248,7 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
137
248
|
"out of alias generation temporarily for testing."
|
|
138
249
|
),
|
|
139
250
|
},
|
|
140
|
-
{
|
|
251
|
+
{ # ty: ignore[missing-typed-dict-key]
|
|
141
252
|
"name": "dbt_sqlserver_use_native_string_types",
|
|
142
253
|
"default": True,
|
|
143
254
|
"description": (
|
|
@@ -149,7 +260,7 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
149
260
|
"and will be removed in a future release."
|
|
150
261
|
),
|
|
151
262
|
},
|
|
152
|
-
{
|
|
263
|
+
{ # ty: ignore[missing-typed-dict-key]
|
|
153
264
|
"name": "dbt_sqlserver_enable_safe_type_expansion",
|
|
154
265
|
"default": False,
|
|
155
266
|
"description": (
|
|
@@ -159,7 +270,7 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
159
270
|
"and numeric(p,s) -> numeric(p2,s2) using alter column."
|
|
160
271
|
),
|
|
161
272
|
},
|
|
162
|
-
{
|
|
273
|
+
{ # ty: ignore[missing-typed-dict-key]
|
|
163
274
|
"name": "dbt_sqlserver_use_dbt_transactions",
|
|
164
275
|
"default": True,
|
|
165
276
|
"description": (
|
|
@@ -176,18 +287,114 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
176
287
|
|
|
177
288
|
@available.parse(lambda *a, **k: [])
|
|
178
289
|
def get_column_schema_from_query(self, sql: str) -> List[BaseColumn]:
|
|
179
|
-
"""Get a list of the Columns with names and data types from the given sql.
|
|
290
|
+
"""Get a list of the Columns with names and data types from the given sql.
|
|
291
|
+
|
|
292
|
+
Only the result *shape* is wanted, but the query still runs, so the
|
|
293
|
+
cursor comes back holding the whole result set. Usually that set is
|
|
294
|
+
empty: dbt-core's ``get_column_schema_from_query`` macro wraps the
|
|
295
|
+
query first, and ``sqlserver__get_empty_subquery_sql`` renders that as
|
|
296
|
+
``select * from (...) where 1 = 0``. A query that opens with a CTE
|
|
297
|
+
cannot be wrapped that way, though, and is passed through untouched
|
|
298
|
+
(dbt/include/sqlserver/macros/adapters/columns.sql), so snapshot
|
|
299
|
+
staging queries and CTE-headed contract models arrive here in full.
|
|
300
|
+
|
|
301
|
+
Either way the cursor must not be abandoned holding rows -- see
|
|
302
|
+
``_discard_pending_results`` for what that costs.
|
|
303
|
+
"""
|
|
304
|
+
if _SQL_COMMENT.sub("", sql).strip().lower().startswith("with"):
|
|
305
|
+
described = self._describe_result_set(sql)
|
|
306
|
+
if described is not None:
|
|
307
|
+
return described
|
|
308
|
+
|
|
180
309
|
_, cursor = self.connections.add_select_query(sql)
|
|
181
310
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
311
|
+
try:
|
|
312
|
+
columns = [
|
|
313
|
+
self.Column.create(
|
|
314
|
+
column_name, self.connections.data_type_code_to_name(column_type_code)
|
|
315
|
+
)
|
|
316
|
+
# https://peps.python.org/pep-0249/#description
|
|
317
|
+
for column_name, column_type_code, *_ in cursor.description
|
|
318
|
+
]
|
|
319
|
+
finally:
|
|
320
|
+
_discard_pending_results(cursor)
|
|
321
|
+
|
|
189
322
|
return columns
|
|
190
323
|
|
|
324
|
+
def _describe_result_set(self, sql: str) -> Optional[List[BaseColumn]]:
|
|
325
|
+
"""Read a query's column shape without running it, or None to fall back.
|
|
326
|
+
|
|
327
|
+
``sp_describe_first_result_set`` compiles the query and reports its
|
|
328
|
+
result shape, which is all this method ever wanted. It is already how
|
|
329
|
+
``sqlserver__get_columns_in_query`` handles CTEs (#698).
|
|
330
|
+
|
|
331
|
+
Returns None -- deliberately, rather than raising -- whenever the
|
|
332
|
+
describe cannot be trusted to match what executing would have reported:
|
|
333
|
+
an unsupported backend, a query it refuses to describe (it cannot see
|
|
334
|
+
through ``#temp`` tables, where executing works), or a type this
|
|
335
|
+
backend's driver has no known executed name for. The caller then
|
|
336
|
+
executes as before, which is slower but never disagrees with itself.
|
|
337
|
+
"""
|
|
338
|
+
credentials = self.connections.profile.credentials
|
|
339
|
+
if is_adbc_backend(credentials.backend):
|
|
340
|
+
# ADBC derives its type names from Arrow codes (int64 -> bigint,
|
|
341
|
+
# large_string -> varchar(max)), so the map above -- built for the
|
|
342
|
+
# pyodbc/mssql-python collapse -- would make the two branches
|
|
343
|
+
# disagree on that backend.
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
# Inline rather than bound: mssql-python binds str as varchar and the
|
|
347
|
+
# procedure demands nvarchar(max). columns.sql:24 escapes it the same
|
|
348
|
+
# way for the same reason.
|
|
349
|
+
describe_sql = "exec sp_describe_first_result_set @tsql = N'{}'".format(
|
|
350
|
+
sql.replace("'", "''")
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
_, cursor = self.connections.add_select_query(describe_sql)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.debug(f"Could not describe a CTE query, falling back to executing it: {e}")
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
fields = [description[0].lower() for description in cursor.description]
|
|
361
|
+
rows = cursor.fetchall()
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.debug(f"Could not read a described result set, executing the query: {e}")
|
|
364
|
+
return None
|
|
365
|
+
finally:
|
|
366
|
+
_discard_pending_results(cursor)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
hidden, name, type_name = (
|
|
370
|
+
fields.index("is_hidden"),
|
|
371
|
+
fields.index("name"),
|
|
372
|
+
fields.index("system_type_name"),
|
|
373
|
+
)
|
|
374
|
+
except ValueError: # pragma: no cover - shape is fixed by SQL Server
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
columns = []
|
|
378
|
+
for row in rows:
|
|
379
|
+
if row[hidden]:
|
|
380
|
+
continue
|
|
381
|
+
# "varchar(10)" / "decimal(10,2)" -> "varchar" / "decimal"
|
|
382
|
+
base_type = str(row[type_name]).split("(")[0].strip().lower()
|
|
383
|
+
executed_name = _executed_name_for_system_type(base_type, credentials.backend)
|
|
384
|
+
if executed_name is None or row[name] is None:
|
|
385
|
+
logger.debug(
|
|
386
|
+
f"Describing a CTE query reported {base_type!r}, which has no "
|
|
387
|
+
"equivalent in the executed path; executing it instead"
|
|
388
|
+
)
|
|
389
|
+
return None
|
|
390
|
+
columns.append(self.Column.create(row[name], executed_name))
|
|
391
|
+
|
|
392
|
+
# Every select has at least one column, so nothing described means
|
|
393
|
+
# sp_describe_first_result_set could not work the shape out. Returning
|
|
394
|
+
# an empty list would read as "this query has no columns" and surface
|
|
395
|
+
# as a baffling contract mismatch; execute instead.
|
|
396
|
+
return columns or None
|
|
397
|
+
|
|
191
398
|
@classmethod
|
|
192
399
|
def quote(cls, identifier: str) -> str:
|
|
193
400
|
"""Double-quote an identifier, doubling any embedded double quote.
|
|
@@ -381,7 +588,10 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
381
588
|
"""Return the number of rows in the given relation."""
|
|
382
589
|
sql = f"SELECT COUNT_BIG(*) FROM {relation}"
|
|
383
590
|
_, cursor = self.connections.add_select_query(sql)
|
|
384
|
-
|
|
591
|
+
try:
|
|
592
|
+
row = cursor.fetchone()
|
|
593
|
+
finally:
|
|
594
|
+
_discard_pending_results(cursor)
|
|
385
595
|
return int(row[0]) if row else 0
|
|
386
596
|
|
|
387
597
|
def expand_column_types(self, goal, current, max_rows: int = 1000000):
|
|
@@ -663,6 +873,52 @@ class SQLServerAdapter(SQLAdapter):
|
|
|
663
873
|
existing_columns=(list(existing_columns) if existing_columns is not None else None),
|
|
664
874
|
)
|
|
665
875
|
|
|
876
|
+
@available
|
|
877
|
+
def resolve_denies(self, model: Any, model_denies: Optional[dict] = None) -> Dict[str, list]:
|
|
878
|
+
"""Normalise the model-level `denies` config into a `{privilege:
|
|
879
|
+
[principals]}` map for `apply_denies`.
|
|
880
|
+
|
|
881
|
+
`model` is the Jinja `model` dict (`node.to_dict()`); its `grants` config
|
|
882
|
+
(under `model['config']`) is read only to warn when a principal is both
|
|
883
|
+
granted and denied the same privilege. `model_denies` is
|
|
884
|
+
`config.get('denies')`, already surface-merged by dbt.
|
|
885
|
+
|
|
886
|
+
Unsupported privileges (anything other than the object-level table
|
|
887
|
+
privileges) are warned and skipped rather than failing the run — the
|
|
888
|
+
warning surfaces the likely typo without taking down the build.
|
|
889
|
+
"""
|
|
890
|
+
model = model or {}
|
|
891
|
+
grant_config = (model.get("config") or {}).get("grants")
|
|
892
|
+
model_name = model.get("name") or model.get("alias") or "<unknown>"
|
|
893
|
+
resolved, warnings, unsupported = _resolve_denies(model_denies, grant_config, model_name)
|
|
894
|
+
for warning in warnings:
|
|
895
|
+
logger.warning(warning)
|
|
896
|
+
if unsupported:
|
|
897
|
+
from dbt.adapters.sqlserver.sqlserver_deny import SUPPORTED_PRIVILEGES
|
|
898
|
+
|
|
899
|
+
logger.warning(
|
|
900
|
+
f"On model '{model_name}', the `denies` config lists unsupported "
|
|
901
|
+
f"privilege(s): {', '.join(sorted(unsupported))}; skipping them. "
|
|
902
|
+
f"Object-level DENY is supported only for the table privileges: "
|
|
903
|
+
f"{', '.join(SUPPORTED_PRIVILEGES)}."
|
|
904
|
+
)
|
|
905
|
+
return resolved
|
|
906
|
+
|
|
907
|
+
@available
|
|
908
|
+
def deny_changes(self, existing_denies: Any, deny_config: Optional[dict]) -> dict:
|
|
909
|
+
"""Diff a resolved deny map against current `sys.database_permissions`.
|
|
910
|
+
|
|
911
|
+
`existing_denies` is the agate table from `get_show_deny_sql` (columns
|
|
912
|
+
`grantee`, `privilege_type`). Returns plain lists for jinja: `denies` and
|
|
913
|
+
`revokes`, each a list of `[privilege, principal]` pairs. The macro emits
|
|
914
|
+
`DENY` for the former and `REVOKE` for the latter."""
|
|
915
|
+
rows = []
|
|
916
|
+
if existing_denies is not None:
|
|
917
|
+
column_names = existing_denies.column_names
|
|
918
|
+
for row in existing_denies.rows:
|
|
919
|
+
rows.append(dict(zip(column_names, row)))
|
|
920
|
+
return _deny_changes(rows, deny_config or {})
|
|
921
|
+
|
|
666
922
|
|
|
667
923
|
COLUMNS_EQUAL_SQL = """
|
|
668
924
|
with diff_count as (
|