Skip to content

Commit 5c8b584

Browse files
authored
Merge branch 'main' into fix/databricks-dependencies
2 parents e1fecc4 + 7c1e4e4 commit 5c8b584

3 files changed

Lines changed: 85 additions & 15 deletions

File tree

‎sqlmesh/core/engine_adapter/snowflake.py‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -757,14 +757,32 @@ def close(self) -> t.Any:
757757
def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]:
758758
from sqlmesh.utils.date import to_timestamp
759759

760-
num_tables = len(table_names)
761-
762-
query = "SELECT LAST_ALTERED FROM INFORMATION_SCHEMA.TABLES WHERE"
763-
for i, table_name in enumerate(table_names):
760+
# INFORMATION_SCHEMA only contains the objects of the database it belongs to, so the tables
761+
# are grouped by database and each group is looked up in its own database's INFORMATION_SCHEMA.
762+
# The groups are keyed on the catalog identifier rather than its name, because quoting matters
763+
# in Snowflake: unquoted raw is the database RAW, while quoted "raw" is a different database
764+
catalogs_to_tables: t.Dict[t.Optional[exp.Identifier], t.List[exp.Table]] = {}
765+
for table_name in table_names:
764766
table = exp.to_table(table_name)
765-
query += f"""(TABLE_NAME = '{table.name}' AND TABLE_SCHEMA = '{table.db}' AND TABLE_CATALOG = '{table.catalog}')"""
766-
if i < num_tables - 1:
767-
query += " OR "
767+
catalogs_to_tables.setdefault(table.args.get("catalog"), []).append(table)
768+
769+
results = []
770+
for catalog, tables in catalogs_to_tables.items():
771+
query = (
772+
exp.select("LAST_ALTERED")
773+
.from_(exp.table_("TABLES", db="INFORMATION_SCHEMA", catalog=catalog))
774+
.where(
775+
exp.or_(
776+
*(
777+
exp.and_(
778+
exp.column("TABLE_NAME").eq(exp.Literal.string(table.name)),
779+
exp.column("TABLE_SCHEMA").eq(exp.Literal.string(table.db)),
780+
)
781+
for table in tables
782+
)
783+
)
784+
)
785+
)
786+
results.extend(self.fetchall(query))
768787

769-
result = self.fetchall(query)
770-
return [to_timestamp(row[0]) for row in result]
788+
return [to_timestamp(row[0]) for row in results]

‎tests/core/engine_adapter/test_snowflake.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing as t
2+
from datetime import datetime, timezone
23

