Skip to content

Commit c71fa19

Browse files
committed
fix(planner): avoid duplicate planning work
1 parent 0807069 commit c71fa19

16 files changed

Lines changed: 625 additions & 87 deletions

File tree

‎src/sqlbuild/compiler/planner/_helpers/changes/detect.py‎

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,13 @@ def detect_changes(
6060
) -> PlannerChangeResults:
6161
"""Detect selected model and function changes."""
6262

63-
model_changes: dict[str, ChangeDetectionResult] = {}
64-
function_local_hashes: dict[str, str] = build_function_local_hashes(
65-
functions=project.functions,
63+
model_changes: dict[str, ChangeDetectionResult] = detect_model_changes_in_scope(
64+
project=project,
65+
scope=scope,
66+
snapshot=snapshot,
67+
expected_version_hashes=expected_version_hashes,
68+
expected_metadata_jsons=expected_metadata_jsons,
6669
)
67-
hook_version_hashes: dict[str, str] = {
68-
name: identity.version_hash
69-
for name, identity in build_hook_identities(project.hook_functions).items()
70-
}
71-
key: CompiledObjectKey
72-
for key in scope.execution_order:
73-
if key not in scope.selected_keys or key.resource_type != CompiledResourceType.MODEL:
74-
continue
75-
model: CompiledModel | None = scope.models_by_name.get(key.name)
76-
if model is None:
77-
continue
78-
model_changes[model.name] = detect_model_changes(
79-
model=model,
80-
snapshot=snapshot,
81-
sql_analysis_enabled=project.settings.sql_analysis,
82-
query_change_tracking=project.settings.query_change_tracking,
83-
full_refresh=False,
84-
function_local_hashes=function_local_hashes,
85-
hook_version_hashes=hook_version_hashes,
86-
expected_version_hash=(expected_version_hashes or {}).get(model.name),
87-
expected_metadata_json=(expected_metadata_jsons or {}).get(model.name),
88-
)
8970

9071
function_changes: dict[str, FunctionChangeResult] = {}
9172
function: CompiledFunction
@@ -116,6 +97,51 @@ def detect_changes(
11697
return PlannerChangeResults(models=model_changes, functions=function_changes)
11798

11899

100+
def detect_model_changes_in_scope(
101+
*,
102+
project: CompiledProject,
103+
scope: PlannerScope,
104+
snapshot: WarehouseSnapshot,
105+
query_change_tracking: bool | None = None,
106+
expected_version_hashes: dict[str, str] | None = None,
107+
expected_metadata_jsons: dict[str, str] | None = None,
108+
) -> dict[str, ChangeDetectionResult]:
109+
"""Detect changes for selected models without inspecting functions."""
110+
111+
model_changes: dict[str, ChangeDetectionResult] = {}
112+
effective_query_change_tracking: bool = (
113+
project.settings.query_change_tracking
114+
if query_change_tracking is None
115+
else query_change_tracking
116+
)
117+
function_local_hashes: dict[str, str] = build_function_local_hashes(
118+
functions=project.functions,
119+
)
120+
hook_version_hashes: dict[str, str] = {
121+
name: identity.version_hash
122+
for name, identity in build_hook_identities(project.hook_functions).items()
123+
}
124+
key: CompiledObjectKey
125+
for key in scope.execution_order:
126+
if key not in scope.selected_keys or key.resource_type != CompiledResourceType.MODEL:
127+
continue
128+
model: CompiledModel | None = scope.models_by_name.get(key.name)
129+
if model is None:
130+
continue
131+
model_changes[model.name] = detect_model_changes(
132+
model=model,
133+
snapshot=snapshot,
134+
sql_analysis_enabled=project.settings.sql_analysis,
135+
query_change_tracking=effective_query_change_tracking,
136+
full_refresh=False,
137+
function_local_hashes=function_local_hashes,
138+
hook_version_hashes=hook_version_hashes,
139+
expected_version_hash=(expected_version_hashes or {}).get(model.name),
140+
expected_metadata_json=(expected_metadata_jsons or {}).get(model.name),
141+
)
142+
return model_changes
143+
144+
119145
def detect_model_changes(
120146
*,
121147
model: CompiledModel,

‎src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,19 +198,27 @@ def _fixture_keys_requiring_analysis(
198198
return frozenset(required)
199199

200200

201-
def build_relation_fixture_context(*, project: CompiledProject) -> RelationFixturePlanningContext:
201+
def build_relation_fixture_context(
202+
*,
203+
project: CompiledProject,
204+
models_by_name: dict[str, CompiledModel] | None = None,
205+
) -> RelationFixturePlanningContext:
202206
"""Build immutable project-wide fixture metadata once per planning invocation."""
203207

204-
models_by_name: dict[str, CompiledModel] = {model.name: model for model in project.models}
208+
model_map: dict[str, CompiledModel] = (
209+
{model.name: model for model in project.models}
210+
if models_by_name is None
211+
else models_by_name
212+
)
205213
relations: dict[FixtureKey, FixtureRelationMetadata] = _fixture_relation_metadata(
206214
project=project,
207-
model_map=models_by_name,
215+
model_map=model_map,
208216
)
209217
authoritative_columns: dict[FixtureKey, frozenset[str]] = _authoritative_fixture_columns(
210218
relations=relations
211219
)
212220
return RelationFixturePlanningContext(
213-
models_by_name=models_by_name,
221+
models_by_name=model_map,
214222
relations=relations,
215223
authoritative_columns=authoritative_columns,
216224
expected_types=_base_expected_fixture_types(relations=relations),

‎src/sqlbuild/compiler/planner/_helpers/output/plan_output.py‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
scope_overlaps,
3434
)
3535
from sqlbuild.compiler.planner._helpers.resolve.resolve import resolve_function_sql
36-
from sqlbuild.compiler.planner._helpers.sql_tests.assembly import plan_test
36+
from sqlbuild.compiler.planner._helpers.sql_tests.assembly import (
37+
build_sql_test_planning_context,
38+
plan_test,
39+
)
3740
from sqlbuild.compiler.planner.exceptions import (
3841
PlannerInputError,
3942
SqlTestFixtureValidationError,
@@ -56,6 +59,7 @@
5659
SeedPlanEntry,
5760
SourceLoadPlanEntry,
5861
SqlTestPlanEntry,
62+
SqlTestPlanningContext,
5963
WarehouseSnapshot,
6064
)
6165
from sqlbuild.compiler.planner.types import PlanReason
@@ -445,15 +449,29 @@ def build_selected_test_entries(
445449
entries: list[SqlTestPlanEntry] = []
446450
warnings: list[PlanWarning] = []
447451
fixture_diagnostics: list[str] = []
448-
fixture_planning_context: RelationFixturePlanningContext | None = None
452+
selected_tests: list[CompiledSqlTest] = []
449453
sql_test: CompiledSqlTest
450454
for sql_test in project.sql_tests:
451455
if not scope_overlaps(scope_deps=sql_test.scope_deps, selected_keys=selected_keys):
452456
continue
453457
if case_name is not None and sql_test.case_name != case_name:
454458
continue
455-
if project.settings.sql_analysis and fixture_planning_context is None:
456-
fixture_planning_context = build_relation_fixture_context(project=project)
459+
selected_tests.append(sql_test)
460+
if not selected_tests:
461+
return entries, warnings
462+
test_planning_context: SqlTestPlanningContext = build_sql_test_planning_context(
463+
project=project,
464+
tests=tuple(selected_tests),
465+
)
466+
fixture_planning_context: RelationFixturePlanningContext | None = (
467+
build_relation_fixture_context(
468+
project=project,
469+
models_by_name=test_planning_context.models_by_name,
470+
)
471+
if project.settings.sql_analysis
472+
else None
473+
)
474+
for sql_test in selected_tests:
457475
test_entry: SqlTestPlanEntry
458476
test_warnings: tuple[PlanWarning, ...]
459477
try:
@@ -464,6 +482,7 @@ def build_selected_test_entries(
464482
sql_analysis_enabled=project.settings.sql_analysis,
465483
validate_fixtures=True,
466484
fixture_planning_context=fixture_planning_context,
485+
planning_context=test_planning_context,
467486
)
468487
except SqlTestFixtureValidationError as error:
469488
fixture_diagnostics.extend(error.diagnostics)

‎src/sqlbuild/compiler/planner/_helpers/planning/identities.py‎

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
from dataclasses import replace
66

7-
from sqlbuild.compiler.compile.models import CompiledProject
8-
from sqlbuild.compiler.planner._helpers.changes.detect import detect_changes
7+
from sqlbuild.compiler.compile.models import CompiledObjectKey, CompiledProject
8+
from sqlbuild.compiler.compile.types import CompiledResourceType
9+
from sqlbuild.compiler.planner._helpers.changes.detect import detect_model_changes_in_scope
910
from sqlbuild.compiler.planner._helpers.identity.direct import (
1011
build_direct_model_version_identities,
1112
)
1213
from sqlbuild.compiler.planner.models import (
14+
ChangeDetectionResult,
15+
DirectModelVersionIdentities,
1316
PlannerChangeResults,
1417
PlannerIdentityContext,
1518
PlannerScopeResolution,
@@ -21,21 +24,27 @@ def build_planner_identity_context(
2124
*,
2225
project: CompiledProject,
2326
scopes: PlannerScopeResolution,
27+
include_stale_warning_identities: bool = True,
2428
) -> PlannerIdentityContext:
2529
"""Build expected version identities for the inspection and stale-warning scopes."""
2630

31+
version_identities: DirectModelVersionIdentities = build_direct_model_version_identities(
32+
functions=project.functions,
33+
seeds=project.seeds,
34+
scope=scopes.inspection_scope,
35+
hook_functions=project.hook_functions,
36+
)
2737
return PlannerIdentityContext(
28-
version_identities=build_direct_model_version_identities(
29-
functions=project.functions,
30-
seeds=project.seeds,
31-
scope=scopes.inspection_scope,
32-
hook_functions=project.hook_functions,
33-
),
34-
stale_warning_identities=build_direct_model_version_identities(
35-
functions=project.functions,
36-
seeds=project.seeds,
37-
scope=scopes.stale_warning_scope,
38-
hook_functions=project.hook_functions,
38+
version_identities=version_identities,
39+
stale_warning_identities=(
40+
build_direct_model_version_identities(
41+
functions=project.functions,
42+
seeds=project.seeds,
43+
scope=scopes.stale_warning_scope,
44+
hook_functions=project.hook_functions,
45+
)
46+
if include_stale_warning_identities
47+
else version_identities
3948
),
4049
)
4150

@@ -46,17 +55,64 @@ def detect_stale_warning_changes(
4655
scopes: PlannerScopeResolution,
4756
snapshot: WarehouseSnapshot,
4857
identities: PlannerIdentityContext,
58+
execution_changes: PlannerChangeResults,
4959
) -> PlannerChangeResults:
50-
"""Detect project-wide changes used for stale-out-of-selection warnings."""
60+
"""Detect only stale-warning model changes not already known from execution planning."""
5161

52-
return detect_changes(
62+
reusable_model_names: frozenset[str] = _reusable_execution_model_names(
63+
identities=identities,
64+
execution_changes=execution_changes,
65+
query_change_tracking=project.settings.query_change_tracking,
66+
)
67+
stale_keys: frozenset[CompiledObjectKey] = frozenset(
68+
scopes.stale_warning_scope.all_keys.values()
69+
)
70+
remaining_model_keys: frozenset[CompiledObjectKey] = frozenset(
71+
key
72+
for key in stale_keys
73+
if key.resource_type == CompiledResourceType.MODEL and key.name not in reusable_model_names
74+
)
75+
remaining_model_changes: dict[str, ChangeDetectionResult] = detect_model_changes_in_scope(
5376
project=project,
5477
scope=replace(
5578
scopes.stale_warning_scope,
56-
selected_keys=frozenset(scopes.stale_warning_scope.all_keys.values()),
79+
selected_keys=remaining_model_keys,
5780
),
5881
snapshot=snapshot,
59-
full_refresh=False,
82+
query_change_tracking=True,
6083
expected_version_hashes=identities.stale_warning_identities.model_version_hashes,
6184
expected_metadata_jsons=identities.stale_warning_identities.model_metadata_jsons,
6285
)
86+
return PlannerChangeResults(
87+
models={
88+
**{
89+
name: change
90+
for name, change in execution_changes.models.items()
91+
if name in reusable_model_names
92+
},
93+
**remaining_model_changes,
94+
},
95+
functions={},
96+
)
97+
98+
99+
def _reusable_execution_model_names(
100+
*,
101+
identities: PlannerIdentityContext,
102+
execution_changes: PlannerChangeResults,
103+
query_change_tracking: bool,
104+
) -> frozenset[str]:
105+
if not query_change_tracking:
106+
return frozenset()
107+
execution_identities: DirectModelVersionIdentities = identities.version_identities
108+
stale_identities: DirectModelVersionIdentities = identities.stale_warning_identities
109+
return frozenset(
110+
model_name
111+
for model_name in execution_changes.models
112+
if (
113+
execution_identities.model_version_hashes.get(model_name)
114+
== stale_identities.model_version_hashes.get(model_name)
115+
and execution_identities.model_metadata_jsons.get(model_name)
116+
== stale_identities.model_metadata_jsons.get(model_name)
117+
)
118+
)

‎src/sqlbuild/compiler/planner/_helpers/planning/output_assembly.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def with_plan_warnings(
9393
version_identities=identities.stale_warning_identities,
9494
source_freshness=source_freshness,
9595
include_sources=False,
96+
model_changes_complete=True,
9697
)
9798
)
9899
if stale_out_of_selection_warnings:

‎src/sqlbuild/compiler/planner/_helpers/pruning/selection_staleness.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def build_stale_out_of_selection_warnings(
3434
version_identities: DirectModelVersionIdentities,
3535
source_freshness: DirectSourceFreshnessPlanningResult | None,
3636
include_sources: bool = True,
37+
model_changes_complete: bool = False,
3738
) -> tuple[PlanWarning, ...]:
3839
"""Warn for selected models stale through changed upstreams outside the run set."""
3940

@@ -67,6 +68,7 @@ def build_stale_out_of_selection_warnings(
6768
changes=changes,
6869
snapshot=snapshot,
6970
version_identities=version_identities,
71+
model_changes_complete=model_changes_complete,
7072
),
7173
changed_seed_names=_changed_seed_names(
7274
snapshot=snapshot,
@@ -129,6 +131,7 @@ def _changed_model_names(
129131
changes: PlannerChangeResults,
130132
snapshot: WarehouseSnapshot,
131133
version_identities: DirectModelVersionIdentities,
134+
model_changes_complete: bool,
132135
) -> frozenset[str]:
133136
changed: set[str] = {
134137
model_name
@@ -150,6 +153,7 @@ def _changed_model_names(
150153
changes=changes,
151154
snapshot=snapshot,
152155
version_identities=version_identities,
156+
model_changes_complete=model_changes_complete,
153157
)
154158
)
155159
return frozenset(changed)
@@ -186,16 +190,25 @@ def _model_own_identity_changed(
186190
changes: PlannerChangeResults,
187191
snapshot: WarehouseSnapshot,
188192
version_identities: DirectModelVersionIdentities,
193+
model_changes_complete: bool,
189194
) -> bool:
190195
selected_change: ChangeDetectionResult | None = changes.models.get(model_name)
191-
if selected_change is not None:
192-
if selected_change.change_kind in {
196+
if selected_change is not None and (
197+
model_changes_complete
198+
or selected_change.change_kind
199+
in {
193200
ChangeKind.FIRST_RUN,
194201
ChangeKind.QUERY_CHANGED,
195202
ChangeKind.CONFIG_CHANGED,
196203
ChangeKind.SCHEMA_CHANGED,
197-
}:
198-
return True
204+
}
205+
):
206+
return selected_change.change_kind in {
207+
ChangeKind.FIRST_RUN,
208+
ChangeKind.QUERY_CHANGED,
209+
ChangeKind.CONFIG_CHANGED,
210+
ChangeKind.SCHEMA_CHANGED,
211+
}
199212
model: CompiledModel | None = original_scope.models_by_name.get(model_name)
200213
if model is None:
201214
return False

0 commit comments

Comments
 (0)