Skip to content

Commit d9bb235

Browse files
authored
Merge pull request #674 from Benjamin-Knight/feat/#613-query-options
Add query_options model config for SQL Server OPTION clauses (#613).
2 parents 36b68b8 + a5b5ba2 commit d9bb235

12 files changed

Lines changed: 968 additions & 17 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ coverage.xml
5252
# Django stuff:
5353
*.log
5454

55+
# dbt test logs (one subdir per test schema; accumulates indefinitely)
56+
logs/
57+
5558
# Sphinx documentation
5659
docs/_build/
5760

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### v1.10.0
4+
5+
- Add `query_options` / `query_options_raw` model configs for emitting SQL Server `OPTION` clauses on table, incremental (delete+insert / microbatch), snapshot, and unit_test materializations. See https://github.com/dbt-msft/dbt-sqlserver/issues/613.
6+
- `get_query_options()` is the new extension point for customising the emitted `OPTION` clause.
7+
- **Migration note:** `apply_label()` is preserved as a callable alias (emits LABEL only) in case you use it in your own project but is no longer called by adapter macros. Projects that override `apply_label()` to customise the OPTION clause must override `get_query_options()` instead.
8+
39
### v1.9.1
410

511
- Removes the dependency on `dbt-fabric`.

dbt/include/sqlserver/macros/adapters/catalog.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% macro sqlserver__get_catalog(information_schemas, schemas) -%}
2-
{% set query_label = apply_label() %}
2+
{% set query_label = get_query_options() %}
33
{%- call statement('catalog', fetch_result=True) -%}
44
{{ get_use_database_sql(information_schemas.database) }}
55
with
@@ -126,7 +126,7 @@
126126
{%- endmacro %}
127127

128128
{% macro sqlserver__get_catalog_relations(information_schema, relations) -%}
129-
{% set query_label = apply_label() %}
129+
{% set query_label = get_query_options() %}
130130
{%- set distinct_databases = relations | map(attribute='database') | unique | list -%}
131131

132132
{%- if distinct_databases | length == 1 -%}

dbt/include/sqlserver/macros/adapters/columns.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
{% endmacro %}
1313