34
import pandas as pd # noqa: TID253
45
import pytest
@@ -14,6 +15,7 @@
1415
from sqlmesh.core.model import load_sql_based_model
1516
from sqlmesh.core.model.definition import SqlModel
1617
from sqlmesh.core.node import IntervalUnit
18+
from sqlmesh.utils.date import to_timestamp
1719
from sqlmesh.utils.errors import SQLMeshError
1820
from sqlmesh.utils import optional_import
1921
from tests.core.engine_adapter import to_sql_calls
@@ -73,6 +75,38 @@ def test_get_data_objects_lowercases_columns(
7375
assert data_object.clustering_key == "ID"
7476

7577

78+
def test_get_table_last_modified_ts_multiple_catalogs(
79+
snowflake_mocked_engine_adapter: SnowflakeEngineAdapter,
80+
) -> None:
81+
adapter = snowflake_mocked_engine_adapter
82+
adapter.cursor.fetchall.side_effect = [
83+
[
84+
(datetime(2025, 1, 1, tzinfo=timezone.utc),),
85+
(datetime(2025, 1, 2, tzinfo=timezone.utc),),
86+
],
87+
[(datetime(2025, 1, 3, tzinfo=timezone.utc),)],
88+
]
89+
90+
result = adapter.get_table_last_modified_ts(
91+
[
92+
'"ANALYTICS"."STAGING"."ORDERS"',
93+
'"RAW"."SHOPIFY"."ORDERS"',
94+
'"ANALYTICS"."STAGING"."CUSTOMERS"',
95+
]
96+
)
97+
98+
# INFORMATION_SCHEMA only covers the database it belongs to, so each database is queried separately
99+
assert to_sql_calls(adapter) == [
100+
"""SELECT LAST_ALTERED FROM "ANALYTICS".INFORMATION_SCHEMA.TABLES WHERE (TABLE_NAME = 'ORDERS' AND TABLE_SCHEMA = 'STAGING') OR (TABLE_NAME = 'CUSTOMERS' AND TABLE_SCHEMA = 'STAGING')""",
101+
"""SELECT LAST_ALTERED FROM "RAW".INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ORDERS' AND TABLE_SCHEMA = 'SHOPIFY'""",
102+
]
103+
assert result == [
104+
to_timestamp("2025-01-01"),
105+
to_timestamp("2025-01-02"),
106+
to_timestamp("2025-01-03"),
107+
]
108+
109+
76110
@pytest.mark.parametrize(
77111
"current_warehouse, current_warehouse_exp, configured_warehouse, configured_warehouse_exp, should_change",
78112
[
@@ -171,6 +205,29 @@ def test_session(
171205
assert to_sql_calls(adapter) == expected_exception_calls
172206

173207

208+
def test_get_table_last_modified_ts_quoted_and_unquoted_catalogs(
209+
snowflake_mocked_engine_adapter: SnowflakeEngineAdapter,
210+
) -> None:
211+
# unquoted raw resolves to the database RAW, quoted "raw" is a different database,
212+
# so the two must be looked up in two different information schemas
213+
adapter = snowflake_mocked_engine_adapter
214+
adapter.cursor.fetchall.side_effect = [
215+
[(datetime(2025, 1, 1, tzinfo=timezone.utc),)],
216+
[(datetime(2025, 1, 2, tzinfo=timezone.utc),)],
217+
]
218+
219+
result = adapter.get_table_last_modified_ts(['"raw".shopify.orders', "raw.shopify.customers"])
220+
221+
assert result == [
222+
to_timestamp(datetime(2025, 1, 1, tzinfo=timezone.utc)),
223+
to_timestamp(datetime(2025, 1, 2, tzinfo=timezone.utc)),
224+
]
225+
assert to_sql_calls(adapter) == [
226+
"""SELECT LAST_ALTERED FROM "raw".INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'orders' AND TABLE_SCHEMA = 'shopify'""",
227+
"""SELECT LAST_ALTERED FROM raw.INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'customers' AND TABLE_SCHEMA = 'shopify'""",
228+
]
229+
230+
174231
def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture):
175232
adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter)
176233

‎tests/core/test_context.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,19 +2609,14 @@ def test_requirements(copy_to_temp_path: t.Callable):
26092609
"dev", no_prompts=True, skip_tests=True, skip_backfill=True, auto_apply=True
26102610
).environment
26112611
requirements = {"ipywidgets", "numpy", "pandas", "test_package"}
2612-
if IS_WINDOWS:
2613-
requirements.add("pendulum")
26142612
assert environment.requirements["pandas"] == "2.2.2"
26152613
assert set(environment.requirements) == requirements
26162614

26172615
context._requirements = {"numpy": "2.1.2", "pandas": "2.2.1"}
26182616
context._excluded_requirements = {"ipywidgets", "ruamel.yaml", "ruamel.yaml.clib"}
26192617
diff = context.plan_builder("dev", skip_tests=True, skip_backfill=True).build().context_diff
26202618
assert set(diff.previous_requirements) == requirements
2621-
reqs = {"numpy", "pandas"}
2622-
if IS_WINDOWS:
2623-
reqs.add("pendulum")
2624-
assert set(diff.requirements) == reqs
2619+
assert set(diff.requirements) == {"numpy", "pandas"}
26252620

26262621

26272622
def test_deactivate_automatic_requirement_inference(copy_to_temp_path: t.Callable):

0 commit comments

Comments
 (0)