Skip to content

Commit 264f84c

Browse files
authored
fix: qualify Snowflake function discovery by database (#842)
* fix: qualify Snowflake function discovery by database * test: use dataclass cases for function discovery regressions
1 parent c61d1f8 commit 264f84c

5 files changed

Lines changed: 167 additions & 2 deletions

File tree

‎src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1705,7 +1705,9 @@ def list_functions(
17051705
) -> tuple[FunctionInfo, ...]:
17061706
query: str = (
17071707
"SELECT function_name, function_schema, 'function' "
1708-
"FROM information_schema.functions WHERE 1=1"
1708+
"FROM "
1709+
+ self._information_schema_relation(database=database, name="functions")
1710+
+ " WHERE 1=1"
17091711
)
17101712
params: list[str] = []
17111713
if schemas:

‎tests/integration/src/sqlbuild/adapters/snowflake/_test_types.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,10 @@ class SnowflakeCostTestCase:
123123
expected_query_count: int
124124
expected_resource_name: str
125125
expected_minimum_credits: int
126+
127+
128+
@dataclass(frozen=True)
129+
class SnowflakeFunctionDiscoveryTestCase:
130+
description: str
131+
function_name: str
132+
expected_function_count: int
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Real Snowflake function discovery against an explicit catalog."""
2+
3+
from typing import Any
4+
5+
import pytest
6+
7+
from sqlbuild.adapter.contract.models import FunctionInfo
8+
from sqlbuild.adapters.snowflake.classes.snowflake_adapter import SnowflakeAdapter
9+
from tests.integration.src.sqlbuild.adapters.snowflake._test_types import (
10+
SnowflakeFunctionDiscoveryTestCase,
11+
)
12+
from tests.integration.src.sqlbuild.adapters.snowflake.helpers import (
13+
build_snowflake_connection_config,
14+
)
15+
16+
17+
@pytest.mark.parametrize(
18+
"test_case",
19+
[
20+
SnowflakeFunctionDiscoveryTestCase(
21+
description="connection without database finds the explicit target",
22+
function_name="normalize_order",
23+
expected_function_count=1,
24+
)
25+
],
26+
ids=lambda case: case.description,
27+
)
28+
def test_given_connection_without_database_when_discovering_udf_then_finds_explicit_target(
29+
test_case: SnowflakeFunctionDiscoveryTestCase,
30+
adapter: SnowflakeAdapter,
31+
connection: Any,
32+
snowflake_database: str,
33+
snowflake_schema: str,
34+
) -> None:
35+
target: str = ".".join(
36+
adapter.render_identifier(part)
37+
for part in (snowflake_database, snowflake_schema, test_case.function_name)
38+
)
39+
adapter.execute(
40+
connection=connection,
41+
sql=f"CREATE FUNCTION {target}(value VARCHAR) RETURNS VARCHAR AS 'UPPER(value)'",
42+
)
43+
config: dict[str, object] = build_snowflake_connection_config()
44+
config.pop("database", None)
45+
config.pop("schema", None)
46+
discovery_connection: Any = adapter.connect(config)
47+
try:
48+
functions: tuple[FunctionInfo, ...] = adapter.list_functions(
49+
connection=discovery_connection,
50+
database=snowflake_database,
51+
schemas=(snowflake_schema,),
52+
names=(test_case.function_name,),
53+
)
54+
finally:
55+
adapter.close(discovery_connection)
56+
57+
assert len(functions) == test_case.expected_function_count
58+
assert functions[0].name == test_case.function_name
59+
assert functions[0].schema == snowflake_schema.lower()
60+
assert functions[0].database == snowflake_database
61+
62+
63+
if __name__ == "__main__":
64+
pytest.main([__file__, "-vv"])

‎tests/unit/src/sqlbuild/adapters/snowflake/_test_types.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass
22
from datetime import datetime
33

4-
from sqlbuild.adapter.contract.models import SchemaDiffResult
4+
from sqlbuild.adapter.contract.models import FunctionInfo, SchemaDiffResult
55
from sqlbuild.compiler.lineage.types import InferredNullability
66
from sqlbuild.cost.types import CostStatus
77

@@ -204,3 +204,15 @@ class SnowflakeRelationAgeMetadataTestCase:
204204
last_altered: datetime | None
205205
expected_created_at: datetime | None
206206
expected_last_altered_at: datetime | None
207+
208+
209+
@dataclass(frozen=True)
210+
class SnowflakeFunctionDiscoveryTestCase:
211+
description: str
212+
database: str | None
213+
schemas: tuple[str, ...] | None
214+
names: tuple[str, ...] | None
215+
rows: tuple[tuple[object, ...], ...]
216+
expected_relation: str
217+
expected_params: tuple[str, ...]
218+
expected_functions: tuple[FunctionInfo, ...]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Function discovery must address the requested catalog independently of session state."""
2+
3+
from typing import Any, cast
4+
5+
import pytest
6+
7+
from sqlbuild.adapter.contract.models import FunctionInfo
8+
from sqlbuild.adapters.snowflake.classes.snowflake_adapter import SnowflakeAdapter
9+
from tests.unit.src.sqlbuild.adapters.snowflake._test_types import (
10+
SnowflakeFunctionDiscoveryTestCase,
11+
)
12+
from tests.unit.src.sqlbuild.adapters.snowflake.helpers import (
13+
FakeSnowflakeMetadataConnection,
14+
FakeSnowflakeMetadataCursor,
15+
)
16+
17+
18+
@pytest.mark.parametrize(
19+
"test_case",
20+
[
21+
SnowflakeFunctionDiscoveryTestCase(
22+
description="explicit database qualifies the information schema",
23+
database="analytics",
24+
schemas=("public",),
25+
names=("normalize_order",),
26+
rows=(("NORMALIZE_ORDER", "PUBLIC", "function"),),
27+
expected_relation='FROM "ANALYTICS".information_schema.functions',
28+
expected_params=("PUBLIC", "NORMALIZE_ORDER", "analytics"),
29+
expected_functions=(
30+
FunctionInfo(
31+
database="analytics",
32+
schema="public",
33+
name="normalize_order",
34+
function_type="function",
35+
),
36+
),
37+
),
38+
SnowflakeFunctionDiscoveryTestCase(
39+
description="database containing a quote is escaped",
40+
database='order"catalog',
41+
schemas=None,
42+
names=None,
43+
rows=(),
44+
expected_relation='FROM "ORDER""CATALOG".information_schema.functions',
45+
expected_params=('order"catalog',),
46+
expected_functions=(),
47+
),
48+
SnowflakeFunctionDiscoveryTestCase(
49+
description="omitted database uses the session catalog",
50+
database=None,
51+
schemas=None,
52+
names=None,
53+
rows=(),
54+
expected_relation="FROM information_schema.functions",
55+
expected_params=(),
56+
expected_functions=(),
57+
),
58+
],
59+
ids=lambda case: case.description,
60+
)
61+
def test_given_database_when_listing_functions_then_queries_matching_catalog(
62+
test_case: SnowflakeFunctionDiscoveryTestCase,
63+
) -> None:
64+
cursor: FakeSnowflakeMetadataCursor = FakeSnowflakeMetadataCursor(rows=list(test_case.rows))
65+
66+
result: tuple[FunctionInfo, ...] = SnowflakeAdapter().list_functions(
67+
connection=cast(Any, FakeSnowflakeMetadataConnection(cursor)),
68+
database=test_case.database,
69+
schemas=test_case.schemas,
70+
names=test_case.names,
71+
)
72+
73+
assert test_case.expected_relation in str(cursor.executed_sql)
74+
assert cursor.executed_params == test_case.expected_params
75+
assert result == test_case.expected_functions
76+
assert cursor.closed
77+
78+
79+
if __name__ == "__main__":
80+
pytest.main([__file__, "-vv"])

0 commit comments

Comments
 (0)