Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,13 @@ jobs:
if-no-files-found: warn
retention-days: 30
- name: Compare generated seeds and the failure corpus (python vs shipped native)
run: make compiler-differential-shipped
run: >-
make compiler-differential-shipped
COMPILER_DIFFERENTIAL_ARGS="--stage-captures --require-discovery-coverage
--require-render-coverage"

compiler_differential_python_versions:
name: Native discovery on Python ${{ matrix.python }}
name: Native compiler on Python ${{ matrix.python }}
needs: [changes, lockfile]
if: needs.changes.outputs.compiler_differential == 'true'
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -351,9 +354,15 @@ jobs:
uv run --no-sync python -m scripts.run_compiler_differential
--engines python native-preview
--corpus fixtures examples failures seeds --seeds 4 --jobs 4
- name: Compare python with shipped native on the failure corpus and a few seeds
env:
PYTHONUNBUFFERED: "1"
run: >-
uv run --no-sync python -m scripts.run_compiler_differential
--engines python native --corpus failures seeds --seeds 4 --jobs 4

native_discovery_windows:
name: Native discovery on Windows
name: Native compiler on Windows
needs: [changes, lockfile]
if: needs.changes.outputs.compiler_differential == 'true'
runs-on: windows-2025
Expand Down Expand Up @@ -388,12 +397,22 @@ jobs:
tests/integration/src/sqlbuild/compiler/discovery/test_native_yaml_oracle.py
tests/integration/src/sqlbuild/compiler/discovery/test_native_toml_oracle.py
-n auto --dist loadfile
- name: Compile and build playground projects
- name: Compile playground projects on the default and Python engines, and build
run: |
project="${RUNNER_TEMP}/waffle-shop"
oracle="${RUNNER_TEMP}/waffle-shop-python"
uv run --no-sync sqb playground "${project}"
cp -r "${project}" "${oracle}"
uv run --no-sync sqb --project-dir "${project}" --no-color compile
SQLBUILD_COMPILER_ENGINE=python uv run --no-sync sqb --project-dir "${project}" --no-color compile
SQLBUILD_COMPILER_ENGINE=python uv run --no-sync sqb --project-dir "${oracle}" --no-color compile
diff -r "${oracle}/target/compiled" "${project}/target/compiled"
if ! test -f "${project}/target/cache/compiler-native-v1/macro-calls.bin"; then
echo "The default compile did not write the macro-call store; macro bridge debug log:"
rm -rf "${oracle}/target"
uv run --no-sync sqb --project-dir "${oracle}" --no-color --debug compile 2>&1 \
| grep "macro_bridge" || true
exit 1
fi
uv run --no-sync sqb --project-dir "${project}" --no-color build
nodes="${RUNNER_TEMP}/python-nodes"
uv run --no-sync sqb playground --template python_nodes "${nodes}"
Expand Down Expand Up @@ -561,15 +580,11 @@ jobs:
strategy:
fail-fast: false
matrix:
# Temporary: a one-model edit recompiles the edited model uncached until native
# rendering becomes the default; tighten edit_max_ratio back to max_ratio then.
include:
- kind: dense
max_ratio: "1.10"
edit_max_ratio: "1.25"
- kind: fresh
max_ratio: "1.10"
edit_max_ratio: "1.20"
steps:
- uses: actions/checkout@v4
- name: Check out the comparison base
Expand Down Expand Up @@ -600,13 +615,11 @@ jobs:
env:
KIND: ${{ matrix.kind }}
MAX_RATIO: ${{ matrix.max_ratio }}
EDIT_MAX_RATIO: ${{ matrix.edit_max_ratio }}
run: |
uv run python -m scripts.compare_compile_performance \
--kind "${KIND}" \
--modes cold warm edit \
--max-ratio "${MAX_RATIO}" \
--edit-max-ratio "${EDIT_MAX_RATIO}" \
--models 3000 \
--base-root base \
--base-python base/.venv/bin/python \
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ compiler-differential:
$(COMPILER_DIFFERENTIAL_ARGS)

# python vs native-preview above covers every native stage; this keeps the shipped `native` default
# covered on a bounded corpus.
# (native discovery and rendering) covered on a bounded corpus.
compiler-differential-shipped:
env PYTHONUNBUFFERED=1 uv run python -m scripts.run_compiler_differential \
--engines python native --corpus seeds failures \
Expand Down
15 changes: 13 additions & 2 deletions crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ stages run natively:
| `native` | The default: native stages that passed their flip gate (`shipped` tier). |
| `native-preview` | Opt-in: shipped stages plus stages still in development (`preview` tier). |