1414
{% macro sqlserver__get_columns_in_query(select_sql) %}
15-
{% set query_label = apply_label() %}
15+
{% set query_label = get_query_options() %}
1616
{% call statement('get_columns_in_query', fetch_result=True, auto_begin=False) -%}
1717
select TOP 0 * from (
1818
{{ select_sql }}
@@ -66,7 +66,7 @@
6666
{% endmacro %}
6767
6868
{% macro sqlserver__get_columns_in_relation(relation) -%}
69-
{% set query_label = apply_label() %}
69+
{% set query_label = get_query_options() %}
7070
{% call statement('get_columns_in_relation', fetch_result=True) %}
7171
{{ get_use_database_sql(relation.database) }}
7272
select

dbt/include/sqlserver/macros/adapters/metadata.sql

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,97 @@
1+
{% macro get_query_options(parse_options=False) %}
2+
{{ log (config.get('query_tag','dbt-sqlserver'))}}
3+
{%- set query_label = config.get('query_tag','dbt-sqlserver') -%}
4+
{%- set query_options = config.get('query_options', {}) -%}
5+
{%- set query_options_raw = config.get('query_options_raw', []) -%}
6+
7+
{%- set options_list = ["LABEL = '" ~ query_label ~ "'"] -%}
8+
9+
{%- if parse_options -%}
10+
{%- set valid_options = [
11+
'HASH GROUP', 'ORDER GROUP',
12+
'CONCAT UNION', 'HASH UNION', 'MERGE UNION',
13+
'LOOP JOIN', 'MERGE JOIN', 'HASH JOIN',
14+
'DISABLE_OPTIMIZED_PLAN_FORCING',
15+
'EXPAND VIEWS',
16+
'FAST',
17+
'FORCE ORDER',
18+
'FORCE EXTERNALPUSHDOWN', 'DISABLE EXTERNALPUSHDOWN',
19+
'FORCE SCALEOUTEXECUTION', 'DISABLE SCALEOUTEXECUTION',
20+
'IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX',
21+
'KEEP PLAN',
22+
'KEEPFIXED PLAN',
23+
'MAX_GRANT_PERCENT',
24+
'MIN_GRANT_PERCENT',
25+
'MAXDOP',
26+
'MAXRECURSION',
27+
'NO_PERFORMANCE_SPOOL',
28+
'OPTIMIZE FOR UNKNOWN',
29+
'QUERYTRACEON',
30+
'RECOMPILE',
31+
'ROBUST PLAN',
32+
] -%}
33+
{#- SQL Server uses `OPTION (X = N)` for grant-percent hints, not `OPTION (X N)`. -#}
34+
{%- set equals_syntax_options = ['MAX_GRANT_PERCENT', 'MIN_GRANT_PERCENT'] -%}
35+
36+
{%- for key, value in query_options.items() -%}
37+
{%- if key | upper not in valid_options -%}
38+
{{ exceptions.raise_compiler_error("Invalid query option: '" ~ key ~ "'. Use query_options_raw for non-standard hints. Allowed: " ~ valid_options | join(', ')) }}
39+
{%- endif -%}
40+
41+
{%- if value is none -%}
42+
{%- do options_list.append(key | upper) -%}
43+
{%- else -%}
44+
{%- if value is not number -%}
45+
{{ exceptions.raise_compiler_error("Query option '" ~ key ~ "' value must be a number, got: '" ~ value ~ "'") }}
46+
{%- endif -%}
47+
{%- set separator = ' = ' if key | upper in equals_syntax_options else ' ' -%}
48+
{#- Render the value verbatim: ints become "1", floats become "12.5".
49+
MAX_GRANT_PERCENT / MIN_GRANT_PERCENT accept decimals 0.0100.0; integer-only
50+
options will surface a clear SQL Server parse error on invalid decimals. -#}
51+
{%- do options_list.append(key | upper ~ separator ~ value) -%}
52+
{%- endif -%}
53+
{%- endfor -%}
54+
55+
{#- query_options_raw bypasses the allowlist; users opt in to writing valid SQL Server syntax themselves.
56+
Shape-check only: a plain string would be iterated character-by-character into garbage. -#}
57+
{%- if query_options_raw is string or query_options_raw is mapping -%}
58+
{{ exceptions.raise_compiler_error("query_options_raw must be a list of strings, got: '" ~ query_options_raw ~ "'") }}
59+
{%- endif -%}
60+
{%- for raw in query_options_raw -%}
61+
{%- do options_list.append(raw) -%}
62+
{%- endfor -%}
63+
{%- endif -%}
64+
65+
OPTION ({{ options_list | join(', ') }});
66+
{% endmacro %}
67+
68+
{#- DEPRECATED: backward-compat alias for the pre-1.10 macro.
69+
70+
Calls to `{{ apply_label() }}` from user macros still resolve and emit
71+
a LABEL-only OPTION clause — but apply_label() is no longer the
72+
extension point. Adapter macros now call get_query_options() instead,
73+
so overriding apply_label() in a project's macros directory will have
74+
no effect on adapter-emitted SQL.
75+
76+
To customise the OPTION clause emitted by adapter macros (table,
77+
incremental, snapshot, unit_test), override get_query_options instead. -#}
178
{% macro apply_label() %}
279
{{ log (config.get('query_tag','dbt-sqlserver'))}}
380
{%- set query_label = config.get('query_tag','dbt-sqlserver') -%}
481
OPTION (LABEL = '{{query_label}}');
582
{% endmacro %}
683
84+
{#- Guard for materializations and incremental strategies that cannot emit OPTION clauses.
85+
Raises a compiler error if the user has configured query_options/query_options_raw. -#}
86+
{% macro raise_if_query_options_set(context_label) %}
87+
{%- if config.get('query_options') or config.get('query_options_raw') -%}
88+
{{ exceptions.raise_compiler_error(
89+
"query_options/query_options_raw is not supported on " ~ context_label
90+
~ ". Remove the config or switch to a supported materialization (table, incremental delete+insert, snapshot, unit_test)."
91+
) }}
92+
{%- endif -%}
93+
{% endmacro %}
94+
795
{% macro default__information_schema_hints() %}{% endmacro %}
896
{% macro sqlserver__information_schema_hints() %}with (nolock){% endmacro %}
997
@@ -27,14 +115,14 @@
27115
{% call statement('list_schemas', fetch_result=True, auto_begin=False) -%}
28116
{{ get_use_database_sql(database) }}
29117
select name as [schema]
30-
from sys.schemas {{ information_schema_hints() }} {{ apply_label() }}
118+
from sys.schemas {{ information_schema_hints() }} {{ get_query_options() }}
31119
{% endcall %}
32120
{{ return(load_result('list_schemas').table) }}
33121
{% endmacro %}
34122
35123
{% macro sqlserver__check_schema_exists(information_schema, schema) -%}
36124
{% call statement('check_schema_exists', fetch_result=True, auto_begin=False) -%}
37-
SELECT count(*) as schema_exist FROM sys.schemas WHERE name = '{{ schema }}' {{ apply_label() }}
125+
SELECT count(*) as schema_exist FROM sys.schemas WHERE name = '{{ schema }}' {{ get_query_options() }}
38126
{%- endcall %}
39127
{{ return(load_result('check_schema_exists').table) }}
40128
{% endmacro %}
@@ -58,7 +146,7 @@
58146
'view' as table_type
59147
from sys.views as v {{ information_schema_hints() }}
60148
where v.schema_id = @schema_id
61-
{{ apply_label() }}
149+
{{ get_query_options() }}
62150
{% endcall %}
63151
{{ return(load_result('list_relations_without_caching').table) }}
64152
{% endmacro %}
@@ -82,7 +170,7 @@
82170
'view' as table_type
83171
from sys.views as v {{ information_schema_hints() }}
84172
where v.schema_id = @schema_id and v.name = '{{ schema_relation.identifier }}'
85-
{{ apply_label() }}
173+
{{ get_query_options() }}
86174
{% endcall %}
87175
{{ return(load_result('get_relation_without_caching').table) }}
88176
{% endmacro %}
@@ -113,7 +201,7 @@
113201
upper(o.name) = upper('{{ relation.identifier }}')){%- if not loop.last %} or {% endif -%}
114202
{%- endfor -%}
115203
)
116-
{{ apply_label() }}
204+
{{ get_query_options() }}
117205
{%- endcall -%}
118206
{{ return(load_result('last_modified')) }}
119207

dbt/include/sqlserver/macros/adapters/relation.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
and refs.referenced_schema_name = '{{ relation.schema }}'
2323
and refs.referenced_entity_name = '{{ relation.identifier }}'
2424
and obj.type = 'V'
25-
{{ apply_label() }}
25+
{{ get_query_options() }}
2626
{% endcall %}
2727
{% set references = load_result('find_references')['data'] %}
2828
{% for reference in references -%}

dbt/include/sqlserver/macros/materializations/models/incremental/merge.sql

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% macro sqlserver__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates=none) %}
2-
{{ default__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates) }};
2+
{{ default__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates) }}
3+
{{ get_query_options(parse_options=True) }}
34
{% endmacro %}
45

