@@ -31,9 +31,9 @@ use crate::assembly::analysis_session::_helpers::mappings::{
3131use crate :: assembly:: analysis_session:: _helpers:: mappings:: { producers, waves} ;
3232use crate :: assembly:: analysis_session:: constants:: { UNKNOWN_NULLABILITY , UNKNOWN_TYPE } ;
3333use crate :: assembly:: analysis_session:: models:: {
34- AnalysisSession , ColumnFact , ContractProof , FinishedSession , LineageFacts , LineageRow ,
35- ModelAnalysis , ModelOutcome , ModelRequest , Phase , PivotOutcome , PivotTables , SessionModelFacts ,
36- SessionOutcome , SessionRequest , SessionStep ,
34+ AnalysisSession , ColumnFact , ContractProof , DeferredCleaning , FinishedSession , LineageFacts ,
35+ LineageRow , ModelAnalysis , ModelOutcome , ModelRequest , Phase , PivotOutcome , PivotTables ,
36+ SessionModelFacts , SessionOutcome , SessionRequest , SessionStep ,
3737} ;
3838use crate :: assembly:: analysis_session:: types:: { Pairs , Shapes } ;
3939use crate :: semantic_validation:: models:: ProjectCatalog ;
@@ -63,6 +63,7 @@ impl AnalysisSession {
6363 available_nullability : ShapeTable :: from_shapes ( & request. column_nullability ) ,
6464 complete_shapes,
6565 outcomes : vec ! [ None ; request. models. len( ) ] ,
66+ pending_cleaned : vec ! [ false ; request. models. len( ) ] ,
6667 waves : if dependency_ordered || request. models . is_empty ( ) {
6768 waves
6869 } else {
@@ -112,13 +113,26 @@ impl AnalysisSession {
112113 }
113114 let dynamic_contracts: Vec < PivotOutcome > = self . dynamic_contracts ( ) ?;
114115 let pool: Result < Arc < ThreadPool > , String > = self . catalog . native . analysis_pool ( ) ;
115- let models: Vec < ModelOutcome > = self
116+ let mut models: Vec < ModelOutcome > = self
116117 . outcomes
117118 . into_iter ( )
118119 . collect :: < Option < _ > > ( )
119120 . ok_or ( "the session left a model unanalysed" ) ?;
120121 let ( schema_additions, analysis_names) = self . catalog . into_changes ( ) ;
121- let request: SessionRequest = self . request ;
122+ let mut request: SessionRequest = self . request ;
123+ for ( ( outcome, model) , pending) in models
124+ . iter_mut ( )
125+ . zip ( request. models . iter_mut ( ) )
126+ . zip ( self . pending_cleaned )
127+ {
128+ if pending {
129+ outcome. deferred_cleaning = Some ( DeferredCleaning {
130+ dialect : request. dialect . clone ( ) ,
131+ query_sql : std:: mem:: take ( & mut model. query_sql ) ,
132+ placeholders : std:: mem:: take ( & mut model. placeholders ) ,
133+ } ) ;
134+ }
135+ }
122136 let mut model_facts: HashMap < String , SessionModelFacts > = HashMap :: new ( ) ;
123137 let mut dynamic_proofs: HashMap < String , ContractProof > = HashMap :: new ( ) ;
124138 for ( ( model, outcome) , proof) in request. models . iter ( ) . zip ( & models) . zip ( & dynamic_contracts)
@@ -247,6 +261,7 @@ impl AnalysisSession {
247261 self . outcomes [ * model] = Some ( ModelOutcome {
248262 analysis,
249263 cleaned_sql : result. cleaned_sql ,
264+ deferred_cleaning : None ,
250265 validated_schema : schema,
251266 fused_binding_validated : true ,
252267 } ) ;
@@ -394,8 +409,8 @@ impl AnalysisSession {
394409 /// Python's deferred binding validation, then the dataflow's shape publication.
395410 fn finish_wave ( & mut self , wave : usize ) -> Result < ( ) , String > {
396411 let models: Vec < usize > = self . waves [ wave] . clone ( ) ;
397- let mut validations: Vec < ( usize , String , Shapes ) > = Vec :: new ( ) ;
398412 let validated: & [ usize ] = if self . cyclic { & [ ] } else { & models } ;
413+ let mut revalidated: Vec < ( usize , Shapes ) > = Vec :: new ( ) ;
399414 for model in validated {
400415 let schema: Shapes = self . binding_schema ( & self . request . models [ * model] ) ;
401416 let outcome: & ModelOutcome = self . outcomes [ * model]
@@ -404,9 +419,18 @@ impl AnalysisSession {
404419 if !outcome. fused_binding_validated
405420 || !same_relations ( & outcome. validated_schema , & schema)
406421 {
407- validations . push ( ( * model, outcome . cleaned_sql . clone ( ) , schema) ) ;
422+ revalidated . push ( ( * model, schema) ) ;
408423 }
409424 }
425+ let pending: Vec < usize > = revalidated. iter ( ) . map ( |( model, _) | * model) . collect ( ) ;
426+ self . ensure_cleaned ( & pending) ?;
427+ let mut validations: Vec < ( usize , String , Shapes ) > = Vec :: new ( ) ;
428+ for ( model, schema) in revalidated {
429+ let outcome: & ModelOutcome = self . outcomes [ model]
430+ . as_ref ( )
431+ . ok_or ( "a wave model was not analysed" ) ?;
432+ validations. push ( ( model, outcome. cleaned_sql . clone ( ) , schema) ) ;
433+ }
410434 if !validations. is_empty ( ) {
411435 let requests: Vec < ( & str , & Shapes ) > = validations
412436 . iter ( )
@@ -421,6 +445,9 @@ impl AnalysisSession {
421445 }
422446 }
423447 if self . dependency_ordered {
448+ if self . request . case_sensitive_shapes {
449+ self . ensure_cleaned ( & models) ?;
450+ }
424451 for model in & models {
425452 self . publish ( * model) ?;
426453 }
@@ -500,26 +527,11 @@ impl AnalysisSession {
500527 None => misses. push ( position) ,
501528 }
502529 }
503- let members: Vec < BatchMember < ' _ > > = hits
504- . iter ( )
505- . map ( |( position, _) | {
506- batch_member ( & self . request . models [ models[ * position] ] , & schemas[ * position] )
507- } )
508- . collect ( ) ;
509- let cleaned: Vec < Result < String , String > > = self
510- . catalog
511- . normalize_each ( & self . request . dialect , & members)
512- . unwrap_or_else ( |error| members. iter ( ) . map ( |_| Err ( error. clone ( ) ) ) . collect ( ) ) ;
513- for ( ( position, mut outcome) , sql) in hits. into_iter ( ) . zip ( cleaned) {
530+ for ( position, outcome) in hits {
514531 let model: usize = models[ position] ;
515- match sql {
516- Ok ( sql) => {
517- outcome. cleaned_sql = sql;
518- self . outcomes [ model] = Some ( outcome) ;
519- cache. hits [ model] = true ;
520- }
521- Err ( _) => misses. push ( position) ,
522- }
532+ self . outcomes [ model] = Some ( outcome) ;
533+ self . pending_cleaned [ model] = true ;
534+ cache. hits [ model] = true ;
523535 }
524536 misses. sort_unstable ( ) ;
525537 cache. stats . hits += models. len ( ) - misses. len ( ) ;
@@ -528,6 +540,38 @@ impl AnalysisSession {
528540 Ok ( misses)
529541 }
530542
543+ /// Normalize the cleaned SQL of the cache hits among `models` that still lack it.
544+ fn ensure_cleaned ( & mut self , models : & [ usize ] ) -> Result < ( ) , String > {
545+ let pending: Vec < usize > = models
546+ . iter ( )
547+ . copied ( )
548+ . filter ( |model| self . pending_cleaned [ * model] )
549+ . collect ( ) ;
550+ if pending. is_empty ( ) {
551+ return Ok ( ( ) ) ;
552+ }
553+ let schemas: Vec < Shapes > = pending
554+ . iter ( )
555+ . map ( |model| self . binding_schema ( & self . request . models [ * model] ) )
556+ . collect ( ) ;
557+ let members: Vec < BatchMember < ' _ > > = pending
558+ . iter ( )
559+ . zip ( & schemas)
560+ . map ( |( model, schema) | batch_member ( & self . request . models [ * model] , schema) )
561+ . collect ( ) ;
562+ let cleaned: Vec < Result < String , String > > = self
563+ . catalog
564+ . normalize_each ( & self . request . dialect , & members) ?;
565+ for ( model, sql) in pending. into_iter ( ) . zip ( cleaned) {
566+ let sql: String = sql. map_err ( |reason| {
567+ native_failure ( & model_context ( & self . request . models [ model] ) , & reason)
568+ } ) ?;
569+ self . outcome_mut ( model) ?. cleaned_sql = sql;
570+ self . pending_cleaned [ model] = false ;
571+ }
572+ Ok ( ( ) )
573+ }
574+
531575 /// Store each finished native outcome of `models` the cache did not answer.
532576 fn store_cached ( & mut self , models : & [ usize ] ) {
533577 let Some ( cache) = self . cache . as_mut ( ) else {
0 commit comments