Skip to content

Commit 4039290

Browse files
committed
fix: reduce large-project planning work
1 parent 4b4c078 commit 4039290

11 files changed

Lines changed: 410 additions & 29 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
from sqlbuild.sql_values.models import SqlValue
9696

9797
_EXACT_COLUMN_INSPECTION_LIMIT: int = 32
98-
_BULK_COLUMN_RELATION_CHUNK_SIZE: int = 200
98+
_BULK_COLUMN_RELATION_CHUNK_SIZE: int = 2_000
9999

100100

101101
class SnowflakeAdapter(MicrobatchMixin, UnkeyedDiffMixin, BaseAdapter):

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

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
FixtureRelationMetadata,
3232
RelationFixtureCompletion,
3333
RelationFixtureDiagnostic,
34+
RelationFixturePlanningContext,
3435
)
3536
from sqlbuild.compiler.planner.types import ContractPolicy, FixtureGroups, FixtureKey
3637
from sqlbuild.compiler.references.constants import REF_PATTERN, SEED_PATTERN, SOURCE_PATTERN
@@ -47,10 +48,14 @@ def build_relation_fixture_completion(
4748
adapter: BaseAdapter,
4849
ordered_model_names: tuple[str, ...],
4950
fixture_groups: FixtureGroups,
51+
planning_context: RelationFixturePlanningContext | None = None,
5052
) -> RelationFixtureCompletion:
5153
"""Complete required omitted columns with typed nulls unless explicitly non-nullable."""
5254

53-
model_map: dict[str, CompiledModel] = {model.name: model for model in project.models}
55+
context: RelationFixturePlanningContext = planning_context or build_relation_fixture_context(
56+
project=project
57+
)
58+
model_map: dict[str, CompiledModel] = context.models_by_name
5459
fixture_sql_by_key: dict[FixtureKey, str] = {}
5560
inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]] = {}
5661
for resource_type, fixtures in fixture_groups:
@@ -64,21 +69,23 @@ def build_relation_fixture_completion(
6469
if inferred is not None:
6570
inferred_by_fixture[key] = inferred
6671

67-
relations: dict[FixtureKey, FixtureRelationMetadata] = _fixture_relation_metadata(
68-
project=project,
69-
model_map=model_map,
72+
relations: dict[FixtureKey, FixtureRelationMetadata] = context.relations
73+
fixture_keys_requiring_analysis: frozenset[FixtureKey] = _fixture_keys_requiring_analysis(
74+
inferred_by_fixture=inferred_by_fixture,
75+
authoritative_columns=context.authoritative_columns,
7076
)
7177
required: dict[FixtureKey, dict[str, set[str]]]
7278
star_fixture_keys: frozenset[FixtureKey]
7379
required, star_fixture_keys = _required_fixture_columns(
7480
ordered_model_names=ordered_model_names,
7581
model_map=model_map,
76-
fixture_keys=frozenset(fixture_sql_by_key),
82+
fixture_keys=fixture_keys_requiring_analysis,
7783
relations=relations,
84+
authoritative_columns=context.authoritative_columns,
7885
adapter=adapter,
7986
)
8087
expected_types: dict[FixtureKey, dict[str, str]] = _expected_fixture_types(
81-
relations=relations,
88+
base_expected_types=context.expected_types,
8289
ordered_model_names=ordered_model_names,
8390
model_map=model_map,
8491
)
@@ -177,23 +184,61 @@ def build_relation_fixture_completion(
177184
)
178185

179186

187+
def _fixture_keys_requiring_analysis(
188+
*,
189+
inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]],
190+
authoritative_columns: dict[FixtureKey, frozenset[str]],
191+
) -> frozenset[FixtureKey]:
192+
required: set[FixtureKey] = set()
193+
for key, inferred in inferred_by_fixture.items():
194+
expected_names: frozenset[str] | None = authoritative_columns.get(key)
195+
supplied_names: frozenset[str] = frozenset(column.name.casefold() for column in inferred)
196+
if expected_names is None or not expected_names.issubset(supplied_names):
197+
required.add(key)
198+
return frozenset(required)
199+
200+
201+
def build_relation_fixture_context(*, project: CompiledProject) -> RelationFixturePlanningContext:
202+
"""Build immutable project-wide fixture metadata once per planning invocation."""
203+
204+
models_by_name: dict[str, CompiledModel] = {model.name: model for model in project.models}
205+
relations: dict[FixtureKey, FixtureRelationMetadata] = _fixture_relation_metadata(
206+
project=project,
207+
model_map=models_by_name,
208+
)
209+
authoritative_columns: dict[FixtureKey, frozenset[str]] = _authoritative_fixture_columns(
210+
relations=relations
211+
)
212+
return RelationFixturePlanningContext(
213+
models_by_name=models_by_name,
214+
relations=relations,
215+
authoritative_columns=authoritative_columns,
216+
expected_types=_base_expected_fixture_types(relations=relations),
217+
)
218+
219+
220+
def _authoritative_fixture_columns(
221+
*, relations: dict[FixtureKey, FixtureRelationMetadata]
222+
) -> dict[FixtureKey, frozenset[str]]:
223+
authoritative: dict[FixtureKey, frozenset[str]] = {}
224+
for key, relation in relations.items():
225+
if relation.authoritative_names:
226+
authoritative[key] = frozenset(column.name.casefold() for column in relation.columns)
227+
return authoritative
228+
229+
180230
def _required_fixture_columns(
181231
*,
182232
ordered_model_names: tuple[str, ...],
183233
model_map: dict[str, CompiledModel],
184234
fixture_keys: frozenset[FixtureKey],
185235
relations: dict[FixtureKey, FixtureRelationMetadata],
236+
authoritative_columns: dict[FixtureKey, frozenset[str]],
186237
adapter: BaseAdapter,
187238
) -> tuple[dict[FixtureKey, dict[str, set[str]]], frozenset[FixtureKey]]:
188239
required: dict[FixtureKey, dict[str, set[str]]] = {}
189240
star_fixture_keys: set[FixtureKey] = set()
190-
authoritative_columns: dict[FixtureKey, frozenset[str]] = {}
191-
for key, relation in relations.items():
192-
if not relation.authoritative_names:
193-
continue
194-
authoritative_columns[key] = frozenset(
195-
column.name.casefold() for column in relation.columns
196-
)
241+
dialect: str | None = adapter.expression_inference_profile().sql_analysis_dialect
197242
for model_name in ordered_model_names:
198243
model: CompiledModel | None = model_map.get(model_name)
199244
if model is None:
@@ -203,7 +248,7 @@ def _required_fixture_columns(
203248
references=model.references,
204249
fast_lineage=model.fast_lineage_columns or (),
205250
fixture_keys=fixture_keys,
206-
dialect=adapter.expression_inference_profile().sql_analysis_dialect,
251+
dialect=dialect,
207252
):
208253
key: FixtureKey = (
209254
CompiledResourceType(upstream.resource_type),
@@ -451,22 +496,18 @@ def _model_fixture_metadata(*, model: CompiledModel) -> FixtureRelationMetadata:
451496

452497
def _expected_fixture_types(
453498
*,
454-
relations: dict[FixtureKey, FixtureRelationMetadata],
499+
base_expected_types: dict[FixtureKey, dict[str, str]],
455500
ordered_model_names: tuple[str, ...],
456501
model_map: dict[str, CompiledModel],
457502
) -> dict[FixtureKey, dict[str, str]]:
458-
expected_types: dict[FixtureKey, dict[str, str]] = {}
459-
for key, relation in relations.items():
460-
column_types: dict[str, str] = {}
461-
for column in relation.columns:
462-
if column.type is not None:
463-
column_types[column.name.casefold()] = column.type
464-
expected_types[key] = column_types
503+
expected_types: dict[FixtureKey, dict[str, str]] = dict(base_expected_types)
465504
for model_name in ordered_model_names:
466505
model: CompiledModel | None = model_map.get(model_name)
467506
if model is None:
468507
continue
469-
model_types: dict[str, str] = expected_types[(CompiledResourceType.MODEL, model_name)]
508+
model_key: FixtureKey = (CompiledResourceType.MODEL, model_name)
509+
model_types: dict[str, str] = dict(expected_types[model_key])
510+
expected_types[model_key] = model_types
470511
for lineage_column in model.fast_lineage_columns or ():
471512
if lineage_column.transform_kind != ColumnTransformKind.DIRECT:
472513
continue
@@ -484,6 +525,19 @@ def _expected_fixture_types(
484525
return expected_types
485526

486527

528+
def _base_expected_fixture_types(
529+
*, relations: dict[FixtureKey, FixtureRelationMetadata]
530+
) -> dict[FixtureKey, dict[str, str]]:
531+
expected_types: dict[FixtureKey, dict[str, str]] = {}
532+
for key, relation in relations.items():
533+
column_types: dict[str, str] = {}
534+
for column in relation.columns:
535+
if column.type is not None:
536+
column_types[column.name.casefold()] = column.type
537+
expected_types[key] = column_types
538+
return expected_types
539+
540+
487541
def _unknown_fixture_column_diagnostics(
488542
*,
489543
inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]],

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
PythonHookEntry,
2121
)
2222
from sqlbuild.compiler.fingerprints.models import Fingerprint
23+
from sqlbuild.compiler.planner._helpers.fixtures.completion import (
24+
build_relation_fixture_context,
25+
)
2326
from sqlbuild.compiler.planner._helpers.graph.loader_dag import upstream_loader_dependency_names
2427
from sqlbuild.compiler.planner._helpers.graph.source_load_nodes import build_source_load_entries
2528
from sqlbuild.compiler.planner._helpers.output.audit_entry import plan_audit
@@ -49,6 +52,7 @@
4952
PlanOutputExtras,
5053
PlanProviderUsage,
5154
PlanWarning,
55+
RelationFixturePlanningContext,
5256
SeedPlanEntry,
5357
SourceLoadPlanEntry,
5458
SqlTestPlanEntry,
@@ -441,12 +445,15 @@ def build_selected_test_entries(
441445
entries: list[SqlTestPlanEntry] = []
442446
warnings: list[PlanWarning] = []
443447
fixture_diagnostics: list[str] = []
448+
fixture_planning_context: RelationFixturePlanningContext | None = None
444449
sql_test: CompiledSqlTest
445450
for sql_test in project.sql_tests:
446451
if not scope_overlaps(scope_deps=sql_test.scope_deps, selected_keys=selected_keys):
447452
continue
448453
if case_name is not None and sql_test.case_name != case_name:
449454
continue
455+
if project.settings.sql_analysis and fixture_planning_context is None:
456+
fixture_planning_context = build_relation_fixture_context(project=project)
450457
test_entry: SqlTestPlanEntry
451458
test_warnings: tuple[PlanWarning, ...]
452459
try:
@@ -456,6 +463,7 @@ def build_selected_test_entries(
456463
adapter=adapter,
457464
sql_analysis_enabled=project.settings.sql_analysis,
458465
validate_fixtures=True,
466+
fixture_planning_context=fixture_planning_context,
459467
)
460468
except SqlTestFixtureValidationError as error:
461469
fixture_diagnostics.extend(error.diagnostics)

‎src/sqlbuild/compiler/planner/_helpers/sql_tests/assembly.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from sqlbuild.compiler.planner.models import (
4747
ChainStep,
4848
PlanWarning,
49+
RelationFixturePlanningContext,
4950
SqlAnalysisResolvedTestSql,
5051
SqlTestAssertionStep,
5152
SqlTestPlanEntry,
@@ -133,6 +134,7 @@ def plan_test(
133134
adapter: BaseAdapter,
134135
sql_analysis_enabled: bool = False,
135136
validate_fixtures: bool = False,
137+
fixture_planning_context: RelationFixturePlanningContext | None = None,
136138
) -> tuple[SqlTestPlanEntry, tuple[PlanWarning, ...]]:
137139
"""Build a test plan entry with chained resolution."""
138140

@@ -193,6 +195,7 @@ def plan_test(
193195
mock_sources=mock_sources,
194196
mock_seeds=mock_seeds,
195197
expected_outputs=expected_map,
198+
planning_context=fixture_planning_context,
196199
)
197200

198201
warnings: list[PlanWarning] = []

‎src/sqlbuild/compiler/planner/_helpers/sql_tests/fixture_validation.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from sqlbuild.compiler.planner.models import (
2020
RelationFixtureCompletion,
2121
RelationFixtureDiagnostic,
22+
RelationFixturePlanningContext,
2223
)
2324

2425
_COLLECTION_TYPES: frozenset[str] = frozenset(
@@ -36,6 +37,7 @@ def build_validated_test_fixtures(
3637
mock_sources: dict[str, str],
3738
mock_seeds: dict[str, str],
3839
expected_outputs: dict[str, str],
40+
planning_context: RelationFixturePlanningContext | None = None,
3941
) -> tuple[dict[str, str], dict[str, str], dict[str, str]]:
4042
"""Report all statically provable missing columns and collection-type mismatches."""
4143

@@ -49,6 +51,7 @@ def build_validated_test_fixtures(
4951
adapter=adapter,
5052
ordered_model_names=ordered_model_names,
5153
fixture_groups=fixture_groups,
54+
planning_context=planning_context,
5255
)
5356
errors: list[str] = [
5457
_located_diagnostic(test=test, diagnostic=diagnostic)

‎src/sqlbuild/compiler/planner/models.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ class FixtureRelationMetadata:
122122
authoritative_names: bool
123123

124124

125+
@dataclass(frozen=True)
126+
class RelationFixturePlanningContext:
127+
"""Project-wide fixture metadata shared across test and scenario planning."""
128+
129+
models_by_name: dict[str, CompiledModel]
130+
relations: dict[FixtureKey, FixtureRelationMetadata]
131+
authoritative_columns: dict[FixtureKey, frozenset[str]]
132+
expected_types: dict[FixtureKey, dict[str, str]]
133+
134+
125135
@dataclass(frozen=True)
126136
class GraphNodeKey:
127137
"""Neutral graph key matching fingerprint identity fields."""

‎tests/integration/src/sqlbuild/compiler/planner/_helpers/_test_types.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
from sqlbuild.spec.contracts.models import SourceEntry
88

99

10+
@dataclass(frozen=True)
11+
class FixturePlanningIntegrationTestCase:
12+
description: str
13+
expected_sql_fragment: str
14+
15+
1016
@dataclass(frozen=True)
1117
class GatherWatermarkTypeTestCase:
1218
description: str
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Callable
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from sqlbuild.adapters.duckdb.classes.duckdb_adapter import DuckDbAdapter
9+
from sqlbuild.compiler.compile.models import CompiledProject
10+
from sqlbuild.compiler.discovery.main.discover import discover_project_inputs
11+
from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs
12+
from sqlbuild.compiler.pipeline.main.project import compile_project
13+
from sqlbuild.compiler.planner._helpers.output.plan_output import build_selected_test_entries
14+
from sqlbuild.compiler.planner.models import SqlTestPlanEntry
15+
from tests.integration.src.sqlbuild.compiler.planner._helpers._test_types import (
16+
FixturePlanningIntegrationTestCase,
17+
)
18+
19+
20+
@pytest.mark.parametrize(
21+
"test_case",
22+
(
23+
FixturePlanningIntegrationTestCase(
24+
description="partial source fixture",
25+
expected_sql_fragment='CAST(NULL AS VARCHAR) AS "status"',
26+
),
27+
),
28+
ids=lambda case: case.description,
29+
)
30+
def test_given_partial_fixture_when_compiling_and_planning_then_completes_required_columns(
31+
test_case: FixturePlanningIntegrationTestCase,
32+
tmp_path: Path,
33+
write_repo_files: Callable[[Path, dict[str, str]], None],
34+
) -> None:
35+
write_repo_files(
36+
tmp_path,
37+
{
38+
"sqlbuild_project.toml": (
39+
'name = "fixture_cache_demo"\n'
40+
'adapter = "duckdb"\n\n'
41+
"[connection]\n"
42+
'database = ":memory:"\n'
43+
),
44+
"models/orders.sql": (
45+
'MODEL ();\n\nSELECT order_id, status FROM __source("raw_orders")\n'
46+
),
47+
"sources/raw_orders.yml": (
48+
"sources:\n"
49+
" - name: raw_orders\n"
50+
" expression: SELECT 1 AS order_id, 'open' AS status\n"
51+
" columns:\n"
52+
" - name: order_id\n type: INTEGER\n nullable: false\n"
53+
" - name: status\n type: VARCHAR\n nullable: true\n"
54+
),
55+
"tests/unit/test_orders.sql": (
56+
"TEST();\n\n"
57+
"WITH\n"
58+
"__source__raw_orders AS (SELECT 1 AS order_id),\n"
59+
"__expected__orders AS (\n"
60+
" SELECT 1 AS order_id, CAST(NULL AS VARCHAR) AS status\n"
61+
")\n"
62+
"SELECT 1\n"
63+
),
64+
},
65+
)
66+
discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(project_dir=tmp_path)
67+
adapter: DuckDbAdapter = DuckDbAdapter()
68+
69+
project: CompiledProject = compile_project(
70+
discovered_inputs=discovered_inputs,
71+
adapter=adapter,
72+
)
73+
entries, warnings = build_selected_test_entries(
74+
project=project,
75+
adapter=adapter,
76+
selected_keys=frozenset((project.models[0].key,)),
77+
)
78+
79+
assert warnings == []
80+
assert len(entries) == 1
81+
entry: SqlTestPlanEntry = entries[0]
82+
assert test_case.expected_sql_fragment in entry.chain[0].resolved_sql
83+
84+
85+
if __name__ == "__main__":
86+
pytest.main([__file__, "-vv"])

0 commit comments

Comments
 (0)