Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ coverage.xml
# Django stuff:
*.log

# dbt test logs (one subdir per test schema; accumulates indefinitely)
logs/

# Sphinx documentation
docs/_build/

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

### v1.10.0

- 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.
- `get_query_options()` is the new extension point for customising the emitted `OPTION` clause.
- **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.

### v1.9.1

- Removes the dependency on `dbt-fabric`.
Expand Down
4 changes: 2 additions & 2 deletions dbt/include/sqlserver/macros/adapters/catalog.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% macro sqlserver__get_catalog(information_schemas, schemas) -%}
{% set query_label = apply_label() %}
{% set query_label = get_query_options() %}
{%- call statement('catalog', fetch_result=True) -%}
{{ get_use_database_sql(information_schemas.database) }}
with
Expand Down Expand Up @@ -126,7 +126,7 @@
{%- endmacro %}

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

{%- if distinct_databases | length == 1 -%}
Expand Down
4 changes: 2 additions & 2 deletions dbt/include/sqlserver/macros/adapters/columns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{% endmacro %}

{% macro sqlserver__get_columns_in_query(select_sql) %}
{% set query_label = apply_label() %}
{% set query_label = get_query_options() %}
{% call statement('get_columns_in_query', fetch_result=True, auto_begin=False) -%}
select TOP 0 * from (
{{ select_sql }}
Expand Down Expand Up @@ -66,7 +66,7 @@
{% endmacro %}

{% macro sqlserver__get_columns_in_relation(relation) -%}
{% set query_label = apply_label() %}
{% set query_label = get_query_options() %}
{% call statement('get_columns_in_relation', fetch_result=True) %}
{{ get_use_database_sql(relation.database) }}
with mapping as (
Expand Down
98 changes: 93 additions & 5 deletions dbt/include/sqlserver/macros/adapters/metadata.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,97 @@
{% macro get_query_options(parse_options=False) %}
{{ log (config.get('query_tag','dbt-sqlserver'))}}
{%- set query_label = config.get('query_tag','dbt-sqlserver') -%}
{%- set query_options = config.get('query_options', {}) -%}
{%- set query_options_raw = config.get('query_options_raw', []) -%}
Comment thread
Benjamin-Knight marked this conversation as resolved.

{%- set options_list = ["LABEL = '" ~ query_label ~ "'"] -%}

{%- if parse_options -%}
{%- set valid_options = [
'HASH GROUP', 'ORDER GROUP',
'CONCAT UNION', 'HASH UNION', 'MERGE UNION',
'LOOP JOIN', 'MERGE JOIN', 'HASH JOIN',
'DISABLE_OPTIMIZED_PLAN_FORCING',
'EXPAND VIEWS',
'FAST',
'FORCE ORDER',
'FORCE EXTERNALPUSHDOWN', 'DISABLE EXTERNALPUSHDOWN',
'FORCE SCALEOUTEXECUTION', 'DISABLE SCALEOUTEXECUTION',
'IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX',
'KEEP PLAN',
'KEEPFIXED PLAN',
'MAX_GRANT_PERCENT',
'MIN_GRANT_PERCENT',
'MAXDOP',
'MAXRECURSION',
'NO_PERFORMANCE_SPOOL',
'OPTIMIZE FOR UNKNOWN',
'QUERYTRACEON',
'RECOMPILE',
'ROBUST PLAN',
] -%}
{#- SQL Server uses `OPTION (X = N)` for grant-percent hints, not `OPTION (X N)`. -#}
{%- set equals_syntax_options = ['MAX_GRANT_PERCENT', 'MIN_GRANT_PERCENT'] -%}

{%- for key, value in query_options.items() -%}
{%- if key | upper not in valid_options -%}
{{ exceptions.raise_compiler_error("Invalid query option: '" ~ key ~ "'. Use query_options_raw for non-standard hints. Allowed: " ~ valid_options | join(', ')) }}
{%- endif -%}

{%- if value is none -%}
{%- do options_list.append(key | upper) -%}
{%- else -%}
{%- if value is not number -%}
{{ exceptions.raise_compiler_error("Query option '" ~ key ~ "' value must be a number, got: '" ~ value ~ "'") }}
{%- endif -%}
{%- set separator = ' = ' if key | upper in equals_syntax_options else ' ' -%}
{#- Render the value verbatim: ints become "1", floats become "12.5".
MAX_GRANT_PERCENT / MIN_GRANT_PERCENT accept decimals 0.0–100.0; integer-only
options will surface a clear SQL Server parse error on invalid decimals. -#}
{%- do options_list.append(key | upper ~ separator ~ value) -%}
{%- endif -%}
{%- endfor -%}
Comment thread
Benjamin-Knight marked this conversation as resolved.

{#- query_options_raw bypasses the allowlist; users opt in to writing valid SQL Server syntax themselves.
Shape-check only: a plain string would be iterated character-by-character into garbage. -#}
{%- if query_options_raw is string or query_options_raw is mapping -%}
{{ exceptions.raise_compiler_error("query_options_raw must be a list of strings, got: '" ~ query_options_raw ~ "'") }}
{%- endif -%}
{%- for raw in query_options_raw -%}
{%- do options_list.append(raw) -%}
{%- endfor -%}
{%- endif -%}

OPTION ({{ options_list | join(', ') }});
{% endmacro %}

{#- DEPRECATED: backward-compat alias for the pre-1.10 macro.

Calls to `{{ apply_label() }}` from user macros still resolve and emit
a LABEL-only OPTION clause — but apply_label() is no longer the
extension point. Adapter macros now call get_query_options() instead,
so overriding apply_label() in a project's macros directory will have
no effect on adapter-emitted SQL.

To customise the OPTION clause emitted by adapter macros (table,
incremental, snapshot, unit_test), override get_query_options instead. -#}
{% macro apply_label() %}
{{ log (config.get('query_tag','dbt-sqlserver'))}}
{%- set query_label = config.get('query_tag','dbt-sqlserver') -%}
OPTION (LABEL = '{{query_label}}');
{% endmacro %}

{#- Guard for materializations and incremental strategies that cannot emit OPTION clauses.
Raises a compiler error if the user has configured query_options/query_options_raw. -#}
{% macro raise_if_query_options_set(context_label) %}
{%- if config.get('query_options') or config.get('query_options_raw') -%}
{{ exceptions.raise_compiler_error(
"query_options/query_options_raw is not supported on " ~ context_label
~ ". Remove the config or switch to a supported materialization (table, incremental delete+insert, snapshot, unit_test)."
) }}
{%- endif -%}
{% endmacro %}

{% macro default__information_schema_hints() %}{% endmacro %}
{% macro sqlserver__information_schema_hints() %}with (nolock){% endmacro %}

Expand All @@ -27,14 +115,14 @@
{% call statement('list_schemas', fetch_result=True, auto_begin=False) -%}
{{ get_use_database_sql(database) }}
select name as [schema]
from sys.schemas {{ information_schema_hints() }} {{ apply_label() }}
from sys.schemas {{ information_schema_hints() }} {{ get_query_options() }}
{% endcall %}
{{ return(load_result('list_schemas').table) }}
{% endmacro %}

{% macro sqlserver__check_schema_exists(information_schema, schema) -%}
{% call statement('check_schema_exists', fetch_result=True, auto_begin=False) -%}
SELECT count(*) as schema_exist FROM sys.schemas WHERE name = '{{ schema }}' {{ apply_label() }}
SELECT count(*) as schema_exist FROM sys.schemas WHERE name = '{{ schema }}' {{ get_query_options() }}
{%- endcall %}
{{ return(load_result('check_schema_exists').table) }}
{% endmacro %}
Expand All @@ -59,7 +147,7 @@
)
select * from base
where [schema] like '{{ schema_relation.schema }}'
{{ apply_label() }}
{{ get_query_options() }}
{% endcall %}
{{ return(load_result('list_relations_without_caching').table) }}
{% endmacro %}
Expand All @@ -85,7 +173,7 @@
select * from base
where [schema] like '{{ schema_relation.schema }}'
and [name] like '{{ schema_relation.identifier }}'
{{ apply_label() }}
{{ get_query_options() }}
{% endcall %}
{{ return(load_result('get_relation_without_caching').table) }}
{% endmacro %}
Expand Down Expand Up @@ -119,7 +207,7 @@
upper(o.name) = upper('{{ relation.identifier }}')){%- if not loop.last %} or {% endif -%}
{%- endfor -%}
)
{{ apply_label() }}
{{ get_query_options() }}
{%- endcall -%}
{{ return(load_result('last_modified')) }}

Expand Down
2 changes: 1 addition & 1 deletion dbt/include/sqlserver/macros/adapters/relation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
and refs.referenced_schema_name = '{{ relation.schema }}'
and refs.referenced_entity_name = '{{ relation.identifier }}'
and obj.type = 'V'
{{ apply_label() }}
{{ get_query_options() }}
{% endcall %}
{% set references = load_result('find_references')['data'] %}
{% for reference in references -%}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% macro sqlserver__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates=none) %}
{{ default__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates) }};
{{ default__get_merge_sql(target, source, unique_key, dest_columns, incremental_predicates) }}
{{ get_query_options(parse_options=True) }}
{% endmacro %}

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

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

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

{% if unique_key %}
Expand Down Expand Up @@ -57,6 +58,7 @@
{%- set source = arg_dict["temp_relation"] -%}
{%- set dest_columns = arg_dict["dest_columns"] -%}
{%- set incremental_predicates = [] if arg_dict.get('incremental_predicates') is none else arg_dict.get('incremental_predicates') -%}
{%- set query_label = get_query_options(parse_options=True) -%}

{#-- Add additional incremental_predicates to filter for batch --#}
{% if model.config.get("__dbt_internal_microbatch_event_time_start") -%}
Expand All @@ -74,12 +76,14 @@
{% for predicate in incremental_predicates %}
{%- if not loop.first %}and {% endif -%} {{ predicate }}
{% endfor %}
);
)
{{ query_label }}

