Skip to content

Commit bdf7b5d

Browse files
authored
Merge branch 'master' into test/tablock-query-options-interaction
2 parents 82d2de3 + 2b785c5 commit bdf7b5d

10 files changed

Lines changed: 290 additions & 88 deletions

File tree

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/adapters/sqlserver/sqlserver_adapter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class SQLServerAdapter(SQLAdapter):
5050

5151
def __init__(self, config, mp_context=None):
5252
super().__init__(config, mp_context)
53+
SQLServerRelation.disable_empty_relation_aliases = (
54+
self.behavior.dbt_sqlserver_disable_empty_relation_aliases
55+
)
5356
if self.behavior.dbt_sqlserver_use_native_string_types:
5457
self.Column = SQLServerColumnNative
5558

@@ -76,6 +79,15 @@ def _behavior_flags(self) -> List[BehaviorFlag]:
7679
"macro in your project instead."
7780
),
7881
},
82+
{
83+
"name": "dbt_sqlserver_disable_empty_relation_aliases",
84+
"default": True,
85+
"description": (
86+
"When True, SQL Server limited relations used by --empty and sample mode "
87+
"do not automatically receive dbt-generated aliases. Set this false to opt "
88+
"out of alias generation temporarily for testing."
89+
),
90+
},
7991
{
8092
"name": "dbt_sqlserver_use_native_string_types",
8193
"default": False,

dbt/adapters/sqlserver/sqlserver_relation.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass, field
2-
from typing import Optional, Type
2+
from typing import ClassVar, Optional, Type
33

44
from dbt_common.exceptions import DbtRuntimeError
55

@@ -20,19 +20,26 @@ class SQLServerRelation(BaseRelation):
2020
default_factory=lambda: SQLServerIncludePolicy()
2121
)
2222
quote_policy: SQLServerQuotePolicy = field(default_factory=lambda: SQLServerQuotePolicy())
23+
disable_empty_relation_aliases: ClassVar[bool] = True
2324

2425
@classproperty
2526
def get_relation_type(cls) -> Type[SQLServerRelationType]:
2627
return SQLServerRelationType
2728

29+
def _render_limited_alias(self) -> str:
30+
if self.disable_empty_relation_aliases:
31+
return ""
32+
33+
return super()._render_limited_alias()
34+
2835
def render_limited(self) -> str:
2936
rendered = self.render()
3037
if self.limit is None:
3138
return rendered
3239
elif self.limit == 0:
33-
return f"(select * from {rendered} where 1=0) AS {self._render_limited_alias()}"
40+
return f"(select * from {rendered} where 1=0){self._render_limited_alias()}"
3441
else:
35-
return f"(select TOP {self.limit} * from {rendered}) AS {self._render_limited_alias()}"
42+
return f"(select TOP {self.limit} * from {rendered}){self._render_limited_alias()}"
3643

3744
def __post_init__(self):
3845
# Check for length of Redshift table/view names.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127

128128
{% macro sqlserver__get_catalog_relations(information_schema, relations) -%}
129129
{% set query_label = get_query_options() %}
130-
{%- call statement('catalog', fetch_result=True) -%}
130+
{%- set distinct_databases = relations | map(attribute='database') | unique | list -%}
131131

132132
{%- if distinct_databases | length == 1 -%}
133133
{%- call statement('catalog', fetch_result=True) -%}

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

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,31 +69,20 @@
6969
{% set query_label = get_query_options() %}
7070
{% call statement('get_columns_in_relation', fetch_result=True) %}
7171
{{ get_use_database_sql(relation.database) }}
72-
with mapping as (
73-
select
74-
row_number() over (partition by object_name(c.object_id) order by c.column_id) as ordinal_position,
75-
c.name collate database_default as column_name,
76-
t.name as data_type,
77-
case
78-
when (t.name in ('nchar', 'nvarchar', 'sysname') and c.max_length <> -1) then c.max_length / 2
79-
else c.max_length
80-
end as character_maximum_length,
81-
c.precision as numeric_precision,
82-
c.scale as numeric_scale
83-
from sys.columns c {{ information_schema_hints() }}
84-
inner join sys.types t {{ information_schema_hints() }}
85-
on c.user_type_id = t.user_type_id
86-
where c.object_id = object_id('{{ 'tempdb..' ~ relation.include(database=false, schema=false) if '#' in relation.identifier else relation }}')
87-
)
88-
8972
select
90-
column_name,
91-
data_type,
92-
character_maximum_length,
93-
numeric_precision,
94-
numeric_scale
95-
from mapping
96-
order by ordinal_position
73+
c.name collate database_default as column_name,
74+
t.name as data_type,
75+
case
76+
when (t.name in ('nchar', 'nvarchar', 'sysname') and c.max_length <> -1) then c.max_length / 2
77+
else c.max_length
78+
end as character_maximum_length,
79+
c.precision as numeric_precision,
80+
c.scale as numeric_scale
81+
from sys.columns c {{ information_schema_hints() }}
82+
inner join sys.types t {{ information_schema_hints() }}
83+
on c.user_type_id = t.user_type_id
84+
where c.object_id = object_id('{{ 'tempdb..' ~ relation.include(database=false, schema=false) if '#' in relation.identifier else relation }}')
85+
order by c.column_id
9786
{{ query_label }}
9887
9988
{% endcall %}

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

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
'MAXRECURSION',
2727
'NO_PERFORMANCE_SPOOL',
2828
'OPTIMIZE FOR UNKNOWN',
29-
'PARAMETERIZATION',
3029
'QUERYTRACEON',
3130
'RECOMPILE',
3231
'ROBUST PLAN',
@@ -46,11 +45,18 @@
4645
{{ exceptions.raise_compiler_error("Query option '" ~ key ~ "' value must be a number, got: '" ~ value ~ "'") }}
4746
{%- endif -%}
4847
{%- set separator = ' = ' if key | upper in equals_syntax_options else ' ' -%}
49-
{%- do options_list.append(key | upper ~ separator ~ value | int) -%}
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) -%}
5052
{%- endif -%}
5153
{%- endfor -%}
5254

