Skip to content

Commit 4bd8917

Browse files
committed
fix(athena): wrap build_with_subquery source SQL on its own line
Trailing -- line comments in compiled_code/source_sql could swallow the closing paren of the subquery wrapper, producing broken SQL. Insert a newline before the closing paren in all four wrap sites: - incremental.sql empty_sql for the append branch - incremental.sql empty_sql for the iceberg merge branch - helpers.sql incremental_insert source_sql wrap - merge.sql iceberg_merge source_sql wrap Add unit tests asserting the full rendered SQL for the trailing-comment case in each location.
1 parent c31f06c commit 4bd8917

4 files changed

Lines changed: 89 additions & 6 deletions

File tree

dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/helpers.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@
7171
insert into {{ target_relation }} ({{ dest_cols_csv }})
7272
(
7373
select {{ dest_cols_csv }}
74-
from ({{ source_sql }}) _dbt_sbq
74+
from (
75+
{{ source_sql }}
76+
) _dbt_sbq
7577
);
7678
{%- endset -%}
7779
{%- else -%}

dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/incremental.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
{% if old_tmp_relation is not none %}
164164
{% do drop_relation(old_tmp_relation) %}
165165
{% endif %}
166-
{%- set empty_sql = 'SELECT * FROM (' ~ compiled_code ~ ') _dbt_sbq WITH NO DATA' -%}
166+
{%- set empty_sql = 'SELECT * FROM (\n' ~ compiled_code ~ '\n) _dbt_sbq WITH NO DATA' -%}
167167
{% do run_query(create_table_as(True, tmp_relation, empty_sql)) %}
168168

169169
{% set build_sql = incremental_insert(
@@ -219,7 +219,7 @@
219219
{% if old_tmp_relation is not none %}
220220
{% do drop_relation(old_tmp_relation) %}
221221
{% endif %}
222-
{%- set empty_sql = 'SELECT * FROM (' ~ compiled_code ~ ') _dbt_sbq WITH NO DATA' -%}
222+
{%- set empty_sql = 'SELECT * FROM (\n' ~ compiled_code ~ '\n) _dbt_sbq WITH NO DATA' -%}
223223
{% do run_query(create_table_as(True, tmp_relation, empty_sql)) %}
224224

225225
{% set build_sql = iceberg_merge(

dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/merge.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@
146146
{%- else -%}
147147
{%- if source_sql is not none -%}
148148
{%- set src_part -%}
149-
merge into {{ target_relation }} as target using ({{ source_sql }}) as src
149+
merge into {{ target_relation }} as target using (
150+
{{ source_sql }}
151+
) as src
150152
{%- endset -%}
151153
{%- else -%}
152154
{%- set src_part -%}

dbt-athena/tests/unit/test_build_with_subquery.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"incremental",
2525
)
2626
)
27+
_INCREMENTAL_SQL_PATH = os.path.join(_INCREMENTAL_DIR, "incremental.sql")
2728

2829

2930
class MockRelation:
@@ -165,7 +166,23 @@ def test_insert_uses_subquery(self):
165166
'insert into db.schema.tbl ("id", "msg", "color")'
166167
" ( select"
167168
' "id", "msg", "color"'
168-
" from (SELECT id, msg, color FROM src) _dbt_sbq );"
169+
" from ( SELECT id, msg, color FROM src ) _dbt_sbq );"
170+
)
171+
172+
def test_insert_subquery_isolates_trailing_line_comment(self):
173+
"""A trailing line comment in source_sql must not swallow the closing
174+
paren of the subquery wrapper. Full SQL is asserted so any regression
175+
in whitespace/newline handling is caught."""
176+
source_sql = "SELECT id, msg, color FROM src -- trailing comment"
177+
adapter, _ = _render_incremental_insert(source_sql=source_sql)
178+
assert adapter.last_sql == (
179+
'insert into db.schema.tbl ("id", "msg", "color")\n'
180+
" (\n"
181+
' select "id", "msg", "color"\n'
182+
" from (\n"
183+
" SELECT id, msg, color FROM src -- trailing comment\n"
184+
" ) _dbt_sbq\n"
185+
" );"
169186
)
170187

171188
def test_insert_without_subquery_uses_tmp_relation(self):
@@ -207,7 +224,28 @@ def test_merge_uses_subquery(self):
207224
adapter, _ = _render_iceberg_merge(source_sql="SELECT id, msg, color FROM src")
208225
assert _normalize(adapter.last_sql) == (
209226
"merge into db.schema.tbl as target"
210-
" using (SELECT id, msg, color FROM src) as src" + _EXPECTED_MERGE_CLAUSES
227+
" using ( SELECT id, msg, color FROM src ) as src" + _EXPECTED_MERGE_CLAUSES
228+
)
229+
230+
def test_merge_subquery_isolates_trailing_line_comment(self):
231+
"""A trailing line comment in source_sql must not swallow the closing
232+
paren of the subquery wrapper. Full SQL is asserted so any regression
233+
in whitespace/newline handling is caught."""
234+
source_sql = "SELECT id, msg, color FROM src -- trailing comment"
235+
adapter, _ = _render_iceberg_merge(source_sql=source_sql)
236+
assert adapter.last_sql == (
237+
"merge into db.schema.tbl as target using (\n"
238+
" SELECT id, msg, color FROM src -- trailing comment\n"
239+
" ) as src\n"
240+
" on (target.id = src.id\n"
241+
" \n"
242+
" )\n"
243+
" \n"
244+
" when matched \n"
245+
' then update set"msg" = src."msg","color" = src."color"\n'
246+
" when not matched \n"
247+
' then insert ("id", "msg", "color")\n'
248+
' values (src."id", src."msg", src."color")'
211249
)
212250

213251
def test_merge_without_subquery_uses_tmp_relation(self):
@@ -229,3 +267,44 @@ def test_subquery_too_many_partitions_raises_error(self):
229267
def test_without_subquery_too_many_partitions_falls_back_to_batch(self):
230268
_, context = _render_iceberg_merge(query_result="TOO_MANY_OPEN_PARTITIONS")
231269
context["exceptions"].raise_compiler_error.assert_not_called()
270+
271+
272+
# --- empty_sql in incremental.sql ---
273+
274+
275+
def _render_empty_sql(compiled_code):
276+
"""Render the same `{% set empty_sql ... %}` Jinja expression that lives in
277+
incremental.sql for the build_with_subquery branches. We extract it from the
278+
real file so the test catches drift if the line is rewritten."""
279+
with open(_INCREMENTAL_SQL_PATH) as f:
280+
src = f.read()
281+
matches = re.findall(r"\{%-?\s*set\s+empty_sql\s*=.*?-?%\}", src)
282+
assert matches, "expected at least one `{% set empty_sql = ... %}` in incremental.sql"
283+
rendered = []
284+
env = jinja2.Environment(extensions=["jinja2.ext.do"])
285+
for fragment in matches:
286+
template_src = fragment + "{{ empty_sql }}"
287+
rendered.append(env.from_string(template_src).render(compiled_code=compiled_code))
288+
return rendered
289+
290+
291+
class TestEmptySqlSubqueryWrapping:
292+
def test_empty_sql_wraps_compiled_code(self):
293+
renderings = _render_empty_sql("SELECT 1 AS id")
294+
assert renderings, "no empty_sql fragments rendered"
295+
for rendered in renderings:
296+
assert rendered == "SELECT * FROM (\nSELECT 1 AS id\n) _dbt_sbq WITH NO DATA"
297+
298+
def test_empty_sql_isolates_trailing_line_comment(self):
299+
"""The closing paren of empty_sql must land on a fresh line so a
300+
trailing -- line comment in compiled_code cannot comment it out."""
301+
compiled_code = "SELECT 1 AS id\n-- trailing comment"
302+
renderings = _render_empty_sql(compiled_code)
303+
assert renderings, "no empty_sql fragments rendered"
304+
for rendered in renderings:
305+
assert rendered == (
306+
"SELECT * FROM (\n"
307+
"SELECT 1 AS id\n"
308+
"-- trailing comment\n"
309+
") _dbt_sbq WITH NO DATA"
310+
)

0 commit comments

Comments
 (0)