{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}
insert into {{ target }} ({{ dest_cols_csv }})
(
select {{ dest_cols_csv }}
from {{ source }}
)
{{ query_label }}
{% endmacro %}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{% endmacro %}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
{% else %}
and DBT_INTERNAL_DEST.{{ columns.dbt_valid_to }} is null
{% endif %}
{{ apply_label() }}
{{ get_query_options(parse_options=True) }}

insert into {{ target_table }} ({{ insert_cols_csv }})
select {{target_columns}} from {{ source_table }} as DBT_INTERNAL_SOURCE
where DBT_INTERNAL_SOURCE.dbt_change_type = 'insert'
{{ apply_label() }}
{{ get_query_options(parse_options=True) }}
{% endmacro %}
2 changes: 1 addition & 1 deletion dbt/include/sqlserver/macros/relations/table/create.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% macro sqlserver__create_table_as(temporary, relation, sql) -%}
{%- set query_label = apply_label() -%}
{%- set query_label = get_query_options(parse_options=True) -%}
{%- set tmp_relation = relation.incorporate(path={"identifier": relation.identifier ~ '__dbt_tmp_vw'}, type='view') -%}

{%- do adapter.drop_relation(tmp_relation) -%}
Expand Down
6 changes: 6 additions & 0 deletions dbt/include/sqlserver/macros/relations/views/create.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{% macro sqlserver__create_view_as(relation, sql) -%}
{#- Only guard against user-configured view materializations; this macro is also
called for intermediate temp views during table/snapshot materializations,
where query_options is intended for the *outer* DML and shouldn't trip a guard here. -#}
{%- if config.get('materialized') == 'view' -%}
{{ raise_if_query_options_set('view materializations (SQL Server does not accept OPTION on CREATE VIEW)') }}
{%- endif -%}

{{ get_use_database_sql(relation.database) }}
{% set contract_config = config.get('contract') %}
Expand Down
Loading