From 26fac039e7f36267737f6fa562f4fe1060ab87a6 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:19:20 +0100 Subject: [PATCH 1/6] feat(native): make native rendering the default compiler path With SQLBUILD_COMPILER_ENGINE unset, compiles now run declaration files and scopes, model config, reference extraction, the model loop, macro calls with the macro-call store, and attachments natively. A failing native render stage still re-runs Python for the exact error. SQLBUILD_COMPILER_ENGINE=python remains the oracle; SQL analysis and later stages stay in the preview tier. Tests that assumed only native-preview ran these stages now expect the shipped engine to run them, and an E2E test proves the unset default renders natively, writes the macro-call store and matches Python. CI runs the shipped differential with stage captures and full discovery and render coverage, compares python with native on Python 3.13 and 3.14, and checks on Windows that the default compiles a playground project to the same files as Python. --- .github/workflows/verify.yml | 23 ++++-- Makefile | 2 +- crates/README.md | 15 +++- .../references/docs/concepts/macros.md | 3 +- src/sqlbuild/compiler/frontier/constants.py | 16 ++-- .../main/build/test_model_config_engines.py | 2 +- .../cli/commands/main/compile/_test_types.py | 12 +++ .../cli/commands/main/compile/helpers.py | 2 +- .../compile/test_compile_macro_call_store.py | 20 ++--- .../compile/test_compiler_engine_switch.py | 31 ++++--- .../main/compile/test_native_model_config.py | 8 +- .../main/compile/test_native_model_loop.py | 10 +-- .../compiler/attachments/_test_types.py | 2 +- .../test_native_attachment_project.py | 29 +++---- .../_helpers/refs/test_native_references.py | 4 +- .../test_native_declaration_files.py | 6 +- .../model_config/test_native_model_config.py | 8 +- .../sqlbuild/compiler/scopes/_test_types.py | 2 +- .../scopes/test_native_scope_parity.py | 4 +- .../compile/_helpers/config/helpers.py | 5 +- .../sqlbuild/compiler/frontier/_test_types.py | 9 +++ .../frontier/test_native_stage_tiers.py | 81 +++++++++++++++++-- .../src/content/docs/docs/concepts/macros.mdx | 3 +- 23 files changed, 212 insertions(+), 85 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index a5a48b0136..9b552db35e 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,16 @@ 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" + test -f "${project}/target/cache/compiler-native-v1/macro-calls.bin" 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}" 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/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/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/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 bc3989c839..f833189f41 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 @@ -792,6 +792,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/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py index 0acd2a0030..d402007a11 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py @@ -4480,7 +4480,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_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/integration/src/sqlbuild/compiler/attachments/_test_types.py b/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py index 7e47c866a5..b62afb0474 100644 --- a/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/attachments/_test_types.py @@ -37,7 +37,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 e2940592d7..c0cf8d7b91 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 @@ -247,7 +247,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 @@ -255,7 +255,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 a778c0f5a7..9d5253425c 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 @@ -163,11 +163,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, }, @@ -185,7 +185,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/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 From 38fc7a0eaf32b3066ecefcb74cdf80aee99cbdf2 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:39:48 +0100 Subject: [PATCH 2/6] fix(compile): track module files behind reused macro calls for compile reuse With the macro-call store on by default, a compile that reuses a stored call never runs the macro, so a module the macro imports while running is not loaded and whole-project reuse stopped tracking it. Editing that module then replayed a stale compile. Compile reuse now also stamps the module files the stored call results depend on. Also: the compile docs no longer say an edited model recompiles uncached, the one-model edit performance guard returns to the shared ratio now that rendering is native, and the native declaration error E2E test covers the shipped engine. --- .github/workflows/verify.yml | 6 ----- .../sqlbuild/references/docs/cli/compile.md | 6 ++--- .../_helpers/runtime_identity.py | 12 ++++++---- .../cli/compile_reuse/_helpers/store.py | 6 ++++- .../macro_bridge/classes/macro_bridge.py | 5 ++++ .../compiler/macro_bridge/constants.py | 1 + .../main/macro_store_module_paths.py | 11 +++++++++ .../main/run_with_macro_bridge.py | 3 ++- .../compile/test_native_declaration_errors.py | 24 ++++++++++--------- website/src/content/docs/docs/cli/compile.mdx | 6 ++--- 10 files changed, 51 insertions(+), 29 deletions(-) create mode 100644 src/sqlbuild/compiler/macro_bridge/main/macro_store_module_paths.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 9b552db35e..3a7d111a5b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -574,15 +574,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 @@ -613,13 +609,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/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/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/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..38006ac8a5 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" 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/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/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 From 511ef93ff656135c7fb7f3a33f6dd17651903b82 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:03:10 +0100 Subject: [PATCH 3/6] test(compile): prove edits to modules behind stored macro calls never replay stale output A macro lazily imports an outside module, or a package module with a transitive import; a compile after an unrelated model edit reuses the stored call, then the module is edited. The next compile must not replay and must match --no-cache, on the default engine and native-preview. Fails without the module tracking fix. --- .../cli/commands/main/compile/_test_types.py | 12 ++ .../cli/commands/main/compile/helpers.py | 47 ++++++++ .../compile/test_macro_store_module_edits.py | 113 ++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 tests/e2e/src/sqlbuild/cli/commands/main/compile/test_macro_store_module_edits.py 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 f833189f41..97edcf907c 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.""" 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 d402007a11..b6312fef87 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py @@ -4276,6 +4276,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.""" 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"]) From fb1fcc2fce0323cd98921dc4e4f2dec888fc501e Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:10:54 +0100 Subject: [PATCH 4/6] fix(native): keep the macro-call store when the CLI runs from a zip launcher uv's Windows console-script launcher runs the entry point as __main__ from a zip appended to sqb.exe. Its location, sqb.exe\__main__.py, is not a readable file, so the store treated the process as running unknown code and was never used or saved on Windows. A __main__ loaded by zipimport is now backed by its archive, the launcher executable. Other zip-imported modules still disable the store. The Windows CI step prints the macro bridge debug log when the store is missing. --- .github/workflows/verify.yml | 8 ++++++- .../_helpers/store_environment.py | 16 +++++++++++++ .../compiler/macro_bridge/constants.py | 1 + .../compiler/macro_bridge/_helpers/helpers.py | 23 +++++++++++++++++++ .../_helpers/test_store_environment.py | 16 +++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3a7d111a5b..c9ac5ef1e7 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -406,7 +406,13 @@ jobs: 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" - test -f "${project}/target/cache/compiler-native-v1/macro-calls.bin" + 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}" 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/constants.py b/src/sqlbuild/compiler/macro_bridge/constants.py index 38006ac8a5..89f27cde8e 100644 --- a/src/sqlbuild/compiler/macro_bridge/constants.py +++ b/src/sqlbuild/compiler/macro_bridge/constants.py @@ -35,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/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, From 4ba709e4316ca81d012beb6e115b6cd08f68cae1 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:42:11 +0100 Subject: [PATCH 5/6] test(native): expect the default engine to re-run failing macro stages like preview Until the macro bridge stops re-running failing stages, the default engine re-runs macros the same way native-preview does. --- .../compile/attachment_errors/test_native_attachment_errors.py | 2 +- .../cli/commands/main/compile/test_native_config_errors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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, From 6285f0f81f97f626b33f5a6adec4e322846a7884 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 9 Oct 2026 00:00:25 +0100 Subject: [PATCH 6/6] test(native): expect the default engine to re-run unlocated reference errors like preview --- .../cli/commands/main/compile/test_native_reference_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(