Skip to content

Commit d47a7ae

Browse files
perf: selective snapshot hydration (#6016)
Signed-off-by: Andreas Fredhøi <andreas.fredhoi@fresio.no> Co-authored-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com>
1 parent c1cc16b commit d47a7ae

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎sqlmesh/core/context.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,25 @@ def load(
748748
prod = self.state_reader.get_environment(c.PROD)
749749

750750
if prod:
751-
for snapshot in self.state_reader.get_snapshots(prod.snapshots).values():
751+
# Environment snapshot infos already contain the names we need to distinguish
752+
# local nodes from nodes owned by another project. Hydrating local snapshots
753+
# here is wasteful: their payloads are not used, and a large remote state can
754+
# spend most of Context.load() decoding those model object graphs.
755+
#
756+
# Only hydrate names which are absent locally. These can be deleted nodes from
757+
# this project or nodes from another project which must be merged into the
758+
# context. The project field on the hydrated node disambiguates the two cases.
759+
remote_snapshot_infos = []
760+
for snapshot_info in prod.snapshots:
761+
local_store = (
762+
self._standalone_audits if snapshot_info.is_audit else self._models
763+
)
764+
if snapshot_info.name in local_store:
765+
uncached.add(snapshot_info.name)
766+
else:
767+
remote_snapshot_infos.append(snapshot_info)
768+
769+
for snapshot in self.state_reader.get_snapshots(remote_snapshot_infos).values():
752770
if snapshot.node.project in self._projects:
753771
uncached.add(snapshot.name)
754772
else:

‎tests/core/test_context.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import sqlmesh.core.constants
1919
from sqlmesh.cli.project_init import init_example_project
20+
from sqlmesh.core.audit import StandaloneAudit
2021
from sqlmesh.core.console import TerminalConsole
2122
from sqlmesh.core import dialect as d, constants as c
2223
from sqlmesh.core.config import (
@@ -276,6 +277,78 @@ def test_render_seed_model(sushi_context, assert_exp_eq):
276277
)
277278

278279

280+
@pytest.mark.slow
281+
def test_load_only_hydrates_remote_snapshots_missing_locally(sushi_context: Context) -> None:
282+
prod = sushi_context.state_reader.get_environment("prod")
283+
assert prod is not None
284+
sushi_context._projects = {"local_project"}
285+
286+
local_node_names = {*sushi_context.models, *sushi_context.standalone_audits}
287+
expected_remote_names = {
288+
snapshot_info.name
289+
for snapshot_info in prod.snapshots
290+
if snapshot_info.name not in local_node_names
291+
}
292+
assert len(expected_remote_names) < len(prod.snapshots)
293+
294+
with patch.object(
295+
sushi_context.state_reader,
296+
"get_snapshots",
297+
wraps=sushi_context.state_reader.get_snapshots,
298+
) as get_snapshots_mock:
299+
sushi_context.load(update_schemas=False)
300+
301+
load_snapshot_names = {
302+
snapshot_info.name
303+
for call_args in get_snapshots_mock.call_args_list
304+
for snapshot_info in call_args.args[0]
305+
}
306+
assert load_snapshot_names == expected_remote_names
307+
308+
309+
@pytest.mark.slow
310+
def test_load_hydrates_opposite_type_snapshot_name_collision(
311+
sushi_context: Context, make_snapshot: t.Callable
312+
) -> None:
313+
prod = sushi_context.state_reader.get_environment("prod")
314+
assert prod is not None
315+
sushi_context._projects = {"local_project"}
316+
317+
local_model = next(iter(sushi_context.models.values()))
318+
remote_audit_snapshot = make_snapshot(
319+
StandaloneAudit(
320+
name=local_model.fqn,
321+
query=parse_one("SELECT NULL LIMIT 0"),
322+
project="remote_project",
323+
)
324+
)
325+
remote_audit_snapshot.categorize_as(SnapshotChangeCategory.BREAKING)
326+
prod_with_collision = prod.copy(
327+
update={"snapshots_": [*prod.snapshots, remote_audit_snapshot.table_info]}
328+
)
329+
330+
with (
331+
patch.object(
332+
sushi_context.state_reader,
333+
"get_environment",
334+
return_value=prod_with_collision,
335+
),
336+
patch.object(
337+
sushi_context.state_reader,
338+
"get_snapshots",
339+
wraps=sushi_context.state_reader.get_snapshots,
340+
) as get_snapshots_mock,
341+
):
342+
sushi_context.load(update_schemas=False)
343+
344+
requested_snapshot_names = {
345+
snapshot_info.name
346+
for call_args in get_snapshots_mock.call_args_list
347+
for snapshot_info in call_args.args[0]
348+
}
349+
assert remote_audit_snapshot.name in requested_snapshot_names
350+
351+
279352
@pytest.mark.slow
280353
def test_diff(sushi_context: Context, mocker: MockerFixture):
281354
mock_console = mocker.Mock()

0 commit comments

Comments
 (0)