3131 FixtureRelationMetadata ,
3232 RelationFixtureCompletion ,
3333 RelationFixtureDiagnostic ,
34+ RelationFixturePlanningContext ,
3435)
3536from sqlbuild .compiler .planner .types import ContractPolicy , FixtureGroups , FixtureKey
3637from sqlbuild .compiler .references .constants import REF_PATTERN , SEED_PATTERN , SOURCE_PATTERN
@@ -47,10 +48,14 @@ def build_relation_fixture_completion(
4748 adapter : BaseAdapter ,
4849 ordered_model_names : tuple [str , ...],
4950 fixture_groups : FixtureGroups ,
51+ planning_context : RelationFixturePlanningContext | None = None ,
5052) -> RelationFixtureCompletion :
5153 """Complete required omitted columns with typed nulls unless explicitly non-nullable."""
5254
53- model_map : dict [str , CompiledModel ] = {model .name : model for model in project .models }
55+ context : RelationFixturePlanningContext = planning_context or build_relation_fixture_context (
56+ project = project
57+ )
58+ model_map : dict [str , CompiledModel ] = context .models_by_name
5459 fixture_sql_by_key : dict [FixtureKey , str ] = {}
5560 inferred_by_fixture : dict [FixtureKey , tuple [InferredColumn , ...]] = {}
5661 for resource_type , fixtures in fixture_groups :
@@ -64,21 +69,23 @@ def build_relation_fixture_completion(
6469 if inferred is not None :
6570 inferred_by_fixture [key ] = inferred
6671
67- relations : dict [FixtureKey , FixtureRelationMetadata ] = _fixture_relation_metadata (
68- project = project ,
69- model_map = model_map ,
72+ relations : dict [FixtureKey , FixtureRelationMetadata ] = context .relations
73+ fixture_keys_requiring_analysis : frozenset [FixtureKey ] = _fixture_keys_requiring_analysis (
74+ inferred_by_fixture = inferred_by_fixture ,
75+ authoritative_columns = context .authoritative_columns ,
7076 )
7177 required : dict [FixtureKey , dict [str , set [str ]]]
7278 star_fixture_keys : frozenset [FixtureKey ]
7379 required , star_fixture_keys = _required_fixture_columns (
7480 ordered_model_names = ordered_model_names ,
7581 model_map = model_map ,
76- fixture_keys = frozenset ( fixture_sql_by_key ) ,
82+ fixture_keys = fixture_keys_requiring_analysis ,
7783 relations = relations ,
84+ authoritative_columns = context .authoritative_columns ,
7885 adapter = adapter ,
7986 )
8087 expected_types : dict [FixtureKey , dict [str , str ]] = _expected_fixture_types (
81- relations = relations ,
88+ base_expected_types = context . expected_types ,
8289 ordered_model_names = ordered_model_names ,
8390 model_map = model_map ,
8491 )
@@ -177,23 +184,61 @@ def build_relation_fixture_completion(
177184 )
178185
179186
187+ def _fixture_keys_requiring_analysis (
188+ * ,
189+ inferred_by_fixture : dict [FixtureKey , tuple [InferredColumn , ...]],
190+ authoritative_columns : dict [FixtureKey , frozenset [str ]],
191+ ) -> frozenset [FixtureKey ]:
192+ required : set [FixtureKey ] = set ()
193+ for key , inferred in inferred_by_fixture .items ():
194+ expected_names : frozenset [str ] | None = authoritative_columns .get (key )
195+ supplied_names : frozenset [str ] = frozenset (column .name .casefold () for column in inferred )
196+ if expected_names is None or not expected_names .issubset (supplied_names ):
197+ required .add (key )
198+ return frozenset (required )
199+
200+
201+ def build_relation_fixture_context (* , project : CompiledProject ) -> RelationFixturePlanningContext :
202+ """Build immutable project-wide fixture metadata once per planning invocation."""
203+
204+ models_by_name : dict [str , CompiledModel ] = {model .name : model for model in project .models }
205+ relations : dict [FixtureKey , FixtureRelationMetadata ] = _fixture_relation_metadata (
206+ project = project ,
207+ model_map = models_by_name ,
208+ )
209+ authoritative_columns : dict [FixtureKey , frozenset [str ]] = _authoritative_fixture_columns (
210+ relations = relations
211+ )
212+ return RelationFixturePlanningContext (
213+ models_by_name = models_by_name ,
214+ relations = relations ,
215+ authoritative_columns = authoritative_columns ,
216+ expected_types = _base_expected_fixture_types (relations = relations ),
217+ )
218+
219+
220+ def _authoritative_fixture_columns (
221+ * , relations : dict [FixtureKey , FixtureRelationMetadata ]
222+ ) -> dict [FixtureKey , frozenset [str ]]:
223+ authoritative : dict [FixtureKey , frozenset [str ]] = {}
224+ for key , relation in relations .items ():
225+ if relation .authoritative_names :
226+ authoritative [key ] = frozenset (column .name .casefold () for column in relation .columns )
227+ return authoritative
228+
229+
180230def _required_fixture_columns (
181231 * ,
182232 ordered_model_names : tuple [str , ...],
183233 model_map : dict [str , CompiledModel ],
184234 fixture_keys : frozenset [FixtureKey ],
185235 relations : dict [FixtureKey , FixtureRelationMetadata ],
236+ authoritative_columns : dict [FixtureKey , frozenset [str ]],
186237 adapter : BaseAdapter ,
187238) -> tuple [dict [FixtureKey , dict [str , set [str ]]], frozenset [FixtureKey ]]:
188239 required : dict [FixtureKey , dict [str , set [str ]]] = {}
189240 star_fixture_keys : set [FixtureKey ] = set ()
190- authoritative_columns : dict [FixtureKey , frozenset [str ]] = {}
191- for key , relation in relations .items ():
192- if not relation .authoritative_names :
193- continue
194- authoritative_columns [key ] = frozenset (
195- column .name .casefold () for column in relation .columns
196- )
241+ dialect : str | None = adapter .expression_inference_profile ().sql_analysis_dialect
197242 for model_name in ordered_model_names :
198243 model : CompiledModel | None = model_map .get (model_name )
199244 if model is None :
@@ -203,7 +248,7 @@ def _required_fixture_columns(
203248 references = model .references ,
204249 fast_lineage = model .fast_lineage_columns or (),
205250 fixture_keys = fixture_keys ,
206- dialect = adapter . expression_inference_profile (). sql_analysis_dialect ,
251+ dialect = dialect ,
207252 ):
208253 key : FixtureKey = (
209254 CompiledResourceType (upstream .resource_type ),
@@ -451,22 +496,18 @@ def _model_fixture_metadata(*, model: CompiledModel) -> FixtureRelationMetadata:
451496
452497def _expected_fixture_types (
453498 * ,
454- relations : dict [FixtureKey , FixtureRelationMetadata ],
499+ base_expected_types : dict [FixtureKey , dict [ str , str ] ],
455500 ordered_model_names : tuple [str , ...],
456501 model_map : dict [str , CompiledModel ],
457502) -> dict [FixtureKey , dict [str , str ]]:
458- expected_types : dict [FixtureKey , dict [str , str ]] = {}
459- for key , relation in relations .items ():
460- column_types : dict [str , str ] = {}
461- for column in relation .columns :
462- if column .type is not None :
463- column_types [column .name .casefold ()] = column .type
464- expected_types [key ] = column_types
503+ expected_types : dict [FixtureKey , dict [str , str ]] = dict (base_expected_types )
465504 for model_name in ordered_model_names :
466505 model : CompiledModel | None = model_map .get (model_name )
467506 if model is None :
468507 continue
469- model_types : dict [str , str ] = expected_types [(CompiledResourceType .MODEL , model_name )]
508+ model_key : FixtureKey = (CompiledResourceType .MODEL , model_name )
509+ model_types : dict [str , str ] = dict (expected_types [model_key ])
510+ expected_types [model_key ] = model_types
470511 for lineage_column in model .fast_lineage_columns or ():
471512 if lineage_column .transform_kind != ColumnTransformKind .DIRECT :
472513 continue
@@ -484,6 +525,19 @@ def _expected_fixture_types(
484525 return expected_types
485526
486527
528+ def _base_expected_fixture_types (
529+ * , relations : dict [FixtureKey , FixtureRelationMetadata ]
530+ ) -> dict [FixtureKey , dict [str , str ]]:
531+ expected_types : dict [FixtureKey , dict [str , str ]] = {}
532+ for key , relation in relations .items ():
533+ column_types : dict [str , str ] = {}
534+ for column in relation .columns :
535+ if column .type is not None :
536+ column_types [column .name .casefold ()] = column .type
537+ expected_types [key ] = column_types
538+ return expected_types
539+
540+
487541def _unknown_fixture_column_diagnostics (
488542 * ,
489543 inferred_by_fixture : dict [FixtureKey , tuple [InferredColumn , ...]],
0 commit comments