diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index a5a48b0136..c9ac5ef1e7 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -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 @@ -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 @@ -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}" @@ -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 @@ -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 \ diff --git a/Makefile b/Makefile index 919fd5ffb8..8512fc21b1 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/crates/README.md b/crates/README.md index 05398296bd..11a3f744eb 100644 --- a/crates/README.md +++ b/crates/README.md @@ -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 @@ -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 diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/compile.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/compile.md index aac63d480d..068099a321 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/compile.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/compile.md @@ -129,8 +129,8 @@ 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. @@ -138,7 +138,7 @@ cache. A few caches under `target/cache/` shorten the rest: 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 diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/macros.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/macros.md index 07ccca54b1..0068663e6c 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/macros.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/macros.md @@ -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 diff --git a/src/sqlbuild/cli/compile_reuse/_helpers/runtime_identity.py b/src/sqlbuild/cli/compile_reuse/_helpers/runtime_identity.py index a676b33f60..44637f294a 100644 --- a/src/sqlbuild/cli/compile_reuse/_helpers/runtime_identity.py +++ b/src/sqlbuild/cli/compile_reuse/_helpers/runtime_identity.py @@ -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: diff --git a/src/sqlbuild/cli/compile_reuse/_helpers/store.py b/src/sqlbuild/cli/compile_reuse/_helpers/store.py index 7bed444a1d..9a3a8552f0 100644 --- a/src/sqlbuild/cli/compile_reuse/_helpers/store.py +++ b/src/sqlbuild/cli/compile_reuse/_helpers/store.py @@ -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) @@ -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 diff --git a/src/sqlbuild/compiler/frontier/constants.py b/src/sqlbuild/compiler/frontier/constants.py index 6594b32e77..195b5404c3 100644 --- a/src/sqlbuild/compiler/frontier/constants.py +++ b/src/sqlbuild/compiler/frontier/constants.py @@ -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, diff --git a/src/sqlbuild/compiler/macro_bridge/_helpers/store_environment.py b/src/sqlbuild/compiler/macro_bridge/_helpers/store_environment.py index 67bbca6d4d..600237decb 100644 --- a/src/sqlbuild/compiler/macro_bridge/_helpers/store_environment.py +++ b/src/sqlbuild/compiler/macro_bridge/_helpers/store_environment.py @@ -8,6 +8,7 @@ import os import sys import sysconfig +import zipimport from collections.abc import Iterable from pathlib import Path @@ -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, @@ -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 @@ -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): diff --git a/src/sqlbuild/compiler/macro_bridge/classes/macro_bridge.py b/src/sqlbuild/compiler/macro_bridge/classes/macro_bridge.py index f3a41f228e..e2d317fa45 100644 --- a/src/sqlbuild/compiler/macro_bridge/classes/macro_bridge.py +++ b/src/sqlbuild/compiler/macro_bridge/classes/macro_bridge.py @@ -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.""" diff --git a/src/sqlbuild/compiler/macro_bridge/constants.py b/src/sqlbuild/compiler/macro_bridge/constants.py index af15940fe6..89f27cde8e 100644 --- a/src/sqlbuild/compiler/macro_bridge/constants.py +++ b/src/sqlbuild/compiler/macro_bridge/constants.py @@ -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" @@ -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( { diff --git a/src/sqlbuild/compiler/macro_bridge/main/macro_store_module_paths.py b/src/sqlbuild/compiler/macro_bridge/main/macro_store_module_paths.py new file mode 100644 index 0000000000..82c33dde57 --- /dev/null +++ b/src/sqlbuild/compiler/macro_bridge/main/macro_store_module_paths.py @@ -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) diff --git a/src/sqlbuild/compiler/macro_bridge/main/run_with_macro_bridge.py b/src/sqlbuild/compiler/macro_bridge/main/run_with_macro_bridge.py index a79dc1fbd0..c30adf809f 100644 --- a/src/sqlbuild/compiler/macro_bridge/main/run_with_macro_bridge.py +++ b/src/sqlbuild/compiler/macro_bridge/main/run_with_macro_bridge.py @@ -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: @@ -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( diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/build/test_model_config_engines.py b/tests/e2e/src/sqlbuild/cli/commands/main/build/test_model_config_engines.py index d0f5288fa0..f124f6dd00 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/build/test_model_config_engines.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/build/test_model_config_engines.py @@ -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. """ diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py index 6024b3f300..a1f174b8ff 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py @@ -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.""" @@ -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.""" diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/attachment_errors/test_native_attachment_errors.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/attachment_errors/test_native_attachment_errors.py index a726f9e503..9ffdf0f4cf 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/attachment_errors/test_native_attachment_errors.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/attachment_errors/test_native_attachment_errors.py @@ -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( diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py index 91826449b3..505571605f 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py @@ -4277,6 +4277,53 @@ def add_external_flavor_macro(*, project_dir: Path, extlib: Path, value: str) -> return {"PYTHONPATH": str(extlib), "PYTHONDONTWRITEBYTECODE": "1"} +STORED_FLAVOR_LOG_ENV_VAR: str = "SQB_FLAVOR_CALL_LOG" + + +def write_external_modules(*, extlib: Path, files: dict[str, str]) -> None: + """Write, or rewrite in place, outside modules on the compile's import path.""" + + for relative_path, contents in files.items(): + path: Path = extlib / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(contents, encoding="utf-8") + + +def add_stored_flavor_macro( + *, project_dir: Path, extlib: Path, import_name: str, log_path: Path, engine: str +) -> dict[str, str]: + """Make fact orders call a logged macro that lazily imports `import_name`; return its env.""" + + write_project_file( + project_dir, + "models/marts/_sqlbuild/_macros/flavor.py", + '"""Flavor macros backed by an outside module."""\n\n\n' + "def flavor() -> str:\n" + ' """Return the configured flavor literal and log the call."""\n' + " import os\n\n" + f" import {import_name} as source\n\n" + f' with open(os.environ["{STORED_FLAVOR_LOG_ENV_VAR}"], "a", encoding="utf-8") as log:\n' + ' log.write("call\\n")\n' + " return source.VALUE\n", + ) + replace_project_text( + project_dir, FACT_ORDERS_MODEL, " o.quantity,\n", " o.quantity,\n @flavor() AS flavor,\n" + ) + log_path.write_text("", encoding="utf-8") + return { + "PYTHONPATH": str(extlib), + "PYTHONDONTWRITEBYTECODE": "1", + STORED_FLAVOR_LOG_ENV_VAR: str(log_path), + COMPILER_ENGINE_ENV_VAR: engine, + } + + +def logged_calls(log_path: Path) -> int: + """Return how many times a logged macro ran.""" + + return len(log_path.read_text(encoding="utf-8").splitlines()) + + def compiled_text(*, run: CompileReuseRun, suffix: str) -> str: """Return the compiled artifact whose path ends with the suffix.""" @@ -4481,7 +4528,7 @@ def macro_reference_call_runs( ) -MACRO_CALL_STORE_ENGINE: str = "native-preview" +MACRO_CALL_STORE_ENGINE: str = "native" STORE_ENVIRONMENT_REGION_VAR: str = "SQB_STORE_TEST_REGION" STORE_ARGUMENT_ENV_VAR: str = "STORE_TEST_ARGUMENT" _REUSE_DISABLED_VALUES: dict[bool, str] = {True: "0", False: "1"} diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_macro_call_store.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_macro_call_store.py index 11565105d4..2d610f33f0 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_macro_call_store.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_macro_call_store.py @@ -270,8 +270,8 @@ def test_given_broken_store_key_when_compiling_an_edit_then_the_oracle_reports_a EngineMacroCallGateTestCase( description="default_engine", engine="", - expected_logged_calls=(3, 3), - expected_store_files=(), + expected_logged_calls=(1, 0), + expected_store_files=("target/cache/compiler-native-v1/macro-calls.bin",), ), EngineMacroCallGateTestCase( description="python", @@ -282,8 +282,8 @@ def test_given_broken_store_key_when_compiling_an_edit_then_the_oracle_reports_a EngineMacroCallGateTestCase( description="native", engine="native", - expected_logged_calls=(3, 3), - expected_store_files=(), + expected_logged_calls=(1, 0), + expected_store_files=("target/cache/compiler-native-v1/macro-calls.bin",), ), EngineMacroCallGateTestCase( description="native_preview", @@ -294,7 +294,7 @@ def test_given_broken_store_key_when_compiling_an_edit_then_the_oracle_reports_a ], ids=lambda case: case.description, ) -def test_given_engine_when_compiling_repeatedly_then_only_preview_batches_and_stores_macro_calls( +def test_given_engine_when_compiling_repeatedly_then_only_native_engines_batch_and_store_macro_calls( tmp_path: Path, test_case: EngineMacroCallGateTestCase ) -> None: project_dir: Path = tmp_path / "project" @@ -323,8 +323,8 @@ def test_given_engine_when_compiling_repeatedly_then_only_preview_batches_and_st expected_logged_calls=(2, 2), ), MacroReferenceCallStoreTestCase( - description="native_preview_memo_then_store", - engine="native-preview", + description="native_memo_then_store", + engine="native", expected_logged_calls=(1, 0), ), ], @@ -466,10 +466,10 @@ def test_given_second_compile_in_one_process_when_compiling_then_the_store_is_no "test_case", [ EngineMacroCallGateTestCase( - description="native_preview_without_proc", - engine="native-preview", + description="native_without_proc", + engine="native", expected_logged_calls=(1, 0), - expected_store_files=("target/cache/compiler-native-preview-v1/macro-calls.bin",), + expected_store_files=("target/cache/compiler-native-v1/macro-calls.bin",), ) ], ids=lambda case: case.description, diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compiler_engine_switch.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compiler_engine_switch.py index 18eef94924..f8b5f2195f 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compiler_engine_switch.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compiler_engine_switch.py @@ -11,6 +11,7 @@ CompilerEngineParityTestCase, CompilerEngineRulesStoreTestCase, CompilerEngineStoreTestCase, + DefaultEngineParityTestCase, ) from tests.e2e.src.sqlbuild.cli.commands.main.compile.helpers import ( CompileReuseRun, @@ -22,6 +23,7 @@ report_engine, report_without_engine, store_digests, + store_files, ) from tests.integration.src.sqlbuild.compiler.compile.helpers import ( MACRO_BRIDGE_PROJECT_FILES, @@ -66,28 +68,33 @@ def test_given_same_project_when_compiling_with_each_engine_then_outputs_are_ide @pytest.mark.parametrize( "test_case", [ - CompilerEngineParityTestCase( + DefaultEngineParityTestCase( description="python_oracle_and_unset_default", - left_engine="python", - right_engine="", + oracle_engine="python", + default_engine="", expected_engines=("python", "native"), expected_exit_codes=(0, 0), + expected_macro_call_stores=((), ("target/cache/compiler-native-v1/macro-calls.bin",)), ) ], ids=lambda case: case.description, ) -def test_given_no_engine_selection_when_compiling_then_native_runs_and_matches_python( - test_case: CompilerEngineParityTestCase, tmp_path: Path +def test_given_no_engine_selection_when_compiling_then_native_rendering_runs_and_matches_python( + test_case: DefaultEngineParityTestCase, tmp_path: Path ) -> None: prepared_project: Path = tmp_path / "orders" prepare_compile_reuse_project(project_dir=prepared_project) + oracle_dir: Path = copy_compile_project( + source=prepared_project, destination=tmp_path / "oracle" + ) + default_dir: Path = copy_compile_project( + source=prepared_project, destination=tmp_path / "default" + ) oracle: CompileReuseRun = environment_engine_reuse_compile( - project_dir=copy_compile_project(source=prepared_project, destination=tmp_path / "oracle"), - engine=test_case.left_engine, + project_dir=oracle_dir, engine=test_case.oracle_engine ) default: CompileReuseRun = environment_engine_reuse_compile( - project_dir=copy_compile_project(source=prepared_project, destination=tmp_path / "default"), - engine=test_case.right_engine, + project_dir=default_dir, engine=test_case.default_engine ) assert (oracle.returncode, default.returncode) == test_case.expected_exit_codes @@ -95,6 +102,10 @@ def test_given_no_engine_selection_when_compiling_then_native_runs_and_matches_p assert report_without_engine(default) == report_without_engine(oracle) assert default.compiled == oracle.compiled assert oracle.compiled + assert ( + tuple(store_files(oracle_dir)), + tuple(store_files(default_dir)), + ) == test_case.expected_macro_call_stores @pytest.mark.parametrize( @@ -121,7 +132,7 @@ def test_given_macro_heavy_project_when_compiling_with_each_engine_then_outputs_ ) native_run: CompileReuseRun = engine_reuse_compile( project_dir=write_project(root=tmp_path / "native", files=test_case.files), - engine="native-preview", + engine="native", ) compiled_text: str = b"".join(python_run.compiled.values()).decode() diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_macro_store_module_edits.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_macro_store_module_edits.py new file mode 100644 index 0000000000..55a57621f3 --- /dev/null +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_macro_store_module_edits.py @@ -0,0 +1,113 @@ +"""Editing an outside module behind a stored macro call must not replay a stale compile.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.e2e.src.sqlbuild.cli.commands.main.compile._test_types import ( + StoredMacroModuleEditTestCase, +) +from tests.e2e.src.sqlbuild.cli.commands.main.compile.helpers import ( + CompileReuseRun, + IncrementalEditComparison, + add_stored_flavor_macro, + compiled_text, + logged_calls, + run_reuse_compile, + staging_comment, + write_external_modules, +) + +_MODULE: dict[str, str] = {"extflavor.py": "VALUE = \"'vanilla'\"\n"} +_EDITED_MODULE: dict[str, str] = {"extflavor.py": "VALUE = \"'choco'\"\n"} +_PACKAGE: dict[str, str] = { + "extpkg/__init__.py": "", + "extpkg/flavor.py": "from extpkg._values import VALUE\n", + "extpkg/_values.py": "VALUE = \"'vanilla'\"\n", +} +_EDITED_PACKAGE: dict[str, str] = {"extpkg/_values.py": "VALUE = \"'choco'\"\n"} + + +@pytest.mark.parametrize( + "test_case", + [ + StoredMacroModuleEditTestCase( + description="module_default", + engine="", + import_name="extflavor", + module_files=_MODULE, + edited_files=_EDITED_MODULE, + expected_compiled_value="'choco' AS flavor", + ), + StoredMacroModuleEditTestCase( + description="package_transitive_import_default", + engine="", + import_name="extpkg.flavor", + module_files=_PACKAGE, + edited_files=_EDITED_PACKAGE, + expected_compiled_value="'choco' AS flavor", + ), + StoredMacroModuleEditTestCase( + description="module_native_preview", + engine="native-preview", + import_name="extflavor", + module_files=_MODULE, + edited_files=_EDITED_MODULE, + expected_compiled_value="'choco' AS flavor", + ), + StoredMacroModuleEditTestCase( + description="package_transitive_import_native_preview", + engine="native-preview", + import_name="extpkg.flavor", + module_files=_PACKAGE, + edited_files=_EDITED_PACKAGE, + expected_compiled_value="'choco' AS flavor", + ), + ], + ids=lambda case: case.description, +) +def test_given_stored_call_reused_when_its_lazily_imported_module_changes_then_output_is_fresh( + compile_reuse_project: Path, tmp_path: Path, test_case: StoredMacroModuleEditTestCase +) -> None: + extlib: Path = tmp_path / "extlib" + log_path: Path = tmp_path / "flavor-calls.log" + write_external_modules(extlib=extlib, files=test_case.module_files) + env: dict[str, str] = add_stored_flavor_macro( + project_dir=compile_reuse_project, + extlib=extlib, + import_name=test_case.import_name, + log_path=log_path, + engine=test_case.engine, + ) + cold: CompileReuseRun = run_reuse_compile(project_dir=compile_reuse_project, env=env) + staging_comment(compile_reuse_project) + store_hit: CompileReuseRun = run_reuse_compile(project_dir=compile_reuse_project, env=env) + calls_before_module_edit: int = logged_calls(log_path) + + write_external_modules(extlib=extlib, files=test_case.edited_files) + comparison: IncrementalEditComparison = IncrementalEditComparison( + incremental=run_reuse_compile(project_dir=compile_reuse_project, env=env), + reference=run_reuse_compile( + project_dir=compile_reuse_project, env=env, args=("--no-cache",) + ), + ) + + assert ( + cold.returncode, + store_hit.returncode, + store_hit.reused, + calls_before_module_edit, + comparison.incremental.reused, + comparison.matches, + test_case.expected_compiled_value + in compiled_text(run=comparison.incremental, suffix="fact_orders.sql"), + ) == (0, 0, False, 1, False, True, True), ( + comparison.mismatched_artifacts, + store_hit.stderr, + ) + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_config_errors.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_config_errors.py index f1214edebd..3a2a19e347 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_config_errors.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_config_errors.py @@ -195,7 +195,7 @@ def test_given_config_error_when_compiling_with_preview_then_error_matches_pytho }, engines=("python", "native-preview", "native"), expected_report_text="references unknown model 'ghost'", - expected_macro_calls=[1, 2, 1], + expected_macro_calls=[1, 2, 2], ) ], ids=lambda case: case.description, diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_declaration_errors.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_declaration_errors.py index 90c7a90ed0..2edee79840 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_declaration_errors.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_declaration_errors.py @@ -1,4 +1,4 @@ -"""The preview engine reports declaration and scope errors as Python does, without a re-run.""" +"""Native engines report declaration and scope errors as Python does, without a re-run.""" from __future__ import annotations @@ -21,7 +21,7 @@ write_project_file, ) -_ENGINES: tuple[str, str] = ("python", "native-preview") +_ENGINES: tuple[str, ...] = ("python", "native", "native-preview") _CALL_LOG: str = "macro_calls.log" _COUNTED_MACRO: str = ( "from pathlib import Path\n\n\n" @@ -110,7 +110,7 @@ ], ids=lambda case: case.description, ) -def test_given_declaration_error_when_compiling_with_preview_then_error_matches_python_once( +def test_given_declaration_error_when_compiling_natively_then_error_matches_python_once( test_case: NativeDeclarationErrorTestCase, tmp_path: Path ) -> None: project_dir: Path = tmp_path / "orders" @@ -133,16 +133,18 @@ def test_given_declaration_error_when_compiling_with_preview_then_error_matches_ assert ( tuple(run.returncode for run in runs), - report_without_engine(runs[1]) == report_without_engine(runs[0]), - stderr_without_durations(stderr=runs[1].stderr) - == stderr_without_durations(stderr=runs[0].stderr), + {report_without_engine(run) for run in runs} == {report_without_engine(runs[0])}, + {stderr_without_durations(stderr=run.stderr) for run in runs} + == {stderr_without_durations(stderr=runs[0].stderr)}, test_case.expected_report_text in runs[0].report + runs[0].stderr, macro_calls, - ) == ((1, 1), True, True, True, [test_case.expected_macro_calls] * 2), ( - runs[0].report, - runs[1].report, - runs[1].stderr, - ) + ) == ( + (1,) * len(_ENGINES), + True, + True, + True, + [test_case.expected_macro_calls] * len(_ENGINES), + ), tuple(run.report + run.stderr for run in runs) @pytest.mark.parametrize( diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_config.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_config.py index dcab92987b..ba53f0d8dd 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_config.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_config.py @@ -1,4 +1,4 @@ -"""Native model config in the preview engine compiles exactly as the Python engine does.""" +"""Native model config in the default engine compiles exactly as the Python engine does.""" from __future__ import annotations @@ -80,7 +80,7 @@ project_files={"models/marts/order_quality.sql": _ORDER_QUALITY_MODEL}, project_config_replacements=(_TARGET_SCHEMA,), environment={"SQB_TARGET_SCHEMA": "analytics"}, - engines=("python", "native-preview"), + engines=("python", "native"), expected_exit_codes=(0, 0), expected_report_text="order_quality", ), @@ -89,14 +89,14 @@ project_files={"models/marts/order_quality.sql": _INVALID_SEVERITY_MODEL}, project_config_replacements=(), environment={}, - engines=("python", "native-preview"), + engines=("python", "native"), expected_exit_codes=(1, 1), expected_report_text="severity", ), ], ids=lambda case: case.description, ) -def test_given_authored_model_config_when_compiling_with_preview_then_output_matches_python( +def test_given_authored_model_config_when_compiling_natively_then_output_matches_python( test_case: NativeModelConfigParityTestCase, tmp_path: Path ) -> None: prepared_project: Path = tmp_path / "orders" diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_loop.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_loop.py index 5efb55def9..3cbf097d98 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_loop.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_model_loop.py @@ -1,4 +1,4 @@ -"""The preview engine's native model loop compiles exactly as the Python engine does.""" +"""The default engine's native model loop compiles exactly as the Python engine does.""" from __future__ import annotations @@ -49,14 +49,14 @@ **_MARTS_DECLARATIONS, "models/marts/order_flags.sql": _DECLARATION_MODEL, }, - engines=("python", "native-preview"), + engines=("python", "native"), expected_exit_codes=(0, 0), expected_report_text="order_flags", ), NativeModelLoopParityTestCase( description="python_identity_error_for_a_folder_containing_a_colon", project_files={**_DECLARATIONS, "models/a:b/order_flags.sql": _DECLARATION_MODEL}, - engines=("python", "native-preview"), + engines=("python", "native"), expected_exit_codes=(1, 1), expected_report_text="Unknown qualified identity kind", ), @@ -68,14 +68,14 @@ '__ref("stg_orders")', "__ref('stg_orders')" ), }, - engines=("python", "native-preview"), + engines=("python", "native"), expected_exit_codes=(1, 1), expected_report_text="P012", ), ], ids=lambda case: case.description, ) -def test_given_model_declarations_when_compiling_with_preview_then_output_matches_python( +def test_given_model_declarations_when_compiling_natively_then_output_matches_python( test_case: NativeModelLoopParityTestCase, tmp_path: Path ) -> None: prepared_project: Path = tmp_path / "orders" diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_reference_errors.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_reference_errors.py index 48346c0cf5..c58998d27b 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_reference_errors.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_native_reference_errors.py @@ -104,7 +104,7 @@ expected_message=( f"{FAILURE_MART_PATH}: SQL reference contains an unclosed parenthesis" ), - expected_macro_calls=(1, 1, 2), + expected_macro_calls=(1, 2, 2), expected_error_types=("CompileInputError",) * 3, ), ReferenceScanErrorTestCase( diff --git a/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py b/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py index 8dd34cf57c..5b98ac444d 100644 --- a/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py @@ -39,7 +39,7 @@ class AttachmentProjectTestCase: description: str overrides: dict[str, str] - expected_preview_entries: frozenset[str] + expected_native_entries: frozenset[str] expected_outcome_fragment: str diff --git a/tests/integration/src/sqlbuild/compiler/attachments/test_native_attachment_project.py b/tests/integration/src/sqlbuild/compiler/attachments/test_native_attachment_project.py index 30db6f6ad9..e273b3d91e 100644 --- a/tests/integration/src/sqlbuild/compiler/attachments/test_native_attachment_project.py +++ b/tests/integration/src/sqlbuild/compiler/attachments/test_native_attachment_project.py @@ -1,4 +1,4 @@ -"""Every attachment kind compiles like Python under preview, with macros through the bridge.""" +"""Every attachment kind compiles like Python natively, with macros through the bridge.""" from __future__ import annotations @@ -26,7 +26,7 @@ "bridged:/tests/unit/test_orders.sql", } ) -_PREVIEW_ONLY_ENTRIES: frozenset[str] = ( +_NATIVE_ONLY_ENTRIES: frozenset[str] = ( frozenset( { "pair_seed_files", @@ -69,7 +69,7 @@ AttachmentProjectTestCase( description="macros in every attachment kind, seeds, templates and generic audits", overrides={}, - expected_preview_entries=_ATTACHMENT_ENTRIES | _BRIDGED_CONSUMERS, + expected_native_entries=_ATTACHMENT_ENTRIES | _BRIDGED_CONSUMERS, expected_outcome_fragment="((CompileSeedInput(", ), AttachmentProjectTestCase( @@ -79,7 +79,7 @@ + "WITH\n__seed__channel_codes AS (SELECT 1 AS id, 'web' AS label),\n" + '__expected__orders AS (SELECT id, 1.5 AS amount, label FROM __source("order_events"))\n' }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="error: SQL scenario file tests/scenarios/orders_scenario.sql CTE", ), AttachmentProjectTestCase( @@ -90,7 +90,7 @@ + "__seed__channel_codes AS (SELECT 1 AS id, 'web' AS label),\n" + "__expected__orders AS (SELECT 1 AS id, 1.5 AS amount, 'web' AS label)\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="error: SQL scenario file tests/scenarios/orders_scenario.sql " "references unknown source 'missing_events'", ), @@ -102,7 +102,7 @@ + _HELPER_TEST_EXPECTED + ",\n__assert__has_rows AS (SELECT 1 FROM built_orders WHERE id IS NULL)\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="reference_target_model_names=('orders',)", ), AttachmentProjectTestCase( @@ -114,7 +114,7 @@ + _HELPER_TEST_EXPECTED + ",\n__assert__has_ids AS (SELECT 1 FROM built_ids WHERE id IS NULL)\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="reference_target_model_names=('orders',)", ), AttachmentProjectTestCase( @@ -125,7 +125,7 @@ + _HELPER_TEST_EXPECTED + "\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="read_helper_names=(), reference_target_model_names=()", ), AttachmentProjectTestCase( @@ -136,7 +136,7 @@ + _HELPER_TEST_EXPECTED + ",\n__assert__no_returns AS (SELECT 1 FROM built_returns)\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="P013", ), AttachmentProjectTestCase( @@ -145,14 +145,14 @@ _UNIT_TEST: "TEST();\n\nWITH\n__seed__channel_codes AS (SELECT 1 AS id),\n" "__expected__returns AS (SELECT 1 AS id)\n" }, - expected_preview_entries=frozenset({"SqlTestTargetCatalog"}), + expected_native_entries=frozenset({"SqlTestTargetCatalog"}), expected_outcome_fragment="error: SQL test file tests/unit/test_orders.sql expects " "unknown model 'returns'", ), ], ids=lambda case: case.description, ) -def test_given_attachment_project_when_building_inputs_then_preview_matches_python( +def test_given_attachment_project_when_building_inputs_then_native_engines_match_python( test_case: AttachmentProjectTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: outcomes: dict[CompilerEngine, tuple[frozenset[str], str]] = { @@ -172,9 +172,10 @@ def test_given_attachment_project_when_building_inputs_then_preview_matches_pyth preview_outcome.replace(CompilerEngine.NATIVE_PREVIEW.value, "python"), native_outcome.replace(CompilerEngine.NATIVE.value, "python"), test_case.expected_outcome_fragment in python_outcome, - preview_called >= test_case.expected_preview_entries, - (python_called | native_called) & _PREVIEW_ONLY_ENTRIES, - ) == (python_outcome, python_outcome, True, True, frozenset()), test_case.description + native_called >= test_case.expected_native_entries, + preview_called >= test_case.expected_native_entries, + python_called & _NATIVE_ONLY_ENTRIES, + ) == (python_outcome, python_outcome, True, True, True, frozenset()), test_case.description if __name__ == "__main__": diff --git a/tests/integration/src/sqlbuild/compiler/compile/_helpers/refs/test_native_references.py b/tests/integration/src/sqlbuild/compiler/compile/_helpers/refs/test_native_references.py index 7f7068041e..3aa18da59a 100644 --- a/tests/integration/src/sqlbuild/compiler/compile/_helpers/refs/test_native_references.py +++ b/tests/integration/src/sqlbuild/compiler/compile/_helpers/refs/test_native_references.py @@ -263,7 +263,7 @@ def test_given_generated_reference_sql_when_extracting_natively_then_matches_pyt sql=sql, expected_references=expected_references, expected_error=expected_error, - expected_native_calls=int(engine is CompilerEngine.NATIVE_PREVIEW), + expected_native_calls=int(engine is not CompilerEngine.PYTHON), ) for engine, (name, sql, expected_references, expected_error) in product( CompilerEngine, _ENGINE_SQLS @@ -271,7 +271,7 @@ def test_given_generated_reference_sql_when_extracting_natively_then_matches_pyt ], ids=lambda case: case.description, ) -def test_given_engine_when_extracting_references_then_only_preview_runs_native_scanner( +def test_given_engine_when_extracting_references_then_only_native_engines_run_native_scanner( test_case: ReferenceEngineTestCase, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv(COMPILER_ENGINE_ENV_VAR, test_case.engine.value) diff --git a/tests/integration/src/sqlbuild/compiler/discovery/test_native_declaration_files.py b/tests/integration/src/sqlbuild/compiler/discovery/test_native_declaration_files.py index 72eb243627..217af86f42 100644 --- a/tests/integration/src/sqlbuild/compiler/discovery/test_native_declaration_files.py +++ b/tests/integration/src/sqlbuild/compiler/discovery/test_native_declaration_files.py @@ -473,9 +473,9 @@ def test_given_native_declaration_failure_when_discovering_then_python_is_not_re expected_session=True, ), NativeSessionTestCase( - description="the shipped engine reads declaration files in Python", + description="the shipped engine keeps its declaration session", engine="native", - expected_session=False, + expected_session=True, ), NativeSessionTestCase( description="the Python engine reads declaration files in Python", @@ -485,7 +485,7 @@ def test_given_native_declaration_failure_when_discovering_then_python_is_not_re ], ids=lambda case: case.description, ) -def test_given_engine_when_discovering_then_native_session_is_kept_only_for_preview( +def test_given_engine_when_discovering_then_native_session_is_kept_only_for_native_engines( test_case: NativeSessionTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: write_project(project_dir=tmp_path, files=_EVERY_KIND) diff --git a/tests/integration/src/sqlbuild/compiler/model_config/test_native_model_config.py b/tests/integration/src/sqlbuild/compiler/model_config/test_native_model_config.py index 654831c5f7..7104829642 100644 --- a/tests/integration/src/sqlbuild/compiler/model_config/test_native_model_config.py +++ b/tests/integration/src/sqlbuild/compiler/model_config/test_native_model_config.py @@ -161,11 +161,11 @@ def test_given_generated_templates_when_expanding_then_native_matches_python_or_ }, ), ModelConfigTierTestCase( - description="shipped native stages only", + description="shipped native stages", engine="native", expected_native_calls={ - "parse_model_header_metadata": 0, - "expand_config_templates": 0, + "parse_model_header_metadata": 1, + "expand_config_templates": 1, "config_contains_template": 0, "config_contains_macro_call": 0, }, @@ -183,7 +183,7 @@ def test_given_generated_templates_when_expanding_then_native_matches_python_or_ ], ids=lambda case: case.description, ) -def test_given_engine_tier_when_building_model_inputs_then_native_config_runs_only_in_preview( +def test_given_engine_tier_when_building_model_inputs_then_native_config_runs_only_in_native_engines( test_case: ModelConfigTierTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: calls, config = model_config_engine_outcome( diff --git a/tests/integration/src/sqlbuild/compiler/scopes/_test_types.py b/tests/integration/src/sqlbuild/compiler/scopes/_test_types.py index 39506a0b8f..ff55531470 100644 --- a/tests/integration/src/sqlbuild/compiler/scopes/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/scopes/_test_types.py @@ -37,7 +37,7 @@ class ScopeCommandParityTestCase: @dataclass(frozen=True) class ScopeEngineTierTestCase: - """One compiler engine and whether it runs the preview-tier native declaration scopes.""" + """One compiler engine and whether it runs the native declaration scopes.""" description: str engine: str diff --git a/tests/integration/src/sqlbuild/compiler/scopes/test_native_scope_parity.py b/tests/integration/src/sqlbuild/compiler/scopes/test_native_scope_parity.py index db93b64bc9..caa7791b59 100644 --- a/tests/integration/src/sqlbuild/compiler/scopes/test_native_scope_parity.py +++ b/tests/integration/src/sqlbuild/compiler/scopes/test_native_scope_parity.py @@ -170,7 +170,7 @@ def test_given_scoped_project_when_scope_command_builds_index_then_engines_match description="python oracle", engine="python", expected_native_attempts=0 ), ScopeEngineTierTestCase( - description="shipped native stages only", engine="native", expected_native_attempts=0 + description="shipped native stages", engine="native", expected_native_attempts=1 ), ScopeEngineTierTestCase( description="native preview", engine="native-preview", expected_native_attempts=1 @@ -178,7 +178,7 @@ def test_given_scoped_project_when_scope_command_builds_index_then_engines_match ], ids=lambda case: case.description, ) -def test_given_engine_tier_when_building_scope_then_native_scopes_run_only_in_preview( +def test_given_engine_tier_when_building_scope_then_native_scopes_run_only_in_native_engines( test_case: ScopeEngineTierTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: write_project(project_dir=tmp_path, files=SCOPED_PROJECT) diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/config/helpers.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/config/helpers.py index 40cc04fca8..73fedc0eee 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/config/helpers.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/config/helpers.py @@ -11,6 +11,8 @@ from sqlbuild.compiler.compile.models import CompileModelConfig, CompileProjectInputs from sqlbuild.compiler.discovery.main.discover import discover_project_inputs from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs +from sqlbuild.compiler.frontier.constants import COMPILER_ENGINE_ENV_VAR +from sqlbuild.compiler.frontier.types import CompilerEngine from sqlbuild.spec.contracts.models import SchemaColumn from tests.unit.src.sqlbuild.compiler.compile._helpers.helpers import ( DUCKDB_COMPILE_ADAPTER_CONTEXT, @@ -22,8 +24,9 @@ def compile_with_config_build_count( project_dir: Path, monkeypatch: pytest.MonkeyPatch, ) -> tuple[CompileProjectInputs, int]: - """Compile a project while counting effective model configuration builds.""" + """Compile a project on the Python engine while counting its model configuration builds.""" + monkeypatch.setenv(COMPILER_ENGINE_ENV_VAR, CompilerEngine.PYTHON.value) build_counts: list[int] = [0] original_build_model_config: Callable[..., CompileModelConfig] = ( attachment_core.build_model_config diff --git a/tests/unit/src/sqlbuild/compiler/frontier/_test_types.py b/tests/unit/src/sqlbuild/compiler/frontier/_test_types.py index 8ce2f09dc2..04b982d897 100644 --- a/tests/unit/src/sqlbuild/compiler/frontier/_test_types.py +++ b/tests/unit/src/sqlbuild/compiler/frontier/_test_types.py @@ -124,3 +124,12 @@ class NativeStageTierTestCase: engine: CompilerEngine stage: NativeStage expected_enabled: bool + + +@dataclass(frozen=True) +class DefaultEngineStageTestCase: + """With no engine selected, the native stages the default compile runs and those it skips.""" + + description: str + expected_enabled: frozenset[NativeStage] + expected_disabled: frozenset[NativeStage] diff --git a/tests/unit/src/sqlbuild/compiler/frontier/test_native_stage_tiers.py b/tests/unit/src/sqlbuild/compiler/frontier/test_native_stage_tiers.py index 8e6f7793a2..d7ef9561bb 100644 --- a/tests/unit/src/sqlbuild/compiler/frontier/test_native_stage_tiers.py +++ b/tests/unit/src/sqlbuild/compiler/frontier/test_native_stage_tiers.py @@ -7,7 +7,10 @@ from sqlbuild.compiler.frontier.constants import COMPILER_ENGINE_ENV_VAR from sqlbuild.compiler.frontier.main.native_stage_enabled import native_stage_enabled from sqlbuild.compiler.frontier.types import CompilerEngine, NativeStage -from tests.unit.src.sqlbuild.compiler.frontier._test_types import NativeStageTierTestCase +from tests.unit.src.sqlbuild.compiler.frontier._test_types import ( + DefaultEngineStageTestCase, + NativeStageTierTestCase, +) @pytest.mark.parametrize( @@ -35,7 +38,7 @@ description="native_declaration_scopes", engine=CompilerEngine.NATIVE, stage=NativeStage.DECLARATION_SCOPES, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_discovery", @@ -49,6 +52,24 @@ stage=NativeStage.DECLARATION_SCOPES, expected_enabled=True, ), + NativeStageTierTestCase( + description="python_model_config", + engine=CompilerEngine.PYTHON, + stage=NativeStage.MODEL_CONFIG, + expected_enabled=False, + ), + NativeStageTierTestCase( + description="native_model_config", + engine=CompilerEngine.NATIVE, + stage=NativeStage.MODEL_CONFIG, + expected_enabled=True, + ), + NativeStageTierTestCase( + description="native_preview_model_config", + engine=CompilerEngine.NATIVE_PREVIEW, + stage=NativeStage.MODEL_CONFIG, + expected_enabled=True, + ), NativeStageTierTestCase( description="python_reference_extraction", engine=CompilerEngine.PYTHON, @@ -59,7 +80,7 @@ description="native_reference_extraction", engine=CompilerEngine.NATIVE, stage=NativeStage.REFERENCE_EXTRACTION, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_reference_extraction", @@ -77,7 +98,7 @@ description="native_declaration_files", engine=CompilerEngine.NATIVE, stage=NativeStage.DECLARATION_FILES, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_declaration_files", @@ -95,7 +116,7 @@ description="native_model_loop", engine=CompilerEngine.NATIVE, stage=NativeStage.MODEL_LOOP, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_model_loop", @@ -119,13 +140,13 @@ description="native_macro_calls", engine=CompilerEngine.NATIVE, stage=NativeStage.MACRO_CALLS, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_macro_call_store", engine=CompilerEngine.NATIVE, stage=NativeStage.MACRO_CALL_STORE, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_macro_calls", @@ -149,7 +170,7 @@ description="native_attachments", engine=CompilerEngine.NATIVE, stage=NativeStage.ATTACHMENTS, - expected_enabled=False, + expected_enabled=True, ), NativeStageTierTestCase( description="native_preview_attachments", @@ -294,5 +315,49 @@ def test_given_engine_when_checking_native_stage_then_only_its_tiers_run( assert native_stage_enabled(test_case.stage) is test_case.expected_enabled +@pytest.mark.parametrize( + "test_case", + [ + DefaultEngineStageTestCase( + description="discovery_and_rendering_native_analysis_python", + expected_enabled=frozenset( + { + NativeStage.DISCOVERY, + NativeStage.DECLARATION_SCOPES, + NativeStage.MODEL_CONFIG, + NativeStage.REFERENCE_EXTRACTION, + NativeStage.DECLARATION_FILES, + NativeStage.MODEL_LOOP, + NativeStage.MACRO_CALLS, + NativeStage.MACRO_CALL_STORE, + NativeStage.ATTACHMENTS, + } + ), + expected_disabled=frozenset( + { + NativeStage.TYPE_SYSTEM, + NativeStage.MODEL_ANALYSIS, + NativeStage.SEMANTIC_CHECKS, + NativeStage.CONTRACTS, + NativeStage.LINEAGE_FACTS, + NativeStage.SQL_TEST_GLUE, + NativeStage.PROJECT_ASSEMBLY, + } + ), + ) + ], + ids=lambda case: case.description, +) +def test_given_no_engine_selection_when_checking_native_stages_then_rendering_runs_natively( + test_case: DefaultEngineStageTestCase, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.delenv(COMPILER_ENGINE_ENV_VAR, raising=False) + + assert {stage: native_stage_enabled(stage) for stage in NativeStage} == { + **dict.fromkeys(test_case.expected_enabled, True), + **dict.fromkeys(test_case.expected_disabled, False), + } + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) diff --git a/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/helpers.py b/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/helpers.py index 34953aa1bc..3058392ef8 100644 --- a/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/helpers.py +++ b/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/helpers.py @@ -7,6 +7,7 @@ import sys import types import zipfile +import zipimport from dataclasses import replace from importlib.machinery import ModuleSpec from importlib.util import module_from_spec, spec_from_file_location @@ -110,6 +111,28 @@ def zip_member_module(root: Path) -> object: return _located_module("zipped_flavor", str(archive / "zipped_flavor.py")) +def launcher_main_module(root: Path) -> object: + """The entry point of a console-script launcher that runs `__main__` from its own zip.""" + + archive: Path = root / "sqb.exe" + with zipfile.ZipFile(archive, "w") as bundle: + bundle.writestr("__main__.py", "import sys\n") + spec: ModuleSpec | None = zipimport.zipimporter(str(archive)).find_spec("__main__") + assert spec is not None + return module_from_spec(spec) + + +def zipped_module(root: Path) -> object: + """A module other than `__main__` imported from a zip archive on the import path.""" + + archive: Path = root / "flavors.zip" + with zipfile.ZipFile(archive, "w") as bundle: + bundle.writestr("zipped_flavor.py", "VALUE = 1\n") + spec: ModuleSpec | None = zipimport.zipimporter(str(archive)).find_spec("zipped_flavor") + assert spec is not None + return module_from_spec(spec) + + def _located_module(name: str, origin: str) -> object: spec: ModuleSpec = ModuleSpec(name, None, origin=origin) spec.has_location = True diff --git a/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/test_store_environment.py b/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/test_store_environment.py index 30e76f55d3..779bc9e9b7 100644 --- a/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/test_store_environment.py +++ b/tests/unit/src/sqlbuild/compiler/macro_bridge/_helpers/test_store_environment.py @@ -24,6 +24,7 @@ environment_of, file_module, interpreter_module, + launcher_main_module, moved_module, removed_module, sqlbuild_module, @@ -32,6 +33,7 @@ write_environment_project, write_module, zip_member_module, + zipped_module, ) @@ -186,6 +188,20 @@ def test_given_environment_variable_change_when_digesting_store_environment_then expected_complete=True, expected_digestible=False, ), + ModuleSourcesTestCase( + description="module imported from a zip archive", + module=zipped_module, + expected_paths=lambda root: (str(root / "flavors.zip" / "zipped_flavor.py"),), + expected_complete=True, + expected_digestible=False, + ), + ModuleSourcesTestCase( + description="launcher entry point run from the launcher's own zip", + module=launcher_main_module, + expected_paths=lambda root: (str(root / "sqb.exe"),), + expected_complete=True, + expected_digestible=True, + ), ModuleSourcesTestCase( description="file moved after loading", module=moved_module, diff --git a/website/src/content/docs/docs/cli/compile.mdx b/website/src/content/docs/docs/cli/compile.mdx index f1be1b4fac..802b00f36e 100644 --- a/website/src/content/docs/docs/cli/compile.mdx +++ b/website/src/content/docs/docs/cli/compile.mdx @@ -120,8 +120,8 @@ 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. @@ -129,7 +129,7 @@ cache. A few caches under `target/cache/` shorten the rest: 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](/docs/concepts/macros#macros-must-be-deterministic). An edit therefore costs a full render of the project plus the analysis, test scans, and rules of diff --git a/website/src/content/docs/docs/concepts/macros.mdx b/website/src/content/docs/docs/concepts/macros.mdx index a79d1fcfff..24cc009ba3 100644 --- a/website/src/content/docs/docs/concepts/macros.mdx +++ b/website/src/content/docs/docs/concepts/macros.mdx @@ -215,7 +215,8 @@ SQLBuild relies on this contract to reuse macro results: - [Compile reuse](/docs/cli/compile) 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