Skip to content

Commit 7b092ea

Browse files
committed
fix(snowflake): look up last modified timestamps in each table's database
SnowflakeEngineAdapter.get_table_last_modified_ts queried the unqualified INFORMATION_SCHEMA.TABLES view, which only contains objects from the connection's current database. Tables in any other database were never found, so the freshness signal raised MissingSourceError for models that read external tables from another database. Group the tables by database and query each database's own INFORMATION_SCHEMA.TABLES. The query is now built with sqlglot instead of string formatting, so table and schema names are escaped as literals. Signed-off-by: Dhruv Gupta <todhruvg@gmail.com>
1 parent d69c262 commit 7b092ea

2 files changed

Lines changed: 63 additions & 9 deletions

File tree

‎sqlmesh/core/engine_adapter/snowflake.py‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -757,14 +757,34 @@ 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+
catalogs_to_tables: t.Dict[str, t.List[exp.Table]] = {}
763+
for table_name in table_names:
764764
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 "
765+
catalogs_to_tables.setdefault(table.catalog, []).append(table)
766+
767+
results = []
768+
for tables in catalogs_to_tables.values():
769+
query = (
770+
exp.select("LAST_ALTERED")
771+
.from_(
772+
exp.table_(
773+
"TABLES", db="INFORMATION_SCHEMA", catalog=tables[0].args.get("catalog")
774+
)
775+
)
776+
.where(
777+
exp.or_(
778+
*(
779+
exp.and_(
780+
exp.column("TABLE_NAME").eq(exp.Literal.string(table.name)),
781+
exp.column("TABLE_SCHEMA").eq(exp.Literal.string(table.db)),
782+
)
783+
for table in tables
784+
)
785+
)
786+
)
787+
)
788+
results.extend(self.fetchall(query))
768789

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

‎tests/core/engine_adapter/test_snowflake.py‎

Lines changed: 34 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
[

0 commit comments

Comments
 (0)