53-
{#- query_options_raw bypasses the allowlist; users opt in to writing valid SQL Server syntax themselves. -#}
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 -%}
5460
{%- for raw in query_options_raw -%}
5561
{%- do options_list.append(raw) -%}
5662
{%- endfor -%}
@@ -59,17 +65,16 @@
5965
OPTION ({{ options_list | join(', ') }});
6066
{% endmacro %}
6167

62-
{#- Backward-compat alias for the pre-1.10 macro. Emits only the LABEL hint
63-
and ignores query_options / query_options_raw. New adapter code should
64-
call get_query_options() directly.
65-
66-
Note: this preserves non-breaking *consumption* of apply_label (user
67-
macros calling `{{ apply_label() }}` still resolve), but does NOT
68-
preserve non-breaking *override*: adapter macros no longer call
69-
apply_label internally, so a project that overrides apply_label in its
70-
own macros directory will find that override has no effect on adapter
71-
behaviour. To customise the OPTION clause emitted by adapter macros,
72-
override get_query_options instead. -#}
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. -#}
7378
{% macro apply_label() %}
7479
{{ log (config.get('query_tag','dbt-sqlserver'))}}
7580
{%- set query_label = config.get('query_tag','dbt-sqlserver') -%}
@@ -125,23 +130,22 @@
125130
{% macro sqlserver__list_relations_without_caching(schema_relation) -%}
126131
{% call statement('list_relations_without_caching', fetch_result=True) -%}
127132
{{ get_use_database_sql(schema_relation.database) }}
128-
with base as (
129-
select
130-
DB_NAME() as [database],
131-
t.name as [name],
132-
SCHEMA_NAME(t.schema_id) as [schema],
133-
'table' as table_type
134-
from sys.tables as t {{ information_schema_hints() }}
135-
union all
136-
select
137-
DB_NAME() as [database],
138-
v.name as [name],
139-
SCHEMA_NAME(v.schema_id) as [schema],
140-
'view' as table_type
141-
from sys.views as v {{ information_schema_hints() }}
142-
)
143-
select * from base
144-
where [schema] like '{{ schema_relation.schema }}'
133+
declare @schema_id int = schema_id('{{ schema_relation.schema }}');
134+
select
135+
DB_NAME() as [database],
136+
t.name as [name],
137+
'{{ schema_relation.schema }}' as [schema],
138+
'table' as table_type
139+
from sys.tables as t {{ information_schema_hints() }}
140+
where t.schema_id = @schema_id
141+
union all
142+
select
143+
DB_NAME() as [database],
144+
v.name as [name],
145+
'{{ schema_relation.schema }}' as [schema],
146+
'view' as table_type
147+
from sys.views as v {{ information_schema_hints() }}
148+
where v.schema_id = @schema_id
145149
{{ get_query_options() }}
146150
{% endcall %}
147151
{{ return(load_result('list_relations_without_caching').table) }}
@@ -150,24 +154,22 @@
150154
{% macro sqlserver__get_relation_without_caching(schema_relation) -%}
151155
{% call statement('get_relation_without_caching', fetch_result=True) -%}
152156
{{ get_use_database_sql(schema_relation.database) }}
153-
with base as (
154-
select
155-
DB_NAME() as [database],
156-
t.name as [name],
157-
SCHEMA_NAME(t.schema_id) as [schema],
158-
'table' as table_type
159-
from sys.tables as t {{ information_schema_hints() }}
160-
union all
161-
select
162-
DB_NAME() as [database],
163-
v.name as [name],
164-
SCHEMA_NAME(v.schema_id) as [schema],
165-
'view' as table_type
166-
from sys.views as v {{ information_schema_hints() }}
167-
)
168-
select * from base
169-
where [schema] like '{{ schema_relation.schema }}'
170-
and [name] like '{{ schema_relation.identifier }}'
157+
declare @schema_id int = schema_id('{{ schema_relation.schema }}');
158+
select
159+
DB_NAME() as [database],
160+
t.name as [name],
161+
'{{ schema_relation.schema }}' as [schema],
162+
'table' as table_type
163+
from sys.tables as t {{ information_schema_hints() }}
164+
where t.schema_id = @schema_id and t.name = '{{ schema_relation.identifier }}'
165+
union all
166+
select
167+
DB_NAME() as [database],
168+
v.name as [name],
169+
'{{ schema_relation.schema }}' as [schema],
170+
'view' as table_type
171+
from sys.views as v {{ information_schema_hints() }}
172+
where v.schema_id = @schema_id and v.name = '{{ schema_relation.identifier }}'
171173
{{ get_query_options() }}
172174
{% endcall %}
173175
{{ return(load_result('get_relation_without_caching').table) }}
@@ -178,13 +180,10 @@
178180
{% endmacro %}
179181
180182
{% macro sqlserver__get_view_definition_sql(relation) -%}
183+
{%- set object_name = "quotename('" ~ relation.schema ~ "') + '.' + quotename('" ~ relation.identifier ~ "')" -%}
181184
{{ get_use_database_sql(relation.database) }}
182-
select object_definition(v.object_id) as definition
183-
from sys.views as v {{ information_schema_hints() }}
184-
inner join sys.schemas as s {{ information_schema_hints() }}
185-
on v.schema_id = s.schema_id
186-
where upper(s.name) = upper('{{ relation.schema }}')
187-
and upper(v.name) = upper('{{ relation.identifier }}')
185+
select object_definition(object_id({{ object_name }}, 'V')) as definition
186+
where object_id({{ object_name }}, 'V') is not null
188187
{% endmacro %}
189188
190189
{% macro sqlserver__get_relation_last_modified(information_schema, relations) -%}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ classifiers = [
2727
"Programming Language :: Python :: 3.13",
2828
]
2929
dependencies = [
30-
"dbt-core>=1.10.0,<1.11.0",
30+
"dbt-core>=1.10.0,<2.0",
3131
"dbt-common>=1.22.0,<2.0",
3232
"dbt-adapters>=1.15.2,<2.0",
3333
]

tests/functional/adapter/dbt/test_empty.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@
1212

1313
model_sql_sqlserver = """
1414
select *
15-
from {{ ref('model_input') }}
15+
from {{ ref('model_input') }} as model_input_alias
1616
union all
1717
select *
18-
from {{ source('seed_sources', 'raw_source') }}
18+
from {{ source('seed_sources', 'raw_source') }} as raw_source_alias
1919
"""
2020

2121
model_inline_sql_sqlserver = """
22-
select * from {{ source('seed_sources', 'raw_source') }}
22+
select * from {{ source('seed_sources', 'raw_source') }} as raw_source_alias
23+
"""
24+
25+
model_sql_user_alias_sqlserver = """
26+
select user_alias.id as id
27+
from {{ ref('model_input') }} as user_alias
28+
inner join {{ source('seed_sources', 'raw_source') }} as source_alias
29+
on user_alias.id = source_alias.id
2330
"""
2431

2532

@@ -47,6 +54,49 @@ def test_run_with_empty(self, project):
4754
self.assert_row_count(project, "model", 0)
4855

4956

57+
class TestEmptyWithUserAlias(BaseTestEmpty):
58+
@pytest.fixture(scope="class")
59+
def models(self):
60+
return {
61+
"model_input.sql": model_input_sql,
62+
"model.sql": model_sql_user_alias_sqlserver,
63+
"sources.yml": schema_sources_yml,
64+
}
65+
66+
def test_run_with_empty(self, project):
67+
run_dbt(["seed"])
68+
69+
run_dbt(["run", "--empty"])
70+
self.assert_row_count(project, "model", 0)
71+
72+
73+
@pytest.mark.xfail(
74+
reason="Upstream dbt empty-mode alias handling needs to be contextual aware.",
75+
)
76+
class TestEmptyWithUserAliasAndNoAliasFlag(BaseTestEmpty):
77+
@pytest.fixture(scope="class")
78+
def models(self):
79+
return {
80+
"model_input.sql": model_input_sql,
81+
"model.sql": model_sql_user_alias_sqlserver,
82+
"sources.yml": schema_sources_yml,
83+
}
84+
85+
@pytest.fixture(scope="class")
86+
def project_config_update(self):
87+
return {
88+
"flags": {
89+
"dbt_sqlserver_disable_empty_relation_aliases": False,
90+
}
91+
}
92+
93+
def test_run_with_empty(self, project):
94+
run_dbt(["seed"])
95+
96+
run_dbt(["run", "--empty"])
97+
self.assert_row_count(project, "model", 0)
98+
99+
50100
class TestemptyInlineSourceRef(BaseTestEmptyInlineSourceRef):
51101
@pytest.fixture(scope="class")
52102
def models(self):

0 commit comments

Comments
 (0)