56
{% macro sqlserver__get_insert_overwrite_merge_sql(target, source, dest_columns, predicates, include_sql_header) %}
@@ -8,7 +9,7 @@
89

910
{% macro sqlserver__get_delete_insert_merge_sql(target, source, unique_key, dest_columns, incremental_predicates=none) %}
1011

11-
{% set query_label = apply_label() %}
12+
{% set query_label = get_query_options(parse_options=True) %}
1213
{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}
1314

1415
{% if unique_key %}
@@ -57,6 +58,7 @@
5758
{%- set source = arg_dict["temp_relation"] -%}
5859
{%- set dest_columns = arg_dict["dest_columns"] -%}
5960
{%- set incremental_predicates = [] if arg_dict.get('incremental_predicates') is none else arg_dict.get('incremental_predicates') -%}
61+
{%- set query_label = get_query_options(parse_options=True) -%}
6062

6163
{#-- Add additional incremental_predicates to filter for batch --#}
6264
{% if model.config.get("__dbt_internal_microbatch_event_time_start") -%}
@@ -74,12 +76,14 @@
7476
{% for predicate in incremental_predicates %}
7577
{%- if not loop.first %}and {% endif -%} {{ predicate }}
7678
{% endfor %}
77-
);
79+
)
80+
{{ query_label }}
7881

7982
{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}
8083
insert into {{ target }} ({{ dest_cols_csv }})
8184
(
8285
select {{ dest_cols_csv }}
8386
from {{ source }}
8487
)
88+
{{ query_label }}
8589
{% endmacro %}

dbt/include/sqlserver/macros/materializations/models/unit_test/unit_test_create_table_as.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{% endmacro %}
1414

1515
{% macro sqlserver__unit_test_create_table_as(temporary, relation, sql) -%}
16-
{% set query_label = apply_label() %}
16+
{% set query_label = get_query_options(parse_options=True) %}
1717
{% set contract_config = config.get('contract') %}
1818
{% set is_nested_cte = check_for_nested_cte(sql) %}
1919

dbt/include/sqlserver/macros/materializations/snapshots/snapshot_merge.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
{% else %}
2222
and DBT_INTERNAL_DEST.{{ columns.dbt_valid_to }} is null
2323
{% endif %}
24-
{{ apply_label() }}
24+
{{ get_query_options(parse_options=True) }}
2525

2626
insert into {{ target_table }} ({{ insert_cols_csv }})
2727
select {{target_columns}} from {{ source_table }} as DBT_INTERNAL_SOURCE
2828
where DBT_INTERNAL_SOURCE.dbt_change_type = 'insert'
29-
{{ apply_label() }}
29+
{{ get_query_options(parse_options=True) }}
3030
{% endmacro %}

dbt/include/sqlserver/macros/relations/table/create.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% macro sqlserver__create_table_as(temporary, relation, sql) -%}
2-
{%- set query_label = apply_label() -%}
2+
{%- set query_label = get_query_options(parse_options=True) -%}
33
{%- set tmp_relation = relation.incorporate(path={"identifier": relation.identifier ~ '__dbt_tmp_vw'}, type='view') -%}
44

55
{%- do adapter.drop_relation(tmp_relation) -%}

0 commit comments

Comments
 (0)