The shipped tier covers discovery and rendering: declaration files and scopes, model config,
reference extraction, the model loop, macro calls and the macro-call store, and attachments. SQL
analysis, contracts, lineage, SQL-test glue and project assembly are still `preview`. When a
native render stage fails, the compile runs the Python stage again so the error is exactly
Python's; native error messages replace that re-run later. Because the macro-call store is
shipped, the [macro determinism contract](../website/src/content/docs/docs/concepts/macros.mdx)
applies to every default compile; `SQLBUILD_COMPILER_ENGINE=python` runs every macro call each
time.

Each native stage declares its tier once, in `NATIVE_STAGE_TIERS` in
`src/sqlbuild/compiler/frontier/constants.py`. A stage moves from `preview` to `shipped` by
changing that line, after its flip gate passes: a byte-identical real project, a green
Expand All @@ -70,8 +79,10 @@ its own compiler, Rules and compile-reuse stores, so preview output is never reu

`make compiler-differential` compares `python` with `native-preview` on the full per-PR corpus.
`make compiler-differential-shipped` compares `python` with `native` on the generated seeds and
the failure corpus, so the shipped default stays covered on its own. CI runs both, and compares
`python` with `native` on Python 3.13 and 3.14 as well.
the failure corpus, so the shipped default stays covered on its own; CI runs it with stage
captures and requires full discovery and render coverage. CI runs both, compares `python` with
`native` and `native-preview` on Python 3.13 and 3.14 as well, and on Windows checks that the
default compiles a playground project to the same files as `python`.

## Working on the crates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ prints `Recording compile for reuse...` and a completion line to stderr before t

Any change runs the whole compile again: every model, test, and audit is rendered again, and
contracts and semantic validation run for the whole project. No render is carried over from the
previous compile, so until the native compiler ships, an edited model is recompiled without any
cache. A few caches under `target/cache/` shorten the rest:
previous compile; rendering runs natively and does not run unchanged macro calls again. A few
caches under `target/cache/` shorten the rest:

- An unchanged model takes its column analysis from the analysis cache when its query and the
analyses of its upstream models are unchanged.
- An unchanged SQL test file reuses its stored test scan, the CTEs and expected models read from
it, from `sql-test-scans.bin`. Entries are keyed by the file's expanded text, the adapter's SQL
lexical rules, and the SQLBuild build; a damaged or unreadable entry is scanned again.
- Rule results are reused for unchanged inputs.
- Macro call results may be reused as described in
- Macro call results are reused as described in
[Macros must be deterministic](../concepts/macros.md#macros-must-be-deterministic).

An edit therefore costs a full render of the project plus the analysis, test scans, and rules of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ SQLBuild relies on this contract to reuse macro results:
- [Compile reuse](../cli/compile.md) replays an unchanged compile without running macros again.
- A compile may run each distinct macro call once and reuse its result for identical calls
elsewhere in the project.
- Macro call results may be kept between compiles under `target/cache/`. They are recomputed when
- Macro call results are kept between compiles under `target/cache/` by default, so editing a
model does not run its unchanged macro calls again. They are recomputed when
macro or other Python files, configuration, declarations, seeds, sources, variables, the target,
the adapter, the installed packages, or the SQLBuild version change. Editing only model SQL keeps
them, and a changed call is always run again. Only the first compile in a process uses and saves
Expand Down
12 changes: 8 additions & 4 deletions src/sqlbuild/cli/compile_reuse/_helpers/runtime_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ def search_path_stamps(*, project_dir: str) -> tuple[tuple[str, int], ...]:
return tuple(stamps)


def loaded_module_stamps(*, covered_paths: frozenset[str]) -> tuple[tuple[str, int, int], ...]:
"""Stat every loaded module file that the project walk does not already cover."""
def loaded_module_stamps(
*, covered_paths: frozenset[str], extra_paths: frozenset[str]
) -> tuple[tuple[str, int, int], ...]:
"""Stat every loaded module file, and `extra_paths`, that the project walk does not cover."""

stamps: dict[str, tuple[str, int, int]] = {}
for module in tuple(sys.modules.values()):
path: object = getattr(module, "__file__", None)
module_paths: tuple[object, ...] = tuple(
getattr(module, "__file__", None) for module in tuple(sys.modules.values())
)
for path in (*module_paths, *sorted(extra_paths)):
if not isinstance(path, str) or path in stamps or path in covered_paths:
continue
try:
Expand Down
6 changes: 5 additions & 1 deletion src/sqlbuild/cli/compile_reuse/_helpers/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
)
from sqlbuild.cli.compile_reuse.types import CompileReuseOutcome, FileStamp
from sqlbuild.compiler.compile.classes.compile_input_reads import CompileInputReads
from sqlbuild.compiler.macro_bridge.main.macro_store_module_paths import (
macro_store_module_paths,
)

_LOGGER: logging.Logger = logging.getLogger(REUSE_LOGGER_NAME)

Expand Down Expand Up @@ -146,7 +149,8 @@ def _write_compile_entry(
modules=loaded_module_stamps(
covered_paths=frozenset(
os.path.join(project_dir, relative_path) for relative_path in attempt.snapshot
)
),
extra_paths=macro_store_module_paths(),
),
project_files=stored_project_files(
snapshot=attempt.snapshot, digests=digests, snapshot_ns=attempt.snapshot_ns
Expand Down
16 changes: 8 additions & 8 deletions src/sqlbuild/compiler/frontier/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
}
NATIVE_STAGE_TIERS: dict[NativeStage, NativeStageTier] = {
NativeStage.DISCOVERY: NativeStageTier.SHIPPED,
NativeStage.DECLARATION_SCOPES: NativeStageTier.PREVIEW,
NativeStage.MODEL_CONFIG: NativeStageTier.PREVIEW,
NativeStage.REFERENCE_EXTRACTION: NativeStageTier.PREVIEW,
NativeStage.DECLARATION_FILES: NativeStageTier.PREVIEW,
NativeStage.MODEL_LOOP: NativeStageTier.PREVIEW,
NativeStage.MACRO_CALLS: NativeStageTier.PREVIEW,
NativeStage.MACRO_CALL_STORE: NativeStageTier.PREVIEW,
NativeStage.ATTACHMENTS: NativeStageTier.PREVIEW,
NativeStage.DECLARATION_SCOPES: NativeStageTier.SHIPPED,
NativeStage.MODEL_CONFIG: NativeStageTier.SHIPPED,
NativeStage.REFERENCE_EXTRACTION: NativeStageTier.SHIPPED,
NativeStage.DECLARATION_FILES: NativeStageTier.SHIPPED,
NativeStage.MODEL_LOOP: NativeStageTier.SHIPPED,
NativeStage.MACRO_CALLS: NativeStageTier.SHIPPED,
NativeStage.MACRO_CALL_STORE: NativeStageTier.SHIPPED,
NativeStage.ATTACHMENTS: NativeStageTier.SHIPPED,
NativeStage.TYPE_SYSTEM: NativeStageTier.PREVIEW,
NativeStage.MODEL_ANALYSIS: NativeStageTier.PREVIEW,
NativeStage.SEMANTIC_CHECKS: NativeStageTier.PREVIEW,
Expand Down
16 changes: 16 additions & 0 deletions src/sqlbuild/compiler/macro_bridge/_helpers/store_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import sys
import sysconfig
import zipimport
from collections.abc import Iterable
from pathlib import Path

Expand All @@ -18,6 +19,7 @@
INSTALLED_RECORD_FILE_NAME,
INTERPRETER_MODULE_ORIGINS,
MACRO_CALL_STORE_ENVIRONMENT_VERSION,
MAIN_MODULE_NAME,
MISSING_SEARCH_PATH_STAMP,
MODULE_DIGEST_ROW_FIELDS,
PROJECT_ROOT_SEARCH_PATH_STAMP,
Expand Down Expand Up @@ -114,6 +116,9 @@ def _backing_file(module: object) -> tuple[str | None, bool]:
file: object = getattr(module, "__file__", None)
if spec is None:
return (file, True) if isinstance(file, str) and os.path.isfile(file) else (None, True)
launcher: str | None = _launcher_executable(spec)
if launcher is not None:
return launcher, True
origin: object = getattr(spec, "origin", None)
if getattr(spec, "has_location", False) and isinstance(origin, str):
return origin, True
Expand All @@ -128,6 +133,17 @@ def _backing_file(module: object) -> tuple[str | None, bool]:
return None, False


def _launcher_executable(spec: object) -> str | None:
"""The launcher executable (as uv's on Windows) whose appended zip `__main__` ran from."""

loader: object = getattr(spec, "loader", None)
if getattr(spec, "name", None) == MAIN_MODULE_NAME and isinstance(
loader, zipimport.zipimporter
):
return loader.archive
return None


def _covered_without_content(path: str) -> bool:
installed, interpreter, own = _excluded_prefixes()
if path.startswith(own):
Expand Down
5 changes: 5 additions & 0 deletions src/sqlbuild/compiler/macro_bridge/classes/macro_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ def stats(self) -> tuple[int, int, int]:

return self._memo.stats()

def store_module_paths(self) -> tuple[str, ...]:
"""Module files the stored call results depend on, imported this compile or not."""

return tuple(self._module_digests) if self._store_path is not None else ()

def store_stats(self) -> tuple[int, int]:
"""Calls found in the store and calls recorded into it."""

Expand Down
2 changes: 2 additions & 0 deletions src/sqlbuild/compiler/macro_bridge/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"sqlbuild_active_macro_bridge", default=None
)
COMPILES_STARTED: itertools.count[int] = itertools.count()
STORE_MODULE_PATHS: set[str] = set()
MACRO_CALL_STORE_FILE_NAME: str = "macro-calls.bin"
MACRO_CALL_STORE_ENVIRONMENT_VERSION: str = "macro-call-store-v2"
VALUE_RENDERER_FIELD: str = "_value_renderer"
Expand All @@ -34,6 +35,7 @@
INSTALLED_DISTRIBUTION_SUFFIX: str = ".dist-info"
INSTALLED_RECORD_FILE_NAME: str = "RECORD"
INTERPRETER_MODULE_ORIGINS: frozenset[str] = frozenset({"built-in", "frozen"})
MAIN_MODULE_NAME: str = "__main__"
STORE_TRACKED_ENVIRONMENT_PREFIXES: tuple[str, ...] = ("SQLBUILD_", "SQB_")
STORE_UNTRACKED_ENVIRONMENT_NAMES: frozenset[str] = frozenset(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Module files that macro call results reused from the store in this process depend on."""

from __future__ import annotations

from sqlbuild.compiler.macro_bridge.constants import STORE_MODULE_PATHS


def macro_store_module_paths() -> frozenset[str]:
"""Return the module files behind stored macro call results, even ones never imported."""

return frozenset(STORE_MODULE_PATHS)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlbuild.compiler.compile.exceptions import CompileInputError
from sqlbuild.compiler.frontier.exceptions import NativeStageMismatchError
from sqlbuild.compiler.macro_bridge.classes.macro_bridge import MacroBridge
from sqlbuild.compiler.macro_bridge.constants import ACTIVE_MACRO_BRIDGE
from sqlbuild.compiler.macro_bridge.constants import ACTIVE_MACRO_BRIDGE, STORE_MODULE_PATHS


def run_with_macro_bridge[T](*, stage: Callable[[], T]) -> T:
Expand All @@ -36,6 +36,7 @@ def run_with_macro_bridge[T](*, stage: Callable[[], T]) -> T:
native_error = error
else:
bridge.save_store()
STORE_MODULE_PATHS.update(bridge.store_module_paths())
hits, misses, recorded = bridge.stats()
store_hits, store_records = bridge.store_stats()
logging.getLogger(__name__).debug(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""E2E: layered, templated and validated model config builds the same on every compiler engine.

The preview engine builds model config (path defaults, templates, storage policies) and checks the
The native and preview engines build model config (path defaults, templates, storage policies) and checks the
model validators natively; a validator rejection re-runs Python for the exact error. Every engine
must build the same relations and report the same validator errors.
"""
Expand Down
24 changes: 24 additions & 0 deletions tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,18 @@ class BrokenEditInvalidationTestCase:
expected_matches_uncached: bool


@dataclass(frozen=True)
class StoredMacroModuleEditTestCase:
"""An outside module behind a stored macro call, edited after a compile that reused the call."""

description: str
engine: str
import_name: str
module_files: dict[str, str]
edited_files: dict[str, str]
expected_compiled_value: str


@dataclass(frozen=True)
class ExternalModuleEditTestCase:
"""An outside module a macro imports while rendering, rewritten in place after a compile."""
Expand Down Expand Up @@ -792,6 +804,18 @@ class CompilerEngineParityTestCase:
expected_exit_codes: tuple[int, int]


@dataclass(frozen=True)
class DefaultEngineParityTestCase:
"""The Python oracle and the unset default compiling copies of one project, with stores."""

description: str
oracle_engine: str
default_engine: str
expected_engines: tuple[str, str]
expected_exit_codes: tuple[int, int]
expected_macro_call_stores: tuple[tuple[str, ...], tuple[str, ...]]


@dataclass(frozen=True)
class NativeTypeSystemTestCase:
"""Python and preview compiles of one project whose contracts compare type spellings."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
NativeAttachmentErrorTestCase(
description="a cursor intrinsic in a generic audit re-runs as it reads expanded SQL",
case_name="generic-audit-cursor-intrinsic",
expected_macro_calls=(1, 1, 2),
expected_macro_calls=(1, 2, 2),
expected_error_types=_ERROR_TYPES,
),
NativeAttachmentErrorTestCase(
Expand Down
Loading
Loading