From 4039290e0771eb1c261e720beb4e7f1a2e20fef5 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 20 Sep 2026 06:05:56 +0100 Subject: [PATCH] fix: reduce large-project planning work --- .../snowflake/classes/snowflake_adapter.py | 2 +- .../planner/_helpers/fixtures/completion.py | 100 +++++++--- .../planner/_helpers/output/plan_output.py | 8 + .../planner/_helpers/sql_tests/assembly.py | 3 + .../_helpers/sql_tests/fixture_validation.py | 3 + src/sqlbuild/compiler/planner/models.py | 10 + .../compiler/planner/_helpers/_test_types.py | 6 + .../planner/_helpers/test_fixture_planning.py | 86 ++++++++ .../adapters/snowflake/test_client.py | 15 +- .../compiler/planner/_helpers/_test_types.py | 18 ++ .../_helpers/test_fixture_planning_work.py | 188 ++++++++++++++++++ 11 files changed, 410 insertions(+), 29 deletions(-) create mode 100644 tests/integration/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning.py create mode 100644 tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py diff --git a/src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py b/src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py index 61cd5de2a0..5f4aa7702f 100644 --- a/src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py +++ b/src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py @@ -95,7 +95,7 @@ from sqlbuild.sql_values.models import SqlValue _EXACT_COLUMN_INSPECTION_LIMIT: int = 32 -_BULK_COLUMN_RELATION_CHUNK_SIZE: int = 200 +_BULK_COLUMN_RELATION_CHUNK_SIZE: int = 2_000 class SnowflakeAdapter(MicrobatchMixin, UnkeyedDiffMixin, BaseAdapter): diff --git a/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py b/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py index 3d4ebc5b4c..cebbf65996 100644 --- a/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py +++ b/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py @@ -31,6 +31,7 @@ FixtureRelationMetadata, RelationFixtureCompletion, RelationFixtureDiagnostic, + RelationFixturePlanningContext, ) from sqlbuild.compiler.planner.types import ContractPolicy, FixtureGroups, FixtureKey from sqlbuild.compiler.references.constants import REF_PATTERN, SEED_PATTERN, SOURCE_PATTERN @@ -47,10 +48,14 @@ def build_relation_fixture_completion( adapter: BaseAdapter, ordered_model_names: tuple[str, ...], fixture_groups: FixtureGroups, + planning_context: RelationFixturePlanningContext | None = None, ) -> RelationFixtureCompletion: """Complete required omitted columns with typed nulls unless explicitly non-nullable.""" - model_map: dict[str, CompiledModel] = {model.name: model for model in project.models} + context: RelationFixturePlanningContext = planning_context or build_relation_fixture_context( + project=project + ) + model_map: dict[str, CompiledModel] = context.models_by_name fixture_sql_by_key: dict[FixtureKey, str] = {} inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]] = {} for resource_type, fixtures in fixture_groups: @@ -64,21 +69,23 @@ def build_relation_fixture_completion( if inferred is not None: inferred_by_fixture[key] = inferred - relations: dict[FixtureKey, FixtureRelationMetadata] = _fixture_relation_metadata( - project=project, - model_map=model_map, + relations: dict[FixtureKey, FixtureRelationMetadata] = context.relations + fixture_keys_requiring_analysis: frozenset[FixtureKey] = _fixture_keys_requiring_analysis( + inferred_by_fixture=inferred_by_fixture, + authoritative_columns=context.authoritative_columns, ) required: dict[FixtureKey, dict[str, set[str]]] star_fixture_keys: frozenset[FixtureKey] required, star_fixture_keys = _required_fixture_columns( ordered_model_names=ordered_model_names, model_map=model_map, - fixture_keys=frozenset(fixture_sql_by_key), + fixture_keys=fixture_keys_requiring_analysis, relations=relations, + authoritative_columns=context.authoritative_columns, adapter=adapter, ) expected_types: dict[FixtureKey, dict[str, str]] = _expected_fixture_types( - relations=relations, + base_expected_types=context.expected_types, ordered_model_names=ordered_model_names, model_map=model_map, ) @@ -177,23 +184,61 @@ def build_relation_fixture_completion( ) +def _fixture_keys_requiring_analysis( + *, + inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]], + authoritative_columns: dict[FixtureKey, frozenset[str]], +) -> frozenset[FixtureKey]: + required: set[FixtureKey] = set() + for key, inferred in inferred_by_fixture.items(): + expected_names: frozenset[str] | None = authoritative_columns.get(key) + supplied_names: frozenset[str] = frozenset(column.name.casefold() for column in inferred) + if expected_names is None or not expected_names.issubset(supplied_names): + required.add(key) + return frozenset(required) + + +def build_relation_fixture_context(*, project: CompiledProject) -> RelationFixturePlanningContext: + """Build immutable project-wide fixture metadata once per planning invocation.""" + + models_by_name: dict[str, CompiledModel] = {model.name: model for model in project.models} + relations: dict[FixtureKey, FixtureRelationMetadata] = _fixture_relation_metadata( + project=project, + model_map=models_by_name, + ) + authoritative_columns: dict[FixtureKey, frozenset[str]] = _authoritative_fixture_columns( + relations=relations + ) + return RelationFixturePlanningContext( + models_by_name=models_by_name, + relations=relations, + authoritative_columns=authoritative_columns, + expected_types=_base_expected_fixture_types(relations=relations), + ) + + +def _authoritative_fixture_columns( + *, relations: dict[FixtureKey, FixtureRelationMetadata] +) -> dict[FixtureKey, frozenset[str]]: + authoritative: dict[FixtureKey, frozenset[str]] = {} + for key, relation in relations.items(): + if relation.authoritative_names: + authoritative[key] = frozenset(column.name.casefold() for column in relation.columns) + return authoritative + + def _required_fixture_columns( *, ordered_model_names: tuple[str, ...], model_map: dict[str, CompiledModel], fixture_keys: frozenset[FixtureKey], relations: dict[FixtureKey, FixtureRelationMetadata], + authoritative_columns: dict[FixtureKey, frozenset[str]], adapter: BaseAdapter, ) -> tuple[dict[FixtureKey, dict[str, set[str]]], frozenset[FixtureKey]]: required: dict[FixtureKey, dict[str, set[str]]] = {} star_fixture_keys: set[FixtureKey] = set() - authoritative_columns: dict[FixtureKey, frozenset[str]] = {} - for key, relation in relations.items(): - if not relation.authoritative_names: - continue - authoritative_columns[key] = frozenset( - column.name.casefold() for column in relation.columns - ) + dialect: str | None = adapter.expression_inference_profile().sql_analysis_dialect for model_name in ordered_model_names: model: CompiledModel | None = model_map.get(model_name) if model is None: @@ -203,7 +248,7 @@ def _required_fixture_columns( references=model.references, fast_lineage=model.fast_lineage_columns or (), fixture_keys=fixture_keys, - dialect=adapter.expression_inference_profile().sql_analysis_dialect, + dialect=dialect, ): key: FixtureKey = ( CompiledResourceType(upstream.resource_type), @@ -451,22 +496,18 @@ def _model_fixture_metadata(*, model: CompiledModel) -> FixtureRelationMetadata: def _expected_fixture_types( *, - relations: dict[FixtureKey, FixtureRelationMetadata], + base_expected_types: dict[FixtureKey, dict[str, str]], ordered_model_names: tuple[str, ...], model_map: dict[str, CompiledModel], ) -> dict[FixtureKey, dict[str, str]]: - expected_types: dict[FixtureKey, dict[str, str]] = {} - for key, relation in relations.items(): - column_types: dict[str, str] = {} - for column in relation.columns: - if column.type is not None: - column_types[column.name.casefold()] = column.type - expected_types[key] = column_types + expected_types: dict[FixtureKey, dict[str, str]] = dict(base_expected_types) for model_name in ordered_model_names: model: CompiledModel | None = model_map.get(model_name) if model is None: continue - model_types: dict[str, str] = expected_types[(CompiledResourceType.MODEL, model_name)] + model_key: FixtureKey = (CompiledResourceType.MODEL, model_name) + model_types: dict[str, str] = dict(expected_types[model_key]) + expected_types[model_key] = model_types for lineage_column in model.fast_lineage_columns or (): if lineage_column.transform_kind != ColumnTransformKind.DIRECT: continue @@ -484,6 +525,19 @@ def _expected_fixture_types( return expected_types +def _base_expected_fixture_types( + *, relations: dict[FixtureKey, FixtureRelationMetadata] +) -> dict[FixtureKey, dict[str, str]]: + expected_types: dict[FixtureKey, dict[str, str]] = {} + for key, relation in relations.items(): + column_types: dict[str, str] = {} + for column in relation.columns: + if column.type is not None: + column_types[column.name.casefold()] = column.type + expected_types[key] = column_types + return expected_types + + def _unknown_fixture_column_diagnostics( *, inferred_by_fixture: dict[FixtureKey, tuple[InferredColumn, ...]], diff --git a/src/sqlbuild/compiler/planner/_helpers/output/plan_output.py b/src/sqlbuild/compiler/planner/_helpers/output/plan_output.py index 559fcdea69..0c5fb35cd3 100644 --- a/src/sqlbuild/compiler/planner/_helpers/output/plan_output.py +++ b/src/sqlbuild/compiler/planner/_helpers/output/plan_output.py @@ -20,6 +20,9 @@ PythonHookEntry, ) from sqlbuild.compiler.fingerprints.models import Fingerprint +from sqlbuild.compiler.planner._helpers.fixtures.completion import ( + build_relation_fixture_context, +) from sqlbuild.compiler.planner._helpers.graph.loader_dag import upstream_loader_dependency_names from sqlbuild.compiler.planner._helpers.graph.source_load_nodes import build_source_load_entries from sqlbuild.compiler.planner._helpers.output.audit_entry import plan_audit @@ -49,6 +52,7 @@ PlanOutputExtras, PlanProviderUsage, PlanWarning, + RelationFixturePlanningContext, SeedPlanEntry, SourceLoadPlanEntry, SqlTestPlanEntry, @@ -441,12 +445,15 @@ def build_selected_test_entries( entries: list[SqlTestPlanEntry] = [] warnings: list[PlanWarning] = [] fixture_diagnostics: list[str] = [] + fixture_planning_context: RelationFixturePlanningContext | None = None sql_test: CompiledSqlTest for sql_test in project.sql_tests: if not scope_overlaps(scope_deps=sql_test.scope_deps, selected_keys=selected_keys): continue if case_name is not None and sql_test.case_name != case_name: continue + if project.settings.sql_analysis and fixture_planning_context is None: + fixture_planning_context = build_relation_fixture_context(project=project) test_entry: SqlTestPlanEntry test_warnings: tuple[PlanWarning, ...] try: @@ -456,6 +463,7 @@ def build_selected_test_entries( adapter=adapter, sql_analysis_enabled=project.settings.sql_analysis, validate_fixtures=True, + fixture_planning_context=fixture_planning_context, ) except SqlTestFixtureValidationError as error: fixture_diagnostics.extend(error.diagnostics) diff --git a/src/sqlbuild/compiler/planner/_helpers/sql_tests/assembly.py b/src/sqlbuild/compiler/planner/_helpers/sql_tests/assembly.py index 2c9b6ca62c..3e0ab89447 100644 --- a/src/sqlbuild/compiler/planner/_helpers/sql_tests/assembly.py +++ b/src/sqlbuild/compiler/planner/_helpers/sql_tests/assembly.py @@ -46,6 +46,7 @@ from sqlbuild.compiler.planner.models import ( ChainStep, PlanWarning, + RelationFixturePlanningContext, SqlAnalysisResolvedTestSql, SqlTestAssertionStep, SqlTestPlanEntry, @@ -133,6 +134,7 @@ def plan_test( adapter: BaseAdapter, sql_analysis_enabled: bool = False, validate_fixtures: bool = False, + fixture_planning_context: RelationFixturePlanningContext | None = None, ) -> tuple[SqlTestPlanEntry, tuple[PlanWarning, ...]]: """Build a test plan entry with chained resolution.""" @@ -193,6 +195,7 @@ def plan_test( mock_sources=mock_sources, mock_seeds=mock_seeds, expected_outputs=expected_map, + planning_context=fixture_planning_context, ) warnings: list[PlanWarning] = [] diff --git a/src/sqlbuild/compiler/planner/_helpers/sql_tests/fixture_validation.py b/src/sqlbuild/compiler/planner/_helpers/sql_tests/fixture_validation.py index b76bd268b2..4561c6da77 100644 --- a/src/sqlbuild/compiler/planner/_helpers/sql_tests/fixture_validation.py +++ b/src/sqlbuild/compiler/planner/_helpers/sql_tests/fixture_validation.py @@ -19,6 +19,7 @@ from sqlbuild.compiler.planner.models import ( RelationFixtureCompletion, RelationFixtureDiagnostic, + RelationFixturePlanningContext, ) _COLLECTION_TYPES: frozenset[str] = frozenset( @@ -36,6 +37,7 @@ def build_validated_test_fixtures( mock_sources: dict[str, str], mock_seeds: dict[str, str], expected_outputs: dict[str, str], + planning_context: RelationFixturePlanningContext | None = None, ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: """Report all statically provable missing columns and collection-type mismatches.""" @@ -49,6 +51,7 @@ def build_validated_test_fixtures( adapter=adapter, ordered_model_names=ordered_model_names, fixture_groups=fixture_groups, + planning_context=planning_context, ) errors: list[str] = [ _located_diagnostic(test=test, diagnostic=diagnostic) diff --git a/src/sqlbuild/compiler/planner/models.py b/src/sqlbuild/compiler/planner/models.py index e333d83627..64ccb4d751 100644 --- a/src/sqlbuild/compiler/planner/models.py +++ b/src/sqlbuild/compiler/planner/models.py @@ -122,6 +122,16 @@ class FixtureRelationMetadata: authoritative_names: bool +@dataclass(frozen=True) +class RelationFixturePlanningContext: + """Project-wide fixture metadata shared across test and scenario planning.""" + + models_by_name: dict[str, CompiledModel] + relations: dict[FixtureKey, FixtureRelationMetadata] + authoritative_columns: dict[FixtureKey, frozenset[str]] + expected_types: dict[FixtureKey, dict[str, str]] + + @dataclass(frozen=True) class GraphNodeKey: """Neutral graph key matching fingerprint identity fields.""" diff --git a/tests/integration/src/sqlbuild/compiler/planner/_helpers/_test_types.py b/tests/integration/src/sqlbuild/compiler/planner/_helpers/_test_types.py index 43d0c003e3..0e4a35dbd4 100644 --- a/tests/integration/src/sqlbuild/compiler/planner/_helpers/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/planner/_helpers/_test_types.py @@ -7,6 +7,12 @@ from sqlbuild.spec.contracts.models import SourceEntry +@dataclass(frozen=True) +class FixturePlanningIntegrationTestCase: + description: str + expected_sql_fragment: str + + @dataclass(frozen=True) class GatherWatermarkTypeTestCase: description: str diff --git a/tests/integration/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning.py b/tests/integration/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning.py new file mode 100644 index 0000000000..fcd38e6410 --- /dev/null +++ b/tests/integration/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from sqlbuild.adapters.duckdb.classes.duckdb_adapter import DuckDbAdapter +from sqlbuild.compiler.compile.models import CompiledProject +from sqlbuild.compiler.discovery.main.discover import discover_project_inputs +from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs +from sqlbuild.compiler.pipeline.main.project import compile_project +from sqlbuild.compiler.planner._helpers.output.plan_output import build_selected_test_entries +from sqlbuild.compiler.planner.models import SqlTestPlanEntry +from tests.integration.src.sqlbuild.compiler.planner._helpers._test_types import ( + FixturePlanningIntegrationTestCase, +) + + +@pytest.mark.parametrize( + "test_case", + ( + FixturePlanningIntegrationTestCase( + description="partial source fixture", + expected_sql_fragment='CAST(NULL AS VARCHAR) AS "status"', + ), + ), + ids=lambda case: case.description, +) +def test_given_partial_fixture_when_compiling_and_planning_then_completes_required_columns( + test_case: FixturePlanningIntegrationTestCase, + tmp_path: Path, + write_repo_files: Callable[[Path, dict[str, str]], None], +) -> None: + write_repo_files( + tmp_path, + { + "sqlbuild_project.toml": ( + 'name = "fixture_cache_demo"\n' + 'adapter = "duckdb"\n\n' + "[connection]\n" + 'database = ":memory:"\n' + ), + "models/orders.sql": ( + 'MODEL ();\n\nSELECT order_id, status FROM __source("raw_orders")\n' + ), + "sources/raw_orders.yml": ( + "sources:\n" + " - name: raw_orders\n" + " expression: SELECT 1 AS order_id, 'open' AS status\n" + " columns:\n" + " - name: order_id\n type: INTEGER\n nullable: false\n" + " - name: status\n type: VARCHAR\n nullable: true\n" + ), + "tests/unit/test_orders.sql": ( + "TEST();\n\n" + "WITH\n" + "__source__raw_orders AS (SELECT 1 AS order_id),\n" + "__expected__orders AS (\n" + " SELECT 1 AS order_id, CAST(NULL AS VARCHAR) AS status\n" + ")\n" + "SELECT 1\n" + ), + }, + ) + discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(project_dir=tmp_path) + adapter: DuckDbAdapter = DuckDbAdapter() + + project: CompiledProject = compile_project( + discovered_inputs=discovered_inputs, + adapter=adapter, + ) + entries, warnings = build_selected_test_entries( + project=project, + adapter=adapter, + selected_keys=frozenset((project.models[0].key,)), + ) + + assert warnings == [] + assert len(entries) == 1 + entry: SqlTestPlanEntry = entries[0] + assert test_case.expected_sql_fragment in entry.chain[0].resolved_sql + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/unit/src/sqlbuild/adapters/snowflake/test_client.py b/tests/unit/src/sqlbuild/adapters/snowflake/test_client.py index 0884a60d48..facd416002 100644 --- a/tests/unit/src/sqlbuild/adapters/snowflake/test_client.py +++ b/tests/unit/src/sqlbuild/adapters/snowflake/test_client.py @@ -569,14 +569,19 @@ def test_given_33_mixed_relations_when_getting_columns_then_uses_bulk_query( "test_case", [ SnowflakeQualifiedColumnInspectionTestCase( - description="251 relations use two bounded bulk queries and merge all results", + description="251 relations use one bounded bulk query and merge all results", relation_count=251, - expected_statement_count=2, + expected_statement_count=1, ), SnowflakeQualifiedColumnInspectionTestCase( - description="1000 relations use five bounded bulk queries and merge all results", + description="1000 relations use one bounded bulk query and merge all results", relation_count=1000, - expected_statement_count=5, + expected_statement_count=1, + ), + SnowflakeQualifiedColumnInspectionTestCase( + description="2001 relations use two bounded bulk queries and merge all results", + relation_count=2001, + expected_statement_count=2, ), ], ids=lambda case: case.description, @@ -591,7 +596,7 @@ def test_given_large_relation_set_when_getting_columns_then_chunks_and_merges_bu sorted(relations, key=lambda relation: tuple(part or "" for part in relation.identity)) ) connection: FakeSnowflakeMetadataSequenceConnection = build_bulk_sequence_connection( - relations=ordered_relations, chunk_size=200 + relations=ordered_relations, chunk_size=2000 ) columns: dict[tuple[str | None, str | None, str], tuple[ColumnInfo, ...]] = ( diff --git a/tests/unit/src/sqlbuild/compiler/planner/_helpers/_test_types.py b/tests/unit/src/sqlbuild/compiler/planner/_helpers/_test_types.py index 91858a82e5..aece5714e8 100644 --- a/tests/unit/src/sqlbuild/compiler/planner/_helpers/_test_types.py +++ b/tests/unit/src/sqlbuild/compiler/planner/_helpers/_test_types.py @@ -41,6 +41,24 @@ ) from sqlbuild.compiler.references.types import SqlReferenceKind from sqlbuild.spec.contracts.models import SourceEntry +from tests.unit.src.sqlbuild.compiler.planner._helpers.sql_test_assembly._test_types import ( + PlanTestChainTestCase as PlanTestChainTestCase, +) + + +@dataclass(frozen=True) +class FixturePlanningWorkTestCase: + description: str + test_count: int + expected_metadata_builds: int + expected_analysis_calls: int + + +@dataclass(frozen=True) +class CompleteFixturePlanningTestCase: + description: str + expected_analysis_calls: int + expected_fixture_sql: str @dataclass(frozen=True) diff --git a/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py b/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py new file mode 100644 index 0000000000..d79f5249ff --- /dev/null +++ b/tests/unit/src/sqlbuild/compiler/planner/_helpers/test_fixture_planning_work.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from dataclasses import replace +from unittest.mock import Mock + +import pytest + +from sqlbuild.compiler.compile.models import ( + CompiledModel, + CompiledObjectKey, + CompiledProject, + CompiledSqlTest, + CompileSqlReference, +) +from sqlbuild.compiler.compile.types import CompiledResourceType +from sqlbuild.compiler.planner._helpers.fixtures import completion as fixture_completion +from sqlbuild.compiler.planner._helpers.output.plan_output import build_selected_test_entries +from sqlbuild.compiler.planner.models import ( + FixtureColumnMetadata, + FixtureRelationMetadata, + PlanWarning, + RelationFixtureCompletion, + RelationFixturePlanningContext, + SqlTestPlanEntry, +) +from sqlbuild.compiler.references.types import SqlReferenceKind +from sqlbuild.spec.contracts.models import SettingsConfig +from tests.unit.src.sqlbuild.compiler.planner._helpers._test_types import ( + CompleteFixturePlanningTestCase, + FixturePlanningWorkTestCase, + PlanTestChainTestCase, +) +from tests.unit.src.sqlbuild.compiler.planner._helpers.helpers import PlannerTestAdapter +from tests.unit.src.sqlbuild.compiler.planner._helpers.sql_test_assembly.helpers import ( + build_test_and_project, +) + + +@pytest.mark.parametrize( + "test_case", + ( + FixturePlanningWorkTestCase( + description="shared project metadata", + test_count=250, + expected_metadata_builds=1, + expected_analysis_calls=1, + ), + ), + ids=lambda case: case.description, +) +def test_given_many_tests_when_building_entries_then_project_fixture_metadata_is_built_once( + test_case: FixturePlanningWorkTestCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + compiled_test, project = build_test_and_project( + PlanTestChainTestCase( + description=test_case.description, + model_queries={"orders": 'SELECT order_id FROM __source("raw_orders")'}, + mock_ref_ctes={}, + mock_source_ctes={"raw_orders": "SELECT 1 AS order_id"}, + helper_ctes={}, + expected_model_names=("orders",), + expected_chain_length=1, + expected_cte_bodies={"orders": "SELECT 1 AS order_id"}, + ) + ) + source_reference: CompileSqlReference = CompileSqlReference( + ref_kind=SqlReferenceKind.SOURCE, + ref_name="raw_orders", + ) + model: CompiledModel = replace( + project.models[0], + references=(source_reference,), + ) + tests: tuple[CompiledSqlTest, ...] = tuple( + replace( + compiled_test, + key=CompiledObjectKey( + resource_type=CompiledResourceType.SQL_TEST, + name=f"orders_case_{index:04d}", + ), + name=f"orders_case_{index:04d}", + ) + for index in range(test_case.test_count) + ) + test_project: CompiledProject = replace( + project, + models=(model,), + sql_tests=tests, + settings=SettingsConfig(sql_analysis=True), + ) + metadata_builder: Mock = Mock(wraps=fixture_completion._model_fixture_metadata) + monkeypatch.setattr(fixture_completion, "_model_fixture_metadata", metadata_builder) + fixture_completion._cached_resolved_column_reads.cache_clear() + fallback_analyzer: Mock = Mock(wraps=fixture_completion.analyze_resolved_column_reads) + monkeypatch.setattr( + fixture_completion, + "analyze_resolved_column_reads", + fallback_analyzer, + ) + + entries: list[SqlTestPlanEntry] + warnings: list[PlanWarning] + entries, warnings = build_selected_test_entries( + project=test_project, + adapter=PlannerTestAdapter(), + selected_keys=frozenset((model.key,)), + ) + + assert len(entries) == test_case.test_count + assert warnings == [] + assert metadata_builder.call_count == test_case.expected_metadata_builds + assert fallback_analyzer.call_count == test_case.expected_analysis_calls + + +@pytest.mark.parametrize( + "test_case", + ( + CompleteFixturePlanningTestCase( + description="complete authoritative fixture", + expected_analysis_calls=0, + expected_fixture_sql="SELECT 1 AS order_id", + ), + ), + ids=lambda case: case.description, +) +def test_given_complete_authoritative_fixture_when_completing_then_skips_column_analysis( + test_case: CompleteFixturePlanningTestCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _, project = build_test_and_project( + PlanTestChainTestCase( + description=test_case.description, + model_queries={"orders": 'SELECT order_id FROM __source("raw_orders")'}, + mock_ref_ctes={}, + mock_source_ctes={}, + helper_ctes={}, + expected_model_names=("orders",), + expected_chain_length=1, + ) + ) + source_reference: CompileSqlReference = CompileSqlReference( + ref_kind=SqlReferenceKind.SOURCE, + ref_name="raw_orders", + ) + model: CompiledModel = replace(project.models[0], references=(source_reference,)) + source_key: tuple[CompiledResourceType, str] = ( + CompiledResourceType.SOURCE, + "raw_orders", + ) + context: RelationFixturePlanningContext = RelationFixturePlanningContext( + models_by_name={model.name: model}, + relations={ + source_key: FixtureRelationMetadata( + columns=(FixtureColumnMetadata(name="order_id", type="INTEGER", nullable=False),), + authoritative_names=True, + ) + }, + authoritative_columns={source_key: frozenset(("order_id",))}, + expected_types={ + source_key: {"order_id": "INTEGER"}, + (CompiledResourceType.MODEL, model.name): {}, + }, + ) + fallback_analyzer: Mock = Mock( + side_effect=AssertionError("complete fixtures must not trigger model analysis") + ) + monkeypatch.setattr( + fixture_completion, + "analyze_resolved_column_reads", + fallback_analyzer, + ) + + completed: RelationFixtureCompletion = fixture_completion.build_relation_fixture_completion( + project=replace(project, models=(model,)), + adapter=PlannerTestAdapter(), + ordered_model_names=(model.name,), + fixture_groups=((CompiledResourceType.SOURCE, {"raw_orders": "SELECT 1 AS order_id"}),), + planning_context=context, + ) + + assert completed.diagnostics == () + assert completed.fixture_sql_by_key[source_key] == test_case.expected_fixture_sql + assert fallback_analyzer.call_count == test_case.expected_analysis_calls + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"])