From 524ad11b640a9d33de08f98788e1ce22f68f2019 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:57:33 +0100 Subject: [PATCH 1/4] fix: preserve no-cache compile performance --- .../_helpers/compile/target_writer.py | 6 ++-- .../compile/_helpers/assembly/project.py | 32 ++++++++++--------- .../test_fresh_process_compile_performance.py | 6 ++-- .../compile/test_compile_target_writer.py | 7 ++++ .../compile/_helpers/test_analysis_cache.py | 7 ++++ 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/sqlbuild/cli/commands/_helpers/compile/target_writer.py b/src/sqlbuild/cli/commands/_helpers/compile/target_writer.py index 55e46f548b..4f52b6f1dc 100644 --- a/src/sqlbuild/cli/commands/_helpers/compile/target_writer.py +++ b/src/sqlbuild/cli/commands/_helpers/compile/target_writer.py @@ -332,12 +332,12 @@ def _write_static_tests( if project.compile_cache_dir is not None else None ) - model_chain_names_by_key: dict[CompiledObjectKey, tuple[str, ...]] = ( - sql_test_model_chain_names_by_key( + model_chain_names_by_key: dict[CompiledObjectKey, tuple[str, ...]] = {} + if identity_context is not None: + model_chain_names_by_key = sql_test_model_chain_names_by_key( project=project, tests=project.sql_tests, ) - ) pending: list[tuple[CompiledSqlTest, str | None, str | None]] = [] for test in project.sql_tests: record_key: str | None = None diff --git a/src/sqlbuild/compiler/compile/_helpers/assembly/project.py b/src/sqlbuild/compiler/compile/_helpers/assembly/project.py index 416e734feb..822adf065a 100644 --- a/src/sqlbuild/compiler/compile/_helpers/assembly/project.py +++ b/src/sqlbuild/compiler/compile/_helpers/assembly/project.py @@ -536,21 +536,23 @@ def _analyze_model_sql_in_parallel( request_cache_keys: tuple[str, ...] = tuple( request.cache_key for request in requests if request.cache_key is not None ) - compact_batch_plan: CompactAnalysisCachePlan | None = build_compact_analysis_cache_plan( - context=analysis_cache, - models=tuple( - CompactAnalysisCacheModel( - name=_model_name(request.model_input), - cache_key=request.cache_key, - upstream_names=_referenced_model_names( - model_input=request.model_input, - available_names=analyzed_model_names, - ), - ) - for request in requests - ), - min_model_count=_COMPACT_BATCH_CACHE_MIN_MODEL_COUNT, - ) + compact_batch_plan: CompactAnalysisCachePlan | None = None + if analysis_cache is not None: + compact_batch_plan = build_compact_analysis_cache_plan( + context=analysis_cache, + models=tuple( + CompactAnalysisCacheModel( + name=_model_name(request.model_input), + cache_key=request.cache_key, + upstream_names=_referenced_model_names( + model_input=request.model_input, + available_names=analyzed_model_names, + ), + ) + for request in requests + ), + min_model_count=_COMPACT_BATCH_CACHE_MIN_MODEL_COUNT, + ) compact_cached_analyses: dict[str, PolyglotAnalysisResult] = {} compact_batch_hit_count: int = 0 compact_candidate: CompactAnalysisCacheCandidate | None = ( diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py index 0da365cc8e..c04649f21c 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py @@ -36,7 +36,7 @@ macro_count=37, test_count=2_945, audit_count=5_056, - expected_max_wall_seconds=15.0, + expected_max_wall_seconds=13.0, expected_max_rss_bytes=2 * _GIB, expected_semantic_fingerprint=( "02af81fb7723d30ec5082ed06fb039e45acba3d60b2d8a30fcbccb5a990bb9fc" @@ -51,7 +51,7 @@ macro_count=61, test_count=4_908, audit_count=8_427, - expected_max_wall_seconds=20.0, + expected_max_wall_seconds=17.0, expected_max_rss_bytes=2 * _GIB, expected_semantic_fingerprint=( "0c9258635fe4f9d31ec2dd1c99dbc63f5c8551ca7fea494003c5ab1c3a2ad57c" @@ -66,7 +66,7 @@ macro_count=123, test_count=9_816, audit_count=16_855, - expected_max_wall_seconds=38.0, + expected_max_wall_seconds=27.0, expected_max_rss_bytes=2 * _GIB, expected_semantic_fingerprint=( "6887ddd2547137efd8fe67d13fb07142f6287c0911d152d69eca0d495310e02b" diff --git a/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_target_writer.py b/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_target_writer.py index e235c99713..9915a6b782 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_target_writer.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_target_writer.py @@ -500,6 +500,12 @@ def test_given_compile_cache_disabled_when_writing_twice_then_rebuilds_test_arti ) planner_spy: Mock = Mock(wraps=target_writer_module.plan_and_render_sql_test_artifacts) monkeypatch.setattr(target_writer_module, "plan_and_render_sql_test_artifacts", planner_spy) + closure_index_spy: Mock = Mock(wraps=target_writer_module.sql_test_model_chain_names_by_key) + monkeypatch.setattr( + target_writer_module, + "sql_test_model_chain_names_by_key", + closure_index_spy, + ) for _ in range(2): _ = write_static_compile_target( @@ -509,6 +515,7 @@ def test_given_compile_cache_disabled_when_writing_twice_then_rebuilds_test_arti ) assert planner_spy.call_count == test_case.expected_builder_calls + assert closure_index_spy.call_count == 0 assert not (target_dir / "cache" / "compiler" / "sql-test-artifacts.json").exists() diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_analysis_cache.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_analysis_cache.py index dfa3f3f307..b37ea2b430 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_analysis_cache.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_analysis_cache.py @@ -855,12 +855,19 @@ def test_given_compile_cache_bypass_when_compiling_twice_then_both_runs_analyze_ monkeypatch.setenv(COMPILE_CACHE_DISABLE_ENV_VAR, "1") analyzer: Mock = Mock(wraps=assembly_project.analyze_columns_and_lineage_with_polyglot) monkeypatch.setattr(assembly_project, "analyze_columns_and_lineage_with_polyglot", analyzer) + batch_plan_spy: Mock = Mock(wraps=assembly_project.build_compact_analysis_cache_plan) + monkeypatch.setattr( + assembly_project, + "build_compact_analysis_cache_plan", + batch_plan_spy, + ) _ = compile_project_with_cache(project_dir=tmp_path) with collect_compile_timings() as metrics: _ = compile_project_with_cache(project_dir=tmp_path) assert analyzer.call_count == test_case.expected_count + assert batch_plan_spy.call_count == 0 assert not tuple((tmp_path / "target").rglob("*.sqlite3")) recorded: dict[str, int] = metrics.as_milliseconds() assert recorded["analysis_batch_cache_hits"] == 0 From b7e3607023bd532a5e5992bfec64d52d4aa570a9 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:21:58 +0100 Subject: [PATCH 2/4] perf: reduce cold compile overhead --- .../cli/commands/_helpers/compile/output.py | 7 +- .../compile/_helpers/analysis/columns.py | 27 ++++-- .../compile/_helpers/attachment/core.py | 11 ++- .../compile/_helpers/attachment/sql_tests.py | 37 ++------ src/sqlbuild/compiler/compile/constants.py | 1 + .../discovery/_helpers/filesystem/core.py | 41 +++------ .../_helpers/filesystem/sql_test_files.py | 92 +++++++++++++++++++ .../compiler/discovery/_helpers/sql/tests.py | 16 +++- .../_helpers/sql_tests/native_planning.py | 9 +- .../cli/commands/main/compile/_test_types.py | 3 + .../test_fresh_process_compile_performance.py | 24 ++++- .../cli/commands/main/compile/_test_types.py | 6 ++ .../main/compile/test_compile_output.py | 59 +++++++++++- .../_helpers/test_sql_analysis_columns.py | 22 +++++ .../discovery/_helpers/test_sql_tests.py | 67 +++++++++++++- 15 files changed, 340 insertions(+), 82 deletions(-) create mode 100644 src/sqlbuild/compiler/discovery/_helpers/filesystem/sql_test_files.py diff --git a/src/sqlbuild/cli/commands/_helpers/compile/output.py b/src/sqlbuild/cli/commands/_helpers/compile/output.py index 80fa9696df..9ac6c7b8ef 100644 --- a/src/sqlbuild/cli/commands/_helpers/compile/output.py +++ b/src/sqlbuild/cli/commands/_helpers/compile/output.py @@ -6,6 +6,8 @@ from importlib.metadata import PackageNotFoundError, version from pathlib import Path +import orjson + from sqlbuild.cli.commands.constants import TARGET_DIRECTORY_NAME from sqlbuild.cli.commands.types import CompileLineageMode from sqlbuild.cli.output.models import ( @@ -175,7 +177,10 @@ def format_compile_json( "resources": _resources(graph=graph, lineage=lineage), "artifacts": _artifacts(written=written, manifest=manifest), } - return json.dumps(result, indent=2) + try: + return orjson.dumps(result, option=orjson.OPT_INDENT_2).decode() + except TypeError: + return json.dumps(result, indent=2) def _summary( diff --git a/src/sqlbuild/compiler/compile/_helpers/analysis/columns.py b/src/sqlbuild/compiler/compile/_helpers/analysis/columns.py index 412bd0a056..a65089ce9b 100644 --- a/src/sqlbuild/compiler/compile/_helpers/analysis/columns.py +++ b/src/sqlbuild/compiler/compile/_helpers/analysis/columns.py @@ -23,6 +23,7 @@ FULL_JOIN_SIDE, LEFT_JOIN_SIDE, RIGHT_JOIN_SIDE, + SQL_QUALIFIER_SEPARATOR_TOKEN, SQL_WILDCARD_TOKEN, ) from sqlbuild.compiler.compile.models import ( @@ -174,6 +175,12 @@ r'"([A-Za-z_][A-Za-z0-9_]*)"\)\s*(?=\()' ) _PLACEHOLDER_PATTERN: re.Pattern[str] = re.compile(r"@@@(\w+)") +_QUALIFIED_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile( + r'(?[^"]+)"|`(?P[^`]+)`|' + r"\[(?P[^\]]+)\]|(?P[A-Za-z_$][A-Za-z0-9_$]*))" + r"(?:\s|--[^\r\n]*(?:\r?\n|$)|/\*.*?\*/)*\.", + re.DOTALL, +) def _infer_columns_with_polyglot( @@ -1067,17 +1074,19 @@ def relation_stub(match: re.Match[str]) -> str: def _qualified_reference_names(*, query_sql: str, reference_names: Iterable[str]) -> frozenset[str]: - qualified: set[str] = set() + if SQL_QUALIFIER_SEPARATOR_TOKEN not in query_sql: + return frozenset() + names_by_normalized: dict[str, list[str]] = {} for value in reference_names: name: str = str(value) - escaped: str = re.escape(name) - if re.search( - rf'(? dict[str, object] | None: if not model_header_values: return {} if model_header_values.keys() != _REUSABLE_MODEL_HEADER_KEYS: return None - if _contains_dynamic_or_unsupported_reusable_metadata(model_header_values): + metadata: object = model_header_values["columns"] + cached: tuple[object, bool] | None = self.reusable_metadata_by_identity.get(id(metadata)) + if cached is not None and cached[0] is metadata: + return model_header_values if cached[1] else None + reusable: bool = not _contains_dynamic_or_unsupported_reusable_metadata(metadata) + self.reusable_metadata_by_identity[id(metadata)] = (metadata, reusable) + if not reusable: return None return model_header_values diff --git a/src/sqlbuild/compiler/compile/_helpers/attachment/sql_tests.py b/src/sqlbuild/compiler/compile/_helpers/attachment/sql_tests.py index 016e9cecd9..f11dd9b130 100644 --- a/src/sqlbuild/compiler/compile/_helpers/attachment/sql_tests.py +++ b/src/sqlbuild/compiler/compile/_helpers/attachment/sql_tests.py @@ -188,7 +188,7 @@ def build_test_inputs( parent ) if scoped_declarations is not None: - scoped_declarations = _rebind_test_declarations( + scoped_declarations = _project_test_declaration_consumer( context=scoped_declarations, consumer=resource ) else: @@ -223,9 +223,10 @@ def build_test_inputs( f"unused parameters: {', '.join(unused_parameters)} in case " f"'{test_case.name}'" ) - expanded_test_block: DiscoveredSqlTestBlock = replace( - test_block, - sql_body=parameter_sql, + expanded_test_block: DiscoveredSqlTestBlock = ( + test_block + if parameter_sql is test_block.sql_body + else replace(test_block, sql_body=parameter_sql) ) test_mode: SqlTestMode = test_block.mode tested_resource_names: tuple[str, ...] = () @@ -319,39 +320,15 @@ def build_test_inputs( return tuple(test_inputs) -def _rebind_test_declarations( +def _project_test_declaration_consumer( *, context: DeclarationExpansionContext, consumer: ResourceIdentity ) -> DeclarationExpansionContext: - declarations: DeclarationResolutionContext = context.declarations return replace( context, - declarations=replace( - declarations, - consumer=consumer, - enum_visibility=_rebind_visibility( - visibility=declarations.enum_visibility, consumer=consumer - ), - constant_visibility=_rebind_visibility( - visibility=declarations.constant_visibility, consumer=consumer - ), - macro_visibility=_rebind_visibility( - visibility=declarations.macro_visibility, consumer=consumer - ), - ), + declarations=replace(context.declarations, consumer=consumer), ) -def _rebind_visibility( - *, - visibility: dict[str, tuple[VisibilityRecord, ...]], - consumer: ResourceIdentity, -) -> dict[str, tuple[VisibilityRecord, ...]]: - rebound: dict[str, tuple[VisibilityRecord, ...]] = {} - for name, records in visibility.items(): - rebound[name] = tuple(replace(record, resource=consumer) for record in records) - return rebound - - def _macro_test_declaration_usages( *, sql: str, diff --git a/src/sqlbuild/compiler/compile/constants.py b/src/sqlbuild/compiler/compile/constants.py index 4529a150b8..5843706b0b 100644 --- a/src/sqlbuild/compiler/compile/constants.py +++ b/src/sqlbuild/compiler/compile/constants.py @@ -44,6 +44,7 @@ SQL_STATEMENT_TERMINATOR_TOKEN: str = ";" SQL_SINGLE_QUOTE_TOKEN: str = "'" SQL_QUOTE_TOKENS: frozenset[str] = frozenset({"'", '"', "`"}) +SQL_QUALIFIER_SEPARATOR_TOKEN: str = "." SQL_REFERENCE_NAME_QUOTE_TOKENS: frozenset[str] = frozenset({"'", '"'}) SQL_IDENTIFIER_EXTRA_TOKEN: str = "_" SQL_UNION_KEYWORD: str = "UNION" diff --git a/src/sqlbuild/compiler/discovery/_helpers/filesystem/core.py b/src/sqlbuild/compiler/discovery/_helpers/filesystem/core.py index afc40de786..fcf057f385 100644 --- a/src/sqlbuild/compiler/discovery/_helpers/filesystem/core.py +++ b/src/sqlbuild/compiler/discovery/_helpers/filesystem/core.py @@ -14,6 +14,7 @@ from pydantic import ValidationError +from sqlbuild.compiler.discovery._helpers.filesystem.sql_test_files import discover_sql_test_files from sqlbuild.compiler.discovery._helpers.python.functions import parse_python_function from sqlbuild.compiler.discovery._helpers.sql.audits import parse_sql_audit_file from sqlbuild.compiler.discovery._helpers.sql.declarations import ( @@ -32,7 +33,6 @@ prepare_model_file_headers, ) from sqlbuild.compiler.discovery._helpers.sql.scenarios import parse_sql_scenario_file -from sqlbuild.compiler.discovery._helpers.sql.tests import parse_sql_test_file from sqlbuild.compiler.discovery._helpers.yml.schema import parse_schema_yml from sqlbuild.compiler.discovery._helpers.yml.sources import parse_sources_yml from sqlbuild.compiler.discovery.constants import ( @@ -84,7 +84,6 @@ DiscoveredSqlHookFile, DiscoveredSqlModelFile, DiscoveredSqlScenarioFile, - DiscoveredSqlTestBlock, DiscoveredSqlTestFile, DiscoveredTaskFunction, DiscoveryFileFault, @@ -736,33 +735,17 @@ def discover_test_files( if not tests_root.is_dir(): return () - discovered_test_files: list[DiscoveredSqlTestFile] = [] - file_path: Path - for file_path in sorted(tests_root.rglob("*.sql")): - if selected_paths is not None and file_path.resolve() not in selected_paths: - continue - if _is_in_scoped_declaration_tree(file_path=file_path, project_dir=project_dir): - continue - try: - contents: str = file_path.read_text(encoding="utf-8") - blocks: tuple[DiscoveredSqlTestBlock, ...] = parse_sql_test_file( - contents=contents, file_path=file_path - ) - except (OSError, UnicodeError, ValueError, SyntaxError) as error: - if on_fault is None: - raise - on_fault(_discovery_fault(project_dir=project_dir, path=file_path, error=error)) - continue - discovered_test_files.append( - DiscoveredSqlTestFile( - file_path=file_path, - relative_path=file_path.relative_to(project_dir), - contents=contents, - blocks=blocks, - ownership_root=Path(SQL_TESTS_OWNERSHIP_ROOT), - ) - ) - return tuple(discovered_test_files) + file_paths: tuple[Path, ...] = tuple( + file_path + for file_path in sorted(tests_root.rglob("*.sql")) + if (selected_paths is None or file_path.resolve() in selected_paths) + and not _is_in_scoped_declaration_tree(file_path=file_path, project_dir=project_dir) + ) + return discover_sql_test_files( + project_dir=project_dir, + file_paths=file_paths, + on_fault=on_fault, + ) def discover_scenario_files( diff --git a/src/sqlbuild/compiler/discovery/_helpers/filesystem/sql_test_files.py b/src/sqlbuild/compiler/discovery/_helpers/filesystem/sql_test_files.py new file mode 100644 index 0000000000..87d459a03f --- /dev/null +++ b/src/sqlbuild/compiler/discovery/_helpers/filesystem/sql_test_files.py @@ -0,0 +1,92 @@ +"""Filesystem discovery for SQL-native unit test files.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +from sqlbuild.compiler.discovery._helpers.sql.tests import ( + parse_sql_test_file, + prepare_sql_test_file_headers, +) +from sqlbuild.compiler.discovery.constants import SQL_TESTS_OWNERSHIP_ROOT +from sqlbuild.compiler.discovery.exceptions import SqlTestParseError +from sqlbuild.compiler.discovery.models import ( + DiscoveredSqlTestBlock, + DiscoveredSqlTestFile, + DiscoveryFileFault, +) + +_TEST_FILE_BATCH_SIZE: int = 512 + + +def discover_sql_test_files( + *, + project_dir: Path, + file_paths: tuple[Path, ...], + on_fault: Callable[[DiscoveryFileFault], None] | None, +) -> tuple[DiscoveredSqlTestFile, ...]: + """Read and parse ordered SQL test paths through bounded native header batches.""" + + loaded: list[tuple[Path, str | None, Exception | None]] = [] + for file_path in file_paths: + try: + loaded.append((file_path, file_path.read_text(encoding="utf-8"), None)) + except (OSError, UnicodeError, ValueError, SyntaxError) as error: + loaded.append((file_path, None, error)) + + discovered: list[DiscoveredSqlTestFile] = [] + for batch_start in range(0, len(loaded), _TEST_FILE_BATCH_SIZE): + batch: list[tuple[Path, str | None, Exception | None]] = loaded[ + batch_start : batch_start + _TEST_FILE_BATCH_SIZE + ] + _prepare_batch(batch) + for file_path, contents, read_error in batch: + if read_error is not None: + if on_fault is None: + raise read_error + on_fault(_file_fault(project_dir=project_dir, path=file_path, error=read_error)) + continue + if contents is None: + raise SqlTestParseError("SQL test file read returned neither contents nor an error") + try: + blocks: tuple[DiscoveredSqlTestBlock, ...] = parse_sql_test_file( + contents=contents, file_path=file_path + ) + except (OSError, UnicodeError, ValueError, SyntaxError) as error: + if on_fault is None: + raise + on_fault(_file_fault(project_dir=project_dir, path=file_path, error=error)) + continue + discovered.append( + DiscoveredSqlTestFile( + file_path=file_path, + relative_path=file_path.relative_to(project_dir), + contents=contents, + blocks=blocks, + ownership_root=Path(SQL_TESTS_OWNERSHIP_ROOT), + ) + ) + return tuple(discovered) + + +def _prepare_batch(batch: list[tuple[Path, str | None, Exception | None]]) -> None: + contents_batch: list[str] = [] + for _path, contents, error in batch: + if contents is not None and error is None: + contents_batch.append(contents) + try: + prepare_sql_test_file_headers(contents_batch) + except (OSError, UnicodeError, ValueError, SyntaxError): + pass + + +def _file_fault(*, project_dir: Path, path: Path, error: Exception) -> DiscoveryFileFault: + try: + relative_path: Path | None = path.relative_to(project_dir) + except ValueError: + relative_path = None + return DiscoveryFileFault( + path=relative_path, + message=str(error).replace(str(project_dir), "."), + ) diff --git a/src/sqlbuild/compiler/discovery/_helpers/sql/tests.py b/src/sqlbuild/compiler/discovery/_helpers/sql/tests.py index de1ffc9bcd..8f37fff5f4 100644 --- a/src/sqlbuild/compiler/discovery/_helpers/sql/tests.py +++ b/src/sqlbuild/compiler/discovery/_helpers/sql/tests.py @@ -9,7 +9,10 @@ from sqlbuild.compiler.compile.constants import DEFAULT_SQL_TEST_MODE from sqlbuild.compiler.compile.types import SqlTestMode -from sqlbuild.compiler.discovery._helpers.sql.model_files import parse_header_values +from sqlbuild.compiler.discovery._helpers.sql.model_files import ( + parse_header_values, + prepare_model_header_tokens, +) from sqlbuild.compiler.discovery.exceptions import SqlTestParseError from sqlbuild.compiler.discovery.models import ( DiscoveredSqlTestBlock, @@ -43,6 +46,17 @@ _IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +def prepare_sql_test_file_headers(contents_batch: list[str]) -> None: + """Batch cache TEST header parsing before projecting individual files.""" + + headers: list[str] = [] + for contents in contents_batch: + headers.extend( + match.group("header") for match in _TEST_HEADER_ONLY_PATTERN.finditer(contents) + ) + prepare_model_header_tokens(headers) + + def parse_sql_test_file(*, contents: str, file_path: Path) -> tuple[DiscoveredSqlTestBlock, ...]: """Parse one SQL-native test file into one or more raw TEST(...) blocks.""" diff --git a/src/sqlbuild/compiler/planner/_helpers/sql_tests/native_planning.py b/src/sqlbuild/compiler/planner/_helpers/sql_tests/native_planning.py index 13757e2f86..d4eb11074d 100644 --- a/src/sqlbuild/compiler/planner/_helpers/sql_tests/native_planning.py +++ b/src/sqlbuild/compiler/planner/_helpers/sql_tests/native_planning.py @@ -2,10 +2,11 @@ from __future__ import annotations -import json import time from typing import Any, cast +import orjson + import sqlbuild._native as _native from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter from sqlbuild.compiler.compile.exceptions import CompileInputError @@ -80,7 +81,9 @@ def plan_and_render_sql_test_artifacts( try: native_response: str = cast( NativeSqlTestRenderingModule, _native - ).plan_and_render_sql_tests_json(json.dumps(request, sort_keys=True, separators=(",", ":"))) + ).plan_and_render_sql_tests_json( + orjson.dumps(request, option=orjson.OPT_SORT_KEYS).decode() + ) except ValueError as error: message: str = str(error) if message.startswith("compile_input:"): @@ -88,7 +91,7 @@ def plan_and_render_sql_test_artifacts( if message.startswith("planner_input:"): raise PlannerInputError(message.removeprefix("planner_input:")) from None raise NativeSqlTestPlanningError(f"native SQL-test planning failed: {message}") from error - response_payload: object = json.loads(native_response) + response_payload: object = orjson.loads(native_response) if not isinstance(response_payload, dict): raise NativeSqlTestPlanningError( "native SQL-test planning returned an invalid batch response" 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 6b90899436..ab4a32acdc 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 @@ -101,6 +101,9 @@ class FreshProcessCompilePerformanceGuardTestCase: test_count: int audit_count: int expected_max_wall_seconds: float + expected_max_total_ms: int + expected_max_model_analysis_ms: int + expected_max_test_input_compile_ms: int expected_max_rss_bytes: int expected_semantic_fingerprint: str diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py index c04649f21c..ce2434fe27 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py @@ -4,6 +4,7 @@ import logging from pathlib import Path +from typing import cast import pytest @@ -36,8 +37,11 @@ macro_count=37, test_count=2_945, audit_count=5_056, - expected_max_wall_seconds=13.0, - expected_max_rss_bytes=2 * _GIB, + expected_max_wall_seconds=10.0, + expected_max_total_ms=8_800, + expected_max_model_analysis_ms=2_500, + expected_max_test_input_compile_ms=300, + expected_max_rss_bytes=int(1.5 * _GIB), expected_semantic_fingerprint=( "02af81fb7723d30ec5082ed06fb039e45acba3d60b2d8a30fcbccb5a990bb9fc" ), @@ -51,8 +55,11 @@ macro_count=61, test_count=4_908, audit_count=8_427, - expected_max_wall_seconds=17.0, - expected_max_rss_bytes=2 * _GIB, + expected_max_wall_seconds=14.0, + expected_max_total_ms=13_000, + expected_max_model_analysis_ms=3_800, + expected_max_test_input_compile_ms=650, + expected_max_rss_bytes=int(1.75 * _GIB), expected_semantic_fingerprint=( "0c9258635fe4f9d31ec2dd1c99dbc63f5c8551ca7fea494003c5ab1c3a2ad57c" ), @@ -66,7 +73,10 @@ macro_count=123, test_count=9_816, audit_count=16_855, - expected_max_wall_seconds=27.0, + expected_max_wall_seconds=26.0, + expected_max_total_ms=25_000, + expected_max_model_analysis_ms=6_800, + expected_max_test_input_compile_ms=1_200, expected_max_rss_bytes=2 * _GIB, expected_semantic_fingerprint=( "6887ddd2547137efd8fe67d13fb07142f6287c0911d152d69eca0d495310e02b" @@ -118,4 +128,8 @@ def test_given_scaled_semantic_project_when_compiling_fresh_then_preserves_resou } assert result.semantic_fingerprint == test_case.expected_semantic_fingerprint assert result.elapsed_seconds < test_case.expected_max_wall_seconds + compile_timings: dict[str, int] = cast(dict[str, int], result.payload["compile_timings"]) + assert compile_timings["total_ms"] < test_case.expected_max_total_ms + assert compile_timings["model_analysis_ms"] < test_case.expected_max_model_analysis_ms + assert compile_timings["test_input_compile_ms"] < test_case.expected_max_test_input_compile_ms assert result.peak_rss_bytes < test_case.expected_max_rss_bytes diff --git a/tests/unit/src/sqlbuild/cli/commands/main/compile/_test_types.py b/tests/unit/src/sqlbuild/cli/commands/main/compile/_test_types.py index 8e8233c085..833063594a 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/compile/_test_types.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/compile/_test_types.py @@ -99,6 +99,12 @@ class CompileJsonExecutionLayersTestCase: expected_execution_layers: int +@dataclass(frozen=True) +class CompileJsonLargeIntegerTestCase: + description: str + expected_value: int + + @dataclass(frozen=True) class CompileJsonDiagnosticsTestCase: description: str diff --git a/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_output.py b/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_output.py index c4c4766904..41a7d6582a 100644 --- a/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_output.py +++ b/tests/unit/src/sqlbuild/cli/commands/main/compile/test_compile_output.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from dataclasses import replace from typing import cast import pytest @@ -13,12 +14,14 @@ from sqlbuild.cli.output.models import ( WrittenTarget, ) -from sqlbuild.compiler.compile.models import CompiledObjectKey +from sqlbuild.compiler.compile.models import CompiledModel, CompiledObjectKey, CompileModelConfig from sqlbuild.compiler.compile.types import CompiledResourceType +from sqlbuild.compiler.discovery.models import SqlHookEntry from sqlbuild.compiler.lineage.models import ProjectColumnLineage from sqlbuild.compiler.pipeline.models import ProjectGraph from tests.unit.src.sqlbuild.cli.commands.main.compile._test_types import ( CompileJsonExecutionLayersTestCase, + CompileJsonLargeIntegerTestCase, CompileRichLineageOutputTestCase, CompileSelectedJsonOutputTestCase, CompileSelectedOutputTestCase, @@ -32,6 +35,60 @@ ) +@pytest.mark.parametrize( + "test_case", + ( + CompileJsonLargeIntegerTestCase( + description="preserves integer above unsigned 64-bit range", + expected_value=2**64, + ), + ), + ids=lambda case: case.description, +) +def test_given_large_hook_integer_when_formatting_json_then_preserves_value( + test_case: CompileJsonLargeIntegerTestCase, +) -> None: + graph: ProjectGraph = build_compile_output_graph(model_names=("orders",)) + model: CompiledModel = replace( + graph.project.models[0], + config=CompileModelConfig( + values={ + "pre_hooks": [ + SqlHookEntry( + statement="SELECT 1", + name="record_order", + kwargs={"order_id": test_case.expected_value}, + ) + ] + } + ), + ) + graph = replace(graph, project=replace(graph.project, models=(model,))) + + payload: dict[str, object] = json.loads( + format_compile_json( + graph=graph, + written=WrittenTarget( + model_count=1, + seed_count=0, + function_count=0, + audit_count=0, + test_count=0, + target_dir=model.relative_path.parent.parent / "target", + ), + manifest=False, + timings_ms={}, + lineage=None, + diagnostics=(), + ) + ) + resources: dict[str, object] = cast(dict[str, object], payload["resources"]) + models: list[dict[str, object]] = cast(list[dict[str, object]], resources["models"]) + hooks: list[dict[str, object]] = cast(list[dict[str, object]], models[0]["pre_hooks"]) + + assert cast(dict[str, object], hooks[0]["kwargs"])["order_id"] == test_case.expected_value + + @pytest.mark.parametrize( "test_case", ( diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py index 6777f967d1..4fafde5e17 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py @@ -8,6 +8,7 @@ from polyglot_sql import ParseError from sqlbuild.adapter.contract.models import ExpressionInferenceProfile +from sqlbuild.compiler.compile._helpers.analysis import columns as analysis_columns from sqlbuild.compiler.compile._helpers.analysis.columns import ( import_polyglot_sql, substitute_placeholder_defaults, @@ -233,6 +234,27 @@ def test_given_qualified_reference_when_batch_analyzing_then_preserves_analysis_ assert tuple(actual.lineage_columns) == expected.lineage_columns +@pytest.mark.parametrize( + "test_case", + (ExpectedCountTestCase(description="unqualified SQL skips scanner", expected_count=0),), + ids=lambda case: case.description, +) +def test_given_sql_without_dots_when_finding_qualified_references_then_skips_scanner( + monkeypatch: pytest.MonkeyPatch, + test_case: ExpectedCountTestCase, +) -> None: + scanner: Mock = Mock() + monkeypatch.setattr(analysis_columns, "_QUALIFIED_IDENTIFIER_PATTERN", scanner) + + result: frozenset[str] = analysis_columns._qualified_reference_names( + query_sql='SELECT order_id FROM __ref("orders")', + reference_names=("orders",), + ) + + assert result == frozenset() + assert scanner.finditer.call_count == test_case.expected_count + + @pytest.mark.parametrize( "test_case", [ diff --git a/tests/unit/src/sqlbuild/compiler/discovery/_helpers/test_sql_tests.py b/tests/unit/src/sqlbuild/compiler/discovery/_helpers/test_sql_tests.py index 9a53079e02..3eb50c8684 100644 --- a/tests/unit/src/sqlbuild/compiler/discovery/_helpers/test_sql_tests.py +++ b/tests/unit/src/sqlbuild/compiler/discovery/_helpers/test_sql_tests.py @@ -2,17 +2,27 @@ from decimal import Decimal from pathlib import Path +from unittest.mock import Mock import pytest -from sqlbuild.compiler.discovery._helpers.sql.tests import parse_sql_test_file +from sqlbuild.compiler.discovery._helpers.filesystem import core as filesystem_core +from sqlbuild.compiler.discovery._helpers.filesystem import sql_test_files as test_file_discovery +from sqlbuild.compiler.discovery._helpers.sql import model_files as model_file_helpers +from sqlbuild.compiler.discovery._helpers.sql.tests import ( + parse_sql_test_file, + prepare_sql_test_file_headers, +) from sqlbuild.compiler.discovery.exceptions import SqlTestParseError from sqlbuild.compiler.discovery.models import ( DiscoveredSqlTestBlock, DiscoveredSqlTestCase, + DiscoveredSqlTestFile, + DiscoveryFileFault, SqlTestParameterDeclaration, ) from tests.unit.src.sqlbuild.compiler.discovery._helpers._test_types import ( + ExpectedCountTestCase, ParseSqlTestFileErrorTestCase, ParseSqlTestFileTestCase, ) @@ -23,6 +33,61 @@ ) +@pytest.mark.parametrize( + "test_case", + (ExpectedCountTestCase(description="two headers use one batch", expected_count=2),), + ids=lambda case: case.description, +) +def test_given_multiple_test_files_when_preparing_headers_then_uses_one_native_batch( + monkeypatch: pytest.MonkeyPatch, + test_case: ExpectedCountTestCase, +) -> None: + parser: Mock = Mock(wraps=model_file_helpers._native.parse_model_headers) + monkeypatch.setattr(model_file_helpers._native, "parse_model_headers", parser) + + prepare_sql_test_file_headers( + [ + 'TEST (name "native_batch_guard_alpha");\nSELECT 1', + 'TEST (name "native_batch_guard_beta");\nSELECT 2', + ] + ) + + assert parser.call_count == 1 + assert len(parser.call_args.args[0]) == test_case.expected_count + + +@pytest.mark.parametrize( + "test_case", + (ExpectedCountTestCase(description="one invalid file reports one fault", expected_count=1),), + ids=lambda case: case.description, +) +def test_given_batch_preparation_failure_when_discovering_tolerantly_then_reports_file_faults( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + test_case: ExpectedCountTestCase, +) -> None: + tests_dir: Path = tmp_path / "tests" / "unit" + tests_dir.mkdir(parents=True) + (tests_dir / "a_invalid.sql").write_text("SELECT 1", encoding="utf-8") + (tests_dir / "b_valid.sql").write_text('TEST (name "valid_order");\nSELECT 1', encoding="utf-8") + preparation: Mock = Mock(side_effect=ValueError("invalid batch projection")) + monkeypatch.setattr(test_file_discovery, "prepare_sql_test_file_headers", preparation) + faults: list[DiscoveryFileFault] = [] + + discovered: tuple[DiscoveredSqlTestFile, ...] = filesystem_core.discover_test_files( + project_dir=tmp_path, + on_fault=faults.append, + ) + + assert preparation.call_count == 1 + assert len(faults) == test_case.expected_count + assert faults[0].path == Path("tests/unit/a_invalid.sql") + discovered_names: list[str | None] = [] + for discovered_file in discovered: + discovered_names.extend(block.name for block in discovered_file.blocks) + assert tuple(discovered_names) == ("valid_order",) + + @pytest.mark.parametrize( "test_case", [ From 4276e06f44b9b846de1f0253c99d56e9adc6451d Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:25:33 +0100 Subject: [PATCH 3/4] test: tighten 10k cold compile guard --- .../main/compile/test_fresh_process_compile_performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py index ce2434fe27..0ed312ec19 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py @@ -73,7 +73,7 @@ macro_count=123, test_count=9_816, audit_count=16_855, - expected_max_wall_seconds=26.0, + expected_max_wall_seconds=25.0, expected_max_total_ms=25_000, expected_max_model_analysis_ms=6_800, expected_max_test_input_compile_ms=1_200, From 9901f89dd0ffa645e2378f17c3de86616189e55d Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:42:06 +0100 Subject: [PATCH 4/4] test: calibrate cold compile guards --- .../cli/commands/main/compile/_test_types.py | 3 --- .../test_fresh_process_compile_performance.py | 20 +++---------------- 2 files changed, 3 insertions(+), 20 deletions(-) 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 ab4a32acdc..6b90899436 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 @@ -101,9 +101,6 @@ class FreshProcessCompilePerformanceGuardTestCase: test_count: int audit_count: int expected_max_wall_seconds: float - expected_max_total_ms: int - expected_max_model_analysis_ms: int - expected_max_test_input_compile_ms: int expected_max_rss_bytes: int expected_semantic_fingerprint: str diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py index 0ed312ec19..9bdaf8a679 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_fresh_process_compile_performance.py @@ -4,7 +4,6 @@ import logging from pathlib import Path -from typing import cast import pytest @@ -37,10 +36,7 @@ macro_count=37, test_count=2_945, audit_count=5_056, - expected_max_wall_seconds=10.0, - expected_max_total_ms=8_800, - expected_max_model_analysis_ms=2_500, - expected_max_test_input_compile_ms=300, + expected_max_wall_seconds=11.0, expected_max_rss_bytes=int(1.5 * _GIB), expected_semantic_fingerprint=( "02af81fb7723d30ec5082ed06fb039e45acba3d60b2d8a30fcbccb5a990bb9fc" @@ -55,10 +51,7 @@ macro_count=61, test_count=4_908, audit_count=8_427, - expected_max_wall_seconds=14.0, - expected_max_total_ms=13_000, - expected_max_model_analysis_ms=3_800, - expected_max_test_input_compile_ms=650, + expected_max_wall_seconds=15.5, expected_max_rss_bytes=int(1.75 * _GIB), expected_semantic_fingerprint=( "0c9258635fe4f9d31ec2dd1c99dbc63f5c8551ca7fea494003c5ab1c3a2ad57c" @@ -73,10 +66,7 @@ macro_count=123, test_count=9_816, audit_count=16_855, - expected_max_wall_seconds=25.0, - expected_max_total_ms=25_000, - expected_max_model_analysis_ms=6_800, - expected_max_test_input_compile_ms=1_200, + expected_max_wall_seconds=29.5, expected_max_rss_bytes=2 * _GIB, expected_semantic_fingerprint=( "6887ddd2547137efd8fe67d13fb07142f6287c0911d152d69eca0d495310e02b" @@ -128,8 +118,4 @@ def test_given_scaled_semantic_project_when_compiling_fresh_then_preserves_resou } assert result.semantic_fingerprint == test_case.expected_semantic_fingerprint assert result.elapsed_seconds < test_case.expected_max_wall_seconds - compile_timings: dict[str, int] = cast(dict[str, int], result.payload["compile_timings"]) - assert compile_timings["total_ms"] < test_case.expected_max_total_ms - assert compile_timings["model_analysis_ms"] < test_case.expected_max_model_analysis_ms - assert compile_timings["test_input_compile_ms"] < test_case.expected_max_test_input_compile_ms assert result.peak_rss_bytes < test_case.expected_max_rss_bytes