Skip to content

Commit b58d44b

Browse files
Respect QUOTED_IDENTIFIERS_IGNORE_CASE with dynamic tables (#1115)
Co-authored-by: Colin Rogers <[email protected]>
1 parent 5bd82b4 commit b58d44b

File tree

4 files changed

+148
-32
lines changed

4 files changed

+148
-32
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Respect QUOTED_IDENTIFIERS_IGNORE_CASE with dynamic tables.
3+
time: 2025-05-22T03:15:31.20101-07:00
4+
custom:
5+
Author: versusfacit
6+
Issue: "993"

dbt-snowflake/src/dbt/adapters/snowflake/impl.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,60 @@ def build_catalog_relation(self, model: RelationConfig) -> Optional[CatalogRelat
478478
catalog_integration = self.get_catalog_integration(catalog)
479479
return catalog_integration.build_relation(model)
480480
return None
481+
482+
@available
483+
def describe_dynamic_table(self, relation: RelationConfig) -> Dict[str, Any]:
484+
"""
485+
Get all relevant metadata about a dynamic table to return as a dict to Agate Table row
486+
487+
Args:
488+
relation (SnowflakeRelation): the relation to describe
489+
"""
490+
491+
original_val: Optional[str] = None
492+
try:
493+
# Store old QUOTED_IDENTIFIERS_IGNORE_CASE
494+
show_param_sql = "show parameters like 'QUOTED_IDENTIFIERS_IGNORE_CASE' in SESSION"
495+
show_param_res = self.execute(show_param_sql)
496+
if show_param_res:
497+
param_qid = show_param_res[0].query_id
498+
scan_param_sql = f"SELECT * FROM TABLE(RESULT_SCAN('{param_qid}'))"
499+
param_scan_res, rows = self.execute(scan_param_sql, fetch=True)
500+
if param_scan_res and param_scan_res.code == "SUCCESS":
501+
try:
502+
original_val = rows[0][1]
503+
except (IndexError, TypeError):
504+
original_val = None
505+
506+
# falsify QUOTED_IDENTIFIERS_IGNORE_CASE for execution only
507+
self.execute("alter session set QUOTED_IDENTIFIERS_IGNORE_CASE = FALSE", fetch=False)
508+
509+
show_sql = (
510+
f"show dynamic tables like '{relation.identifier}' "
511+
f"in schema {relation.database}.{relation.schema}"
512+
)
513+
show_res = self.execute(show_sql)
514+
if show_res:
515+
query_id = show_res[0].query_id
516+
scan_sql = (
517+
"select "
518+
' "name", "schema_name", "database_name", "text", "target_lag", "warehouse", '
519+
' "refresh_mode" '
520+
f"from TABLE(RESULT_SCAN('{query_id}'))"
521+
)
522+
res, dt_table = self.execute(scan_sql, fetch=True)
523+
if res.code != "SUCCESS":
524+
raise DbtRuntimeError(
525+
f"Could not get dynamic query metadata: {scan_sql} failed"
526+
)
527+
return {"dynamic_table": dt_table}
528+
529+
return {"dynamic_table": None}
530+
finally:
531+
if original_val is None:
532+
self.execute("ALTER SESSION UNSET QUOTED_IDENTIFIERS_IGNORE_CASE", fetch=False)
533+
else:
534+
bool_str = "TRUE" if original_val.strip().lower() == "true" else "FALSE"
535+
self.execute(
536+
f"ALTER SESSION SET QUOTED_IDENTIFIERS_IGNORE_CASE = {bool_str}", fetch=False
537+
)
Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,4 @@
11
{% macro snowflake__describe_dynamic_table(relation) %}
2-
{#-
3-
Get all relevant metadata about a dynamic table
4-
5-
Args:
6-
- relation: SnowflakeRelation - the relation to describe
7-
Returns:
8-
A dictionary with one or two entries depending on whether iceberg is enabled:
9-
- dynamic_table: the metadata associated with an info schema dynamic table
10-
-#}
11-
{%- set _dynamic_table_sql -%}
12-
alter session set quoted_identifiers_ignore_case = false;
13-
show dynamic tables
14-
like '{{ relation.identifier }}'
15-
in schema {{ relation.database }}.{{ relation.schema }}
16-
;
17-
select
18-
"name",
19-
"schema_name",
20-
"database_name",
21-
"text",
22-
"target_lag",
23-
"warehouse",
24-
"refresh_mode"
25-
from table(result_scan(last_query_id()))
26-
;
27-
{%- endset -%}
28-
29-
{%- set results = {'dynamic_table': run_query(_dynamic_table_sql)} -%}
30-
31-
alter session unset quoted_identifiers_ignore_case;
32-
2+
{%- set results = adapter.describe_dynamic_table(relation) -%}
333
{%- do return(results) -%}
34-
354
{% endmacro %}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import pytest
2+
from unittest.mock import MagicMock
3+
4+
5+
def _fake_relation(project, name="DUMMY_DT"):
6+
"""Return a bare-bones object with the three FQN fields."""
7+
rel = MagicMock()
8+
rel.identifier = name
9+
rel.database = project.database
10+
rel.schema = project.test_schema
11+
return rel
12+
13+
14+
def _get_qiic_value(adapter):
15+
"""
16+
Snowflake always yields one row; default is 'false'.
17+
"""
18+
_, tbl = adapter.execute(
19+
"SHOW PARAMETERS LIKE 'QUOTED_IDENTIFIERS_IGNORE_CASE' IN SESSION",
20+
fetch=True,
21+
)
22+
# tbl.rows[0] is guaranteed by SHOW … IN SESSION
23+
val = tbl.rows[0][1]
24+
return val.strip().lower() if val else None # e.g. 'true' or 'false'
25+
26+
27+
class TestQuotedIdentifiersFlag:
28+
"""
29+
Ensure describe_dynamic_table() restores the session parameter
30+
QUOTED_IDENTIFIERS_IGNORE_CASE to its original value.
31+
"""
32+
33+
@pytest.mark.parametrize("initial_setting", ["true", "false"])
34+
def test_restores_parameter(self, project, initial_setting):
35+
adapter = project.adapter
36+
37+
adapter.execute(
38+
f"ALTER SESSION SET QUOTED_IDENTIFIERS_IGNORE_CASE = {initial_setting.upper()}",
39+
fetch=False,
40+
)
41+
before = _get_qiic_value(adapter)
42+
assert before == initial_setting
43+
44+
relation = _fake_relation(project)
45+
adapter.describe_dynamic_table(relation)
46+
47+
after = _get_qiic_value(adapter)
48+
assert after == before, f"Parameter not restored: before={before!r}, after={after!r}"
49+
50+
51+
class TestFlagRestoredOnSqlError:
52+
"""
53+
Simulate adapter.execute raising an exception *after* the flag
54+
has been flipped, and assert the flag is still reset.
55+
"""
56+
57+
@pytest.mark.parametrize("initial_setting", ["true", "false"])
58+
def test_restored_on_exception(self, project, monkeypatch, initial_setting):
59+
adapter = project.adapter
60+
61+
# initial flag state
62+
adapter.execute(
63+
f"ALTER SESSION SET QUOTED_IDENTIFIERS_IGNORE_CASE = {initial_setting.upper()}",
64+
fetch=False,
65+
)
66+
before = _get_qiic_value(adapter)
67+
68+
# monkey-patch execute so the 5th call raises
69+
call_count = {"n": 0}
70+
71+
def flaky_execute(sql, fetch=False):
72+
call_count["n"] += 1
73+
if call_count["n"] == 5:
74+
raise RuntimeError("boom")
75+
return adapter.__class__.execute(adapter, sql, fetch=fetch)
76+
77+
monkeypatch.setattr(adapter, "execute", flaky_execute)
78+
79+
relation = _fake_relation(project)
80+
with pytest.raises(RuntimeError, match="boom"):
81+
adapter.describe_dynamic_table(relation)
82+
83+
after = _get_qiic_value(adapter)
84+
assert after == before

0 commit comments

Comments
 (0)