Skip to content

Commit d6be114

Browse files
authored
perf(plan): cut plan and build planning time on large projects (#875)
* perf(plan): compute migration fingerprints once and only where needed Rename discovery fingerprinted every unbuilt model under each rename map even when no earlier model could match, and the plan fingerprinted every entry again for storage. Skip matching without candidates, reuse fingerprints across rename maps and phases, attach stored fingerprints only to entries that will be built, and skip them for sqb plan, which never stores them. * perf(plan): remove quadratic union, ordering, and logging work Resolving column reads through a UNION chain materialised every branch's full argument tree at each level, so fixture validation grew cubically with fixture rows. Read only the needed arguments. Order keys with a heap instead of re-sorting the ready list, and look up the package version once instead of per logged statement. * perf(plan): skip rebinding sources whose inspection adds no evidence Expression sources report column names without types, so every source looked changed and every downstream model was analysed again during planning. Treat inspected columns as changed only when names differ or a reported type differs from the compile-time shape. * perf(build): reuse migration fingerprints from the local compile cache Every build fingerprinted every model it built, about 7 ms on average and linear in query size, so a warm build of an unchanged 3,000-model project spent 25 s recomputing identical values. Persist fingerprints in the project-local compile cache, keyed by query, metadata, dialect, visible renames, algorithm, and sqlbuild and polyglot versions, with digest-checked entries that fall back to recomputing when damaged. Also stop walking each query's local names twice. * perf(plan): discover projects for plan and build like compile Plan and build located every model's output columns during discovery, which took about 2.4 s on the 3,000-model benchmark, while compile skips it. The only consumer, contract diagnostics, already computes those locations lazily when they are missing. Skip the eager scan for plan and build as compile does. * perf(plan): infer each distinct SQL test fixture once per plan Fixture validation re-inferred the columns of every fixture body for every test, although identical bodies under the same inference profile always infer the same columns. Memoise inference per planning context, keyed by fixture SQL and an equal profile, and bypass the memo for profiles that carry a binding catalog. * test(perf): guard plan and build time, memory, and scaling Bound sqb plan --json on the 3,000-model inspection benchmark relative to a warm compile measured in the same test, cap its peak RSS, and fail when planning CPU grows super-linearly from a 300-model copy. Build a 1,000-model DuckDB benchmark from an empty warehouse with wall and RSS limits, then plan against its stored state after editing models and after renaming one. Raise the lineage ceilings to 3.0 s for CI headroom. Refs: CHI-432 * fix(plan): treat non-ASCII fingerprint cache digests as cache misses * test: count the migration fingerprint cache among compiler caches
1 parent 6da7c51 commit d6be114

46 files changed

Lines changed: 1781 additions & 78 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/verify.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ jobs:
8484
broad=true
8585
performance=true
8686
;;
87-
src/sqlbuild/cli/commands/_helpers/lineage/*|src/sqlbuild/cli/commands/_helpers/plan/*|src/sqlbuild/cli/commands/_helpers/scope/*|src/sqlbuild/cli/commands/main/inspection/*|src/sqlbuild/cli/commands/main/project/*|src/sqlbuild/cli/output/*|src/sqlbuild/presentation/*)
88-
# Inspection command guards run inside the fresh-process compile jobs.
87+
src/sqlbuild/cli/commands/_helpers/lineage/*|src/sqlbuild/cli/commands/_helpers/plan/*|src/sqlbuild/cli/commands/_helpers/scope/*|src/sqlbuild/cli/commands/_helpers/build_planning/*|src/sqlbuild/cli/commands/main/inspection/*|src/sqlbuild/cli/commands/main/project/*|src/sqlbuild/cli/output/*|src/sqlbuild/executor/build/*|src/sqlbuild/executor/run/*|src/sqlbuild/presentation/*)
88+
# Inspection, plan, and build guards run inside the fresh-process compile jobs.
8989
broad=true
9090
performance=true
9191
;;

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ test-e2e-cold-compile-performance:
237237
env PYTHONUNBUFFERED=1 SQLBUILD_CONCURRENCY=4 uv run pytest \
238238
tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py \
239239
tests/e2e/src/sqlbuild/cli/commands/main/compile/test_inspection_command_performance.py \
240+
tests/e2e/src/sqlbuild/cli/commands/main/compile/test_build_performance.py \
240241
-m cold_compile_performance -k "models_$(SQLBUILD_BENCHMARK_MODELS)" \
241242
-vv --log-cli-level=INFO --color=yes
242243

‎src/sqlbuild/cli/commands/_helpers/build_planning/invocation.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def resolve_build_invocation(*, request: BuildCommandRequest) -> BuildInvocation
3535
request.project_dir if request.project_dir is not None else Path.cwd()
3636
)
3737
discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(
38-
project_dir=effective_project_dir
38+
project_dir=effective_project_dir, extract_output_column_locations=False
3939
)
4040
effective_defer_clone_from: str | None = _resolve_defer_clone_from(
4141
discovered_inputs=discovered_inputs,

‎src/sqlbuild/cli/commands/_helpers/plan/invocation.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def resolve_plan_invocation(*, request: PlanCommandRequest) -> PlanInvocation:
3232
request.project_dir if request.project_dir is not None else Path.cwd()
3333
)
3434
discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(
35-
project_dir=effective_project_dir
35+
project_dir=effective_project_dir, extract_output_column_locations=False
3636
)
3737
_validate_preview_target(discovered_inputs=discovered_inputs, as_target=request.as_target)
3838
adapter_context: AdapterConnectionContext = resolve_adapter_connection_context(

‎src/sqlbuild/cli/commands/_helpers/plan/planning.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def compile_plan_pipeline(
5353
resolve_python_run_selectors=request.include_python,
5454
max_microbatches=request.max_microbatches,
5555
selection_diagnostics=request.selection_diagnostics,
56+
record_migration_fingerprints=False,
5657
),
5758
hooks=ConnectionHooks(
5859
on_progress=invocation.planning_progress.on_progress,

‎src/sqlbuild/compiler/compile/_helpers/analysis/required_columns.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def _resolve_set_operation_reads(
217217
branches: list[_RelationReads] = []
218218
required: list[CompiledLineageSourceFact] = []
219219
for side in ("left", "right"):
220-
branch: Any | None = getattr(operation, "args", {}).get(side)
220+
branch: Any | None = operation.arg(side)
221221
if branch is None:
222222
return _RelationReads(), _deduplicated_sources(required)
223223
branch_reads, branch_required = _resolve_query_reads(
@@ -228,7 +228,7 @@ def _resolve_set_operation_reads(
228228
branches.append(branch_reads)
229229
required.extend(branch_required)
230230
left, right = branches
231-
if bool(getattr(operation, "args", {}).get("by_name")):
231+
if bool(operation.arg("by_name")):
232232
right_by_name: dict[str, tuple[CompiledLineageSourceFact, ...]] = {
233233
name.casefold(): leaves for name, leaves in right.slots
234234
}

‎src/sqlbuild/compiler/compile/_helpers/assembly/source_bindings.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def get_source_binding_diagnostics(
8282
for name, value in columns.items():
8383
if value:
8484
raw_shape: dict[str, str] = {column.name: column.type for column in value}
85-
if raw_shape != shapes.get(name) or (
85+
if _adds_physical_evidence(physical=raw_shape, known=shapes.get(name)) or (
8686
profile.sql_analysis_dialect in CASE_SENSITIVE_BINDING_DIALECTS
8787
and not profile.quoted_identifiers_ignore_case
8888
):
@@ -202,6 +202,16 @@ def get_source_binding_diagnostics(
202202
return tuple(diagnostics)
203203

204204

205+
def _adds_physical_evidence(*, physical: dict[str, str], known: dict[str, str] | None) -> bool:
206+
"""Return whether inspected columns differ from the shape; untyped names match any type."""
207+
208+
if known is None or physical.keys() != known.keys():
209+
return True
210+
return any(
211+
physical_type and physical_type != known[name] for name, physical_type in physical.items()
212+
)
213+
214+
205215
def _placeholders(model: CompiledModel) -> dict[str, str] | None:
206216
values: object = model.config.values.get("placeholders")
207217
if isinstance(values, dict):

‎src/sqlbuild/compiler/pipeline/main/compile.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def _build_result(
247247
auto_load_sources=options.auto_load_sources,
248248
selection_diagnostics=options.selection_diagnostics,
249249
plan_sql_tests=options.plan_sql_tests,
250+
record_migration_fingerprints=options.record_migration_fingerprints,
250251
),
251252
on_progress=on_progress,
252253
project_config=discovered_inputs.project_config,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class CompilePipelineOptions:
6161
max_microbatches: int | None = None
6262
selection_diagnostics: bool = False
6363
plan_sql_tests: bool = True
64+
record_migration_fingerprints: bool = True
6465

6566

6667
@dataclass(frozen=True)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from sqlbuild.compiler.compile.main._analyze_columns_and_lineage import (
1212
analyze_resolved_column_reads,
1313
)
14-
from sqlbuild.compiler.compile.main._infer_fixture_columns import infer_fixture_column_facts
1514
from sqlbuild.compiler.compile.models import (
1615
CompiledLineageColumnFact,
1716
CompiledLineageSourceFact,
@@ -28,6 +27,7 @@
2827
ColumnTransformKind,
2928
)
3029
from sqlbuild.compiler.planner._helpers.fixtures.empty_fixture import is_empty_fixture_query
30+
from sqlbuild.compiler.planner.classes.fixture_column_inferences import FixtureColumnInferences
3131
from sqlbuild.compiler.planner.models import (
3232
FixtureColumnMetadata,
3333
FixtureRelationMetadata,
@@ -74,6 +74,7 @@ def build_relation_fixture_completion(
7474
fixture_groups=fixture_groups,
7575
relations=relations,
7676
adapter=adapter,
77+
inferences=context.fixture_inferences,
7778
)
7879

7980
fixture_keys_requiring_analysis: frozenset[FixtureKey] = _fixture_keys_requiring_analysis(
@@ -217,6 +218,7 @@ def _prepare_fixture_inputs(
217218
fixture_groups: FixtureGroups,
218219
relations: dict[FixtureKey, FixtureRelationMetadata],
219220
adapter: BaseAdapter,
221+
inferences: FixtureColumnInferences,
220222
) -> tuple[
221223
dict[FixtureKey, str],
222224
dict[FixtureKey, tuple[InferredColumn, ...]],
@@ -250,7 +252,7 @@ def _prepare_fixture_inputs(
250252
continue
251253
fixture_sql = empty_sql
252254
fixture_sql_by_key[key] = fixture_sql
253-
inference: FixtureColumnInference | None = infer_fixture_column_facts(
255+
inference: FixtureColumnInference | None = inferences.infer(
254256
query_sql=fixture_sql,
255257
inference_profile=adapter.expression_inference_profile(),
256258
)

0 commit comments

Comments
 (0)