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
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) -%}

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() %}
{%- call statement('catalog', fetch_result=True) -%}

with
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 @@ -11,7 +11,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 @@ -65,7 +65,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
93 changes: 88 additions & 5 deletions dbt/include/sqlserver/macros/adapters/metadata.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,92 @@
{% 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', []) -%}

{%- 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',
'PARAMETERIZATION',
'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 ' ' -%}
{%- do options_list.append(key | upper ~ separator ~ value | int) -%}
{%- endif -%}
{%- endfor -%}

{#- query_options_raw bypasses the allowlist; users opt in to writing valid SQL Server syntax themselves. -#}
{%- for raw in query_options_raw -%}
{%- do options_list.append(raw) -%}
{%- endfor -%}
{%- endif -%}

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

{#- Backward-compat alias for the pre-1.10 macro. Emits only the LABEL hint
and ignores query_options / query_options_raw. New adapter code should
call get_query_options() directly.

Note: this preserves non-breaking *consumption* of apply_label (user
macros calling `{{ apply_label() }}` still resolve), but does NOT
preserve non-breaking *override*: adapter macros no longer call
apply_label internally, so a project that overrides apply_label in its
own macros directory will find that override has no effect on adapter
behaviour. To customise the OPTION clause emitted by adapter macros,
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 +110,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 +142,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 +168,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 All @@ -105,7 +188,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 %}
4 changes: 2 additions & 2 deletions 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 All @@ -22,7 +22,7 @@
{{ "["~column~"]" }}{{ ", " if not loop.last }}
{% endfor %}
{%endset%}
INSERT INTO {{relation}} ({{listColumns}})
INSERT INTO {{relation}} WITH (TABLOCK) ({{listColumns}})
SELECT {{listColumns}} FROM {{tmp_relation}} {{ query_label }}

{% else %}
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
4 changes: 2 additions & 2 deletions tests/functional/adapter/dbt/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def models(self):
@pytest.fixture(scope="class")
def expected_sql(self):
return """
EXEC(' create view <model_identifier> as -- depends_on: <foreign_key_model_identifier> select ''blue'' as color, 1 as id, ''2019-01-01'' as date_day; ') EXEC(' CREATE TABLE <model_identifier> ( id int not null , color varchar(100), date_day varchar(100) ) INSERT INTO <model_identifier> ( [id], [color], [date_day] ) SELECT [id], [color], [date_day] FROM <model_identifier> ') EXEC('DROP VIEW IF EXISTS <model_identifier>
EXEC(' create view <model_identifier> as -- depends_on: <foreign_key_model_identifier> select ''blue'' as color, 1 as id, ''2019-01-01'' as date_day; ') EXEC(' CREATE TABLE <model_identifier> ( id int not null , color varchar(100), date_day varchar(100) ) INSERT INTO <model_identifier> WITH (TABLOCK) ( [id], [color], [date_day] ) SELECT [id], [color], [date_day] FROM <model_identifier> ') EXEC('DROP VIEW IF EXISTS <model_identifier>
"""

# EXEC('DROP view IF EXISTS <model_identifier>
Expand Down Expand Up @@ -592,7 +592,7 @@ def models(self):
@pytest.fixture(scope="class")
def expected_sql(self):
return """
EXEC(' create view <model_identifier> as -- depends_on: <foreign_key_model_identifier> select ''blue'' as color, 1 as id, ''2019-01-01'' as date_day; ') EXEC(' CREATE TABLE <model_identifier> ( id int not null , color varchar(100), date_day varchar(100) ) INSERT INTO <model_identifier> ( [id], [color], [date_day] ) SELECT [id], [color], [date_day] FROM <model_identifier> ') EXEC('DROP VIEW IF EXISTS <model_identifier>
EXEC(' create view <model_identifier> as -- depends_on: <foreign_key_model_identifier> select ''blue'' as color, 1 as id, ''2019-01-01'' as date_day; ') EXEC(' CREATE TABLE <model_identifier> ( id int not null , color varchar(100), date_day varchar(100) ) INSERT INTO <model_identifier> WITH (TABLOCK) ( [id], [color], [date_day] ) SELECT [id], [color], [date_day] FROM <model_identifier> ') EXEC('DROP VIEW IF EXISTS <model_identifier>
"""

def test__model_constraints_ddl(self, project, expected_sql):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Interaction coverage for #613 (query_options) + #640 (TABLOCK on contract INSERT).

Each feature is exercised by its own test suite in isolation. This file verifies
that when both are present, a contract-enforced table model with query_options
emits both `WITH (TABLOCK)` (the INSERT target hint) and the `OPTION (...)`
query hint without either path interfering with the other.

The test lives on a dedicated branch that merges both feature branches because
neither individual branch can exercise the combined output: #640 alone ignores
query_options config, and #613 alone uses the non-contract `SELECT * INTO`
path which has no TABLOCK to emit.
"""

import os

import pytest

from dbt.tests.util import run_dbt

model_sql = """
{{ config(materialized='table', query_options={'MAXDOP': 1}) }}
select 1 as id
"""

model_yml = """
version: 2
models:
- name: contract_with_options
config:
contract:
enforced: true
columns:
- name: id
data_type: int
"""


class TestContractTableWithQueryOptions:
@pytest.fixture(scope="class")
def models(self):
return {
"contract_with_options.sql": model_sql,
"schema.yml": model_yml,
}

def test_both_hints_in_compiled_sql(self, project):
results = run_dbt(["run"])
assert len(results) == 1
assert results[0].status == "success"

target_dir = os.path.join(project.project_root, "target", "run")
for root, _dirs, files in os.walk(target_dir):
if "contract_with_options.sql" in files:
with open(os.path.join(root, "contract_with_options.sql"), "r") as f:
sql = f.read()
break
else:
raise AssertionError("Could not find compiled contract_with_options.sql")

# TABLOCK is the INSERT target hint (#640).
assert "WITH (TABLOCK)" in sql
# MAXDOP comes from query_options (#613).
assert "MAXDOP 1" in sql
# The default LABEL is always present.
assert "LABEL =" in sql
Loading