Skip to content

Commit 2cc70cf

Browse files
committed
perf(compile): plan SQL tests in the background in every compile mode
SQL-test planning only ran in the background for uncached projects of 128 to 5000 models with rules selected, so cache-on and large compiles planned on the critical path in the write phase. Prepared artifacts now fall back to planning SQL tests alone in the background, against the published target and its artifact cache, and the write phase consumes that plan at the point it would otherwise plan inline. Results and errors are identical to inline planning.
1 parent 3308f20 commit 2cc70cf

9 files changed

Lines changed: 470 additions & 44 deletions

File tree

‎src/sqlbuild/cli/commands/_helpers/compile/pipeline.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,12 @@ def _analyze_compile_project(
201201
)
202202
if not graph_failed:
203203
rules_config: RulesConfig = load_rules_config(project_dir=project_dir)
204-
if (
205-
prepared_artifacts is not None
206-
and rules_config.select
207-
and not any(item.is_error for item in core_diagnostics)
208-
):
204+
if prepared_artifacts is not None and not any(item.is_error for item in core_diagnostics):
209205
prepared_artifacts.start(
210-
project=graph.project, adapter=adapter, target_dir=project_dir / "target"
206+
project=graph.project,
207+
adapter=adapter,
208+
target_dir=project_dir / "target",
209+
stage_artifacts=bool(rules_config.select),
211210
)
212211
_ = start_compile_phase(status=status, message="Evaluating built-in and custom rules...")
213212
rules_start: float = time.monotonic()

‎src/sqlbuild/cli/commands/_helpers/compile/target_writer.py‎

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import os
88
import uuid
9+
from collections.abc import Callable
910
from pathlib import Path
1011

1112
from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
@@ -20,6 +21,8 @@
2021
)
2122
from sqlbuild.cli.commands.exceptions import StagedArtifactsChangedError
2223
from sqlbuild.cli.compile.models import (
24+
PendingStaticSqlTest,
25+
PlannedStaticSqlTests,
2326
SqlTestArtifactCacheRecord,
2427
SqlTestArtifactIdentityContext,
2528
)
@@ -124,8 +127,9 @@ def write_static_compile_target(
124127
adapter: BaseAdapter,
125128
project: CompiledProject,
126129
manifest: dict[str, object] | None = None,
130+
planned_tests: Callable[[], PlannedStaticSqlTests] | None = None,
127131
) -> WrittenTarget:
128-
"""Write offline compiled output files under target_dir."""
132+
"""Write offline compiled output files under target_dir, reusing SQL tests planned ahead."""
129133

130134
remove_stale_files: bool = (target_dir / _COMPILED_DIR).is_dir()
131135
target_dir.mkdir(parents=True, exist_ok=True)
@@ -152,6 +156,7 @@ def write_static_compile_target(
152156
adapter=adapter,
153157
project=project,
154158
check_existing=remove_stale_files,
159+
planned_tests=planned_tests,
155160
)
156161
managed_paths.update(test_paths)
157162
if remove_stale_files:
@@ -442,27 +447,60 @@ def _write_tests(
442447

443448

444449
def static_sql_test_planning_diagnostics(
445-
*, target_dir: Path, adapter: BaseAdapter, project: CompiledProject
450+
*,
451+
target_dir: Path,
452+
adapter: BaseAdapter,
453+
project: CompiledProject,
454+
planned_tests: Callable[[], PlannedStaticSqlTests] | None = None,
446455
) -> tuple[CompilerDiagnostic, ...]:
447456
"""Plan uncached SQL tests and report their errors without writing artifacts or cache."""
448457

449-
pending, _, _ = _partition_cached_static_tests(
450-
tests_root=target_dir / _COMPILED_DIR / _TESTS_DIR, adapter=adapter, project=project
451-
)
452-
tests: tuple[CompiledSqlTest, ...] = tuple(test for test, _, _ in pending)
453-
artifacts: tuple[NativeSqlTestArtifact, ...] = _plan_static_test_artifacts(
454-
adapter=adapter, project=project, tests=tests
458+
planned: PlannedStaticSqlTests = _resolve_planned_static_tests(
459+
target_dir=target_dir, adapter=adapter, project=project, planned_tests=planned_tests
455460
)
456461
diagnostics: list[CompilerDiagnostic] = []
457-
for test, artifact in zip(tests, artifacts, strict=True):
458-
diagnostics.extend(_sql_test_artifact_diagnostics(test=test, artifact=artifact))
462+
for pending, artifact in zip(planned.pending, planned.artifacts, strict=True):
463+
diagnostics.extend(_sql_test_artifact_diagnostics(test=pending.test, artifact=artifact))
459464
return tuple(diagnostics)
460465

461466

467+
def plan_static_sql_tests(
468+
*, target_dir: Path, adapter: BaseAdapter, project: CompiledProject
469+
) -> PlannedStaticSqlTests:
470+
"""Plan every SQL test whose cached artifact under target_dir cannot be reused."""
471+
472+
tests_root: Path = target_dir / _COMPILED_DIR / _TESTS_DIR
473+
pending, cached_paths, cached_records = _partition_cached_static_tests(
474+
tests_root=tests_root, adapter=adapter, project=project
475+
)
476+
return PlannedStaticSqlTests(
477+
tests_root=tests_root,
478+
pending=pending,
479+
artifacts=_plan_static_test_artifacts(
480+
adapter=adapter, project=project, tests=tuple(item.test for item in pending)
481+
),
482+
cached_paths=frozenset(cached_paths),
483+
cached_records=cached_records,
484+
)
485+
486+
487+
def _resolve_planned_static_tests(
488+
*,
489+
target_dir: Path,
490+
adapter: BaseAdapter,
491+
project: CompiledProject,
492+
planned_tests: Callable[[], PlannedStaticSqlTests] | None,
493+
) -> PlannedStaticSqlTests:
494+
planned: PlannedStaticSqlTests | None = planned_tests() if planned_tests is not None else None
495+
if planned is not None and planned.tests_root == target_dir / _COMPILED_DIR / _TESTS_DIR:
496+
return planned
497+
return plan_static_sql_tests(target_dir=target_dir, adapter=adapter, project=project)
498+
499+
462500
def _partition_cached_static_tests(
463501
*, tests_root: Path, adapter: BaseAdapter, project: CompiledProject
464502
) -> tuple[
465-
list[tuple[CompiledSqlTest, str | None, str | None]],
503+
tuple[PendingStaticSqlTest, ...],
466504
set[Path],
467505
dict[str, SqlTestArtifactCacheRecord],
468506
]:
@@ -484,7 +522,7 @@ def _partition_cached_static_tests(
484522
project=project,
485523
tests=project.sql_tests,
486524
)
487-
pending: list[tuple[CompiledSqlTest, str | None, str | None]] = []
525+
pending: list[PendingStaticSqlTest] = []
488526
for test in project.sql_tests:
489527
record_key: str | None = None
490528
artifact_identity: str | None = None
@@ -506,8 +544,12 @@ def _partition_cached_static_tests(
506544
managed_paths.add(cached_path)
507545
current_records[record_key] = cached_record
508546
continue
509-
pending.append((test, record_key, artifact_identity))
510-
return pending, managed_paths, current_records
547+
pending.append(
548+
PendingStaticSqlTest(
549+
test=test, record_key=record_key, artifact_identity=artifact_identity
550+
)
551+
)
552+
return tuple(pending), managed_paths, current_records
511553

512554

513555
def _plan_static_test_artifacts(
@@ -534,20 +576,21 @@ def _write_static_tests(
534576
adapter: BaseAdapter,
535577
project: CompiledProject,
536578
check_existing: bool,
579+
planned_tests: Callable[[], PlannedStaticSqlTests] | None,
537580
) -> tuple[set[Path], tuple[CompilerDiagnostic, ...]]:
538581
"""Write offline SQL-native test SQL and report uncached planning errors."""
539582

540583
diagnostics: list[CompilerDiagnostic] = []
541-
tests_root: Path = target_dir / _COMPILED_DIR / _TESTS_DIR
542-
pending, managed_paths, current_records = _partition_cached_static_tests(
543-
tests_root=tests_root, adapter=adapter, project=project
544-
)
545-
native_artifacts: tuple[NativeSqlTestArtifact, ...] = _plan_static_test_artifacts(
546-
adapter=adapter, project=project, tests=tuple(test for test, _, _ in pending)
584+
planned: PlannedStaticSqlTests = _resolve_planned_static_tests(
585+
target_dir=target_dir, adapter=adapter, project=project, planned_tests=planned_tests
547586
)
548-
for (test, record_key, artifact_identity), artifact in zip(
549-
pending, native_artifacts, strict=True
550-
):
587+
tests_root: Path = planned.tests_root
588+
managed_paths: set[Path] = set(planned.cached_paths)
589+
current_records: dict[str, SqlTestArtifactCacheRecord] = dict(planned.cached_records)
590+
for pending, artifact in zip(planned.pending, planned.artifacts, strict=True):
591+
test: CompiledSqlTest = pending.test
592+
record_key: str | None = pending.record_key
593+
artifact_identity: str | None = pending.artifact_identity
551594
test_path: Path = tests_root / compiled_sql_test_output_path(
552595
test=test,
553596
model_names=artifact.model_names,

‎src/sqlbuild/cli/commands/classes/prepared_compile_artifacts.py‎

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Own staged cold compile artifacts and publish them only at the normal write phase."""
1+
"""Prepare compile artifacts in the background and publish them only at the normal write phase."""
22

33
from __future__ import annotations
44

@@ -14,11 +14,14 @@
1414

1515
from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
1616
from sqlbuild.cli.commands._helpers.compile.target_writer import (
17+
plan_static_sql_tests,
1718
publish_static_compile_target,
1819
staged_artifact_files,
20+
static_sql_test_planning_diagnostics,
1921
write_static_compile_target,
2022
)
2123
from sqlbuild.cli.commands.exceptions import StagedArtifactsChangedError
24+
from sqlbuild.cli.compile.models import PlannedStaticSqlTests
2225
from sqlbuild.cli.output.models import WrittenTarget
2326
from sqlbuild.compiler.compile.models import CompiledProject, CompilerDiagnostic
2427

@@ -32,7 +35,7 @@
3235

3336

3437
class PreparedCompileArtifacts:
35-
"""Own one bounded preparation task and its disposable output directory."""
38+
"""Own one background preparation task: staged artifacts, or planned SQL tests only."""
3639

3740
def __init__(self, *, enabled: bool) -> None:
3841
self.enabled: bool = enabled
@@ -41,6 +44,8 @@ def __init__(self, *, enabled: bool) -> None:
4144
self._created_target_dir: Path | None = None
4245
self._executor: ThreadPoolExecutor | None = None
4346
self._future: Future[_PreparedResult] | None = None
47+
self._planned_tests: Future[PlannedStaticSqlTests] | None = None
48+
self._planning_inputs: tuple[CompiledProject, BaseAdapter, Path] | None = None
4449

4550
def __enter__(self) -> PreparedCompileArtifacts:
4651
return self
@@ -73,16 +78,48 @@ def remove_abandoned_staging(self, *, target_dir: Path) -> None:
7378
if not _remove_abandoned_directory(directory):
7479
return
7580

76-
def start(self, *, project: CompiledProject, adapter: BaseAdapter, target_dir: Path) -> None:
77-
"""Stage artifacts beside target_dir so publication can move them instead of copying."""
81+
def start(
82+
self,
83+
*,
84+
project: CompiledProject,
85+
adapter: BaseAdapter,
86+
target_dir: Path,
87+
stage_artifacts: bool,
88+
) -> None:
89+
"""Stage artifacts beside target_dir when worthwhile, otherwise plan SQL tests ahead."""
7890

91+
if not self.enabled:
92+
return
7993
if (
80-
not self.enabled
81-
or project.compile_cache_dir is not None
82-
or len(project.models) < _MIN_PREPARED_ARTIFACT_MODELS
83-
or len(project.models) > _MAX_PREPARED_ARTIFACT_MODELS
94+
stage_artifacts
95+
and _stages_artifacts(project)
96+
and self._start_staging(project=project, adapter=adapter, target_dir=target_dir)
8497
):
8598
return
99+
if project.sql_tests:
100+
self._start_test_planning(project=project, adapter=adapter, target_dir=target_dir)
101+
102+
def _start_test_planning(
103+
self, *, project: CompiledProject, adapter: BaseAdapter, target_dir: Path
104+
) -> None:
105+
"""Plan SQL tests against the published target while the compile continues."""
106+
107+
self._planning_inputs = (project, adapter, target_dir)
108+
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="sqlbuild-tests")
109+
context: Context = copy_context()
110+
111+
def plan() -> PlannedStaticSqlTests:
112+
return context.run(
113+
plan_static_sql_tests, target_dir=target_dir, adapter=adapter, project=project
114+
)
115+
116+
self._planned_tests = self._executor.submit(plan)
117+
118+
def _start_staging(
119+
self, *, project: CompiledProject, adapter: BaseAdapter, target_dir: Path
120+
) -> bool:
121+
"""Stage artifacts beside target_dir so publication can move them instead of copying."""
122+
86123
staging_dir: Path = target_dir / f"{_STAGING_PREFIX}{uuid.uuid4().hex}"
87124
try:
88125
if not target_dir.is_dir():
@@ -92,7 +129,7 @@ def start(self, *, project: CompiledProject, adapter: BaseAdapter, target_dir: P
92129
self._owner_lock.acquire()
93130
_create_staging_directory(staging_dir)
94131
except OSError:
95-
return
132+
return False
96133
self._staging_dir = staging_dir
97134
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="sqlbuild-artifacts")
98135
context: Context = copy_context()
@@ -107,10 +144,20 @@ def prepare() -> _PreparedResult:
107144
return written, staged_artifact_files(target_dir=staging_dir)
108145

109146
self._future = self._executor.submit(prepare)
147+
return True
110148

111149
def publish(
112150
self, *, target_dir: Path, manifest: dict[str, object] | None
113151
) -> WrittenTarget | None:
152+
if self._planned_tests is not None and self._planning_inputs is not None:
153+
project, adapter, _ = self._planning_inputs
154+
return write_static_compile_target(
155+
target_dir=target_dir,
156+
adapter=adapter,
157+
project=project,
158+
manifest=manifest,
159+
planned_tests=self._planned_tests.result,
160+
)
114161
if self._future is None or self._staging_dir is None:
115162
return None
116163
try:
@@ -131,8 +178,16 @@ def publish(
131178
)
132179

133180
def planning_diagnostics(self) -> tuple[CompilerDiagnostic, ...] | None:
134-
"""Return staged SQL test planning diagnostics without publishing any artifact."""
181+
"""Return background SQL test planning diagnostics without publishing any artifact."""
135182

183+
if self._planned_tests is not None and self._planning_inputs is not None:
184+
project, adapter, target_dir = self._planning_inputs
185+
return static_sql_test_planning_diagnostics(
186+
target_dir=target_dir,
187+
adapter=adapter,
188+
project=project,
189+
planned_tests=self._planned_tests.result,
190+
)
136191
if self._future is None:
137192
return None
138193
try:
@@ -141,6 +196,15 @@ def planning_diagnostics(self) -> tuple[CompilerDiagnostic, ...] | None:
141196
return None
142197

143198

199+
def _stages_artifacts(project: CompiledProject) -> bool:
200+
"""Staging pays off for uncached, mid-sized projects whose memory stays within budget."""
201+
202+
return (
203+
project.compile_cache_dir is None
204+
and _MIN_PREPARED_ARTIFACT_MODELS <= len(project.models) <= _MAX_PREPARED_ARTIFACT_MODELS
205+
)
206+
207+
144208
def _owner_lock_path(staging_dir: Path) -> Path:
145209
return staging_dir.with_name(f"{staging_dir.name}{_LOCK_SUFFIX}")
146210

‎src/sqlbuild/cli/compile/models.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
99
from sqlbuild.cli.commands.types import CompileLineageMode
1010
from sqlbuild.cli.output.models import WrittenTarget
11-
from sqlbuild.compiler.compile.models import CompiledObjectKey, CompilerDiagnostic
11+
from sqlbuild.compiler.compile.models import (
12+
CompiledObjectKey,
13+
CompiledSqlTest,
14+
CompilerDiagnostic,
15+
)
1216
from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs
1317
from sqlbuild.compiler.lineage.models import ProjectColumnLineage
1418
from sqlbuild.compiler.pipeline.models import ProjectGraph
19+
from sqlbuild.compiler.planner.models import NativeSqlTestArtifact
1520

1621

1722
@dataclass(frozen=True)
@@ -90,3 +95,23 @@ class SqlTestArtifactIdentityContext:
9095

9196
common_identity: str
9297
model_identities: dict[str, str]
98+
99+
100+
@dataclass(frozen=True)
101+
class PendingStaticSqlTest:
102+
"""One SQL test that needs a freshly planned artifact, with its optional cache identity."""
103+
104+
test: CompiledSqlTest
105+
record_key: str | None
106+
artifact_identity: str | None
107+
108+
109+
@dataclass(frozen=True)
110+
class PlannedStaticSqlTests:
111+
"""SQL-test artifacts planned for one target directory before any file is written."""
112+
113+
tests_root: Path
114+
pending: tuple[PendingStaticSqlTest, ...]
115+
artifacts: tuple[NativeSqlTestArtifact, ...]
116+
cached_paths: frozenset[Path]
117+
cached_records: dict[str, SqlTestArtifactCacheRecord]

0 commit comments

Comments
 (0)