Skip to content

Commit 2b13748

Browse files
committed
feat(native): batch dynamic pivot proofs and opt-out syntax checks through the native session
The finished analysis session is kept on CompiledProject.native_session and proves pivot models outside it from its own tables; without a session one batched standalone call sends the tables once. The per-model native proof path is deleted. SQL-analysis opt-out syntax checks use the native checker under the preview engine. Rule hosts drop the session before pickling.
1 parent 80ba17b commit 2b13748

42 files changed

Lines changed: 966 additions & 283 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Python's `analyze_dynamic_column_contract` over the crate's parse of the model SQL.
22
33
use std::collections::{HashMap, HashSet};
4+
use std::panic::{AssertUnwindSafe, catch_unwind};
45

56
use polyglot_sql::{ComplexityGuardOptions, Dialect, Expression, ParseOptions};
7+
use rayon::ThreadPool;
8+
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
69
use serde_json::{Map, Value, json};
710
use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic;
811

@@ -17,7 +20,7 @@ use crate::assembly::analysis_session::constants::{
1720
UNKNOWN_NULLABILITY, UNKNOWN_TYPE,
1821
};
1922
use crate::assembly::analysis_session::models::{
20-
ColumnFact, ContractProof, DynamicFamily, PivotOutcome,
23+
ColumnFact, ContractProof, DynamicFamily, PivotModel, PivotOutcome, PivotTables,
2124
};
2225
use crate::assembly::analysis_session::types::{Pairs, Shapes};
2326

@@ -33,6 +36,47 @@ pub(crate) struct PivotFacts<'a> {
3336
pub(crate) families_by_table: &'a [(String, Vec<DynamicFamily>)],
3437
}
3538

39+
/// Each model's proof in order; a model whose proof panics is deferred to Python.
40+
pub(crate) fn pivot_outcomes(models: &[PivotModel], tables: &PivotTables) -> Vec<PivotOutcome> {
41+
let facts = pivot_facts(tables);
42+
models
43+
.iter()
44+
.map(|model| guarded_outcome(model, &facts))
45+
.collect()
46+
}
47+
48+
/// Each model's proof in order on `pool`; a model whose proof panics is deferred to Python.
49+
pub(crate) fn pooled_pivot_outcomes(
50+
pool: &ThreadPool,
51+
models: &[PivotModel],
52+
tables: &PivotTables,
53+
) -> Vec<PivotOutcome> {
54+
let facts = pivot_facts(tables);
55+
pool.install(|| {
56+
models
57+
.par_iter()
58+
.map(|model| guarded_outcome(model, &facts))
59+
.collect()
60+
})
61+
}
62+
63+
fn pivot_facts(tables: &PivotTables) -> PivotFacts<'_> {
64+
PivotFacts {
65+
dialect: &tables.dialect,
66+
column_types: &tables.column_types,
67+
authoritative_types: &tables.authoritative_types,
68+
column_nullability: &tables.column_nullability,
69+
families_by_table: &tables.families_by_table,
70+
}
71+
}
72+
73+
fn guarded_outcome(model: &PivotModel, facts: &PivotFacts<'_>) -> PivotOutcome {
74+
catch_unwind(AssertUnwindSafe(|| {
75+
pivot_outcome(&model.sql, &model.families, facts)
76+
}))
77+
.unwrap_or(PivotOutcome::Deferred)
78+
}
79+
3680
/// One CTE body by folded name; None for a CTE with column aliases.
3781
type CteMap<'a> = HashMap<String, Option<&'a Value>>;
3882

‎crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/session.rs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! The session's phases: Python's uncached model analysis, wave by wave.
22
33
use std::collections::HashMap;
4+
use std::sync::Arc;
45

6+
use rayon::ThreadPool;
57
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
68

79
use crate::assembly::analysis_session::_helpers::catalog_state::SessionCatalog;
@@ -21,8 +23,9 @@ use crate::assembly::analysis_session::constants::{
2123
DEFERRAL_ANALYSIS, DEFERRAL_ENRICHMENT, UNKNOWN_NULLABILITY, UNKNOWN_TYPE,
2224
};
2325
use crate::assembly::analysis_session::models::{
24-
AnalysisSession, Awaiting, ColumnFact, Deferral, DeferredAnalysis, LineageFacts, ModelAnalysis,
25-
ModelOutcome, ModelRequest, Phase, PivotOutcome, SessionOutcome, SessionRequest, SessionStep,
26+
AnalysisSession, Awaiting, ColumnFact, Deferral, DeferredAnalysis, FinishedSession,
27+
LineageFacts, ModelAnalysis, ModelOutcome, ModelRequest, Phase, PivotOutcome, PivotTables,
28+
SessionOutcome, SessionRequest, SessionStep,
2629
};
2730
use crate::assembly::analysis_session::types::{Pairs, Shapes};
2831
use crate::semantic_validation::models::ProjectCatalog;
@@ -148,23 +151,36 @@ impl AnalysisSession {
148151
}
149152

150153
/// Every model's outcome and the binding catalog changes Python records.
151-
pub(crate) fn finish(self) -> Result<SessionOutcome, String> {
154+
pub(crate) fn finish(self) -> Result<(SessionOutcome, FinishedSession), String> {
152155
if !matches!(self.phase, Phase::Done) {
153156
return Err("the session has not finished".to_owned());
154157
}
155158
let dynamic_contracts: Vec<PivotOutcome> = self.dynamic_contracts()?;
159+
let pool: Result<Arc<ThreadPool>, String> = self.catalog.native.analysis_pool();
156160
let models: Vec<ModelOutcome> = self
157161
.outcomes
158162
.into_iter()
159163
.collect::<Option<_>>()
160164
.ok_or("the session left a model unanalysed")?;
161165
let (schema_additions, analysis_names) = self.catalog.into_changes();
162-
Ok(SessionOutcome {
166+
let request: SessionRequest = self.request;
167+
let finished = FinishedSession {
168+
tables: PivotTables {
169+
dialect: request.dialect,
170+
column_types: request.column_types,
171+
authoritative_types: request.complete_schemas,
172+
column_nullability: request.column_nullability,
173+
families_by_table: request.dynamic_families_by_table,
174+
},
175+
pool,
176+
};
177+
let outcome = SessionOutcome {
163178
models,
164179
schema_additions,
165180
analysis_names,
166181
dynamic_contracts,
167-
})
182+
};
183+
Ok((outcome, finished))
168184
}
169185

170186
/// Python's dynamic pivot proof of each model over the relation facts before analysis.
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//! Collect a finished native model analysis session's outcomes.
22
3-
use crate::assembly::analysis_session::models::{AnalysisSession, SessionOutcome};
3+
use crate::assembly::analysis_session::models::{AnalysisSession, FinishedSession, SessionOutcome};
44

5-
/// Every model's outcome and the binding catalog changes Python records.
6-
pub fn finish_analysis_session(session: AnalysisSession) -> Result<SessionOutcome, String> {
5+
/// Every model's outcome and catalog changes, and the session kept for later pivot proofs.
6+
pub fn finish_analysis_session(
7+
session: AnalysisSession,
8+
) -> Result<(SessionOutcome, FinishedSession), String> {
79
session.finish()
810
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod expression_shapes;
22
pub mod finish_analysis_session;
3-
pub mod prove_dynamic_contract;
3+
pub mod prove_dynamic_contracts;
4+
pub mod prove_finished_dynamic_contracts;
45
pub mod provide_deferred_analyses;
56
pub mod run_analysis_session;
67
pub mod start_analysis_session;

‎crates/sqlbuild-analysis/src/assembly/analysis_session/main/prove_dynamic_contract.rs‎

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//! Python's `analyze_dynamic_column_contract` for models no analysis session analysed.
2+
3+
use crate::assembly::analysis_session::_helpers::dynamic_pivot::pivot_outcomes;
4+
use crate::assembly::analysis_session::constants::PIVOT_WORKER_STACK_BYTES;
5+
use crate::assembly::analysis_session::models::{PivotBatchRequest, PivotOutcome};
6+
7+
/// Each model's proof in order; a model whose proof panics is deferred to Python.
8+
pub fn prove_dynamic_contracts(request: &PivotBatchRequest) -> Result<Vec<PivotOutcome>, String> {
9+
std::thread::scope(|scope| {
10+
std::thread::Builder::new()
11+
.stack_size(PIVOT_WORKER_STACK_BYTES)
12+
.spawn_scoped(scope, || pivot_outcomes(&request.models, &request.tables))
13+
.map_err(|error| error.to_string())?
14+
.join()
15+
.map_err(|_| "the native dynamic pivot proofs panicked".to_owned())
16+
})
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//! Python's `analyze_dynamic_column_contract` for models outside a finished analysis session.
2+
3+
use crate::assembly::analysis_session::_helpers::dynamic_pivot::pooled_pivot_outcomes;
4+
use crate::assembly::analysis_session::models::{FinishedSession, PivotModel, PivotOutcome};
5+
6+
/// Each model's proof in order from the session's own tables; a panicking proof is deferred.
7+
pub fn prove_finished_dynamic_contracts(
8+
session: &FinishedSession,
9+
models: &[PivotModel],
10+
) -> Result<Vec<PivotOutcome>, String> {
11+
let pool = session.pool.as_ref().map_err(Clone::clone)?;
12+
Ok(pooled_pivot_outcomes(pool, models, &session.tables))
13+
}

‎crates/sqlbuild-analysis/src/assembly/analysis_session/models.rs‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Plain-data request, deferrals and outcomes of the native model analysis session.
22
33
use std::collections::HashMap;
4+
use std::sync::Arc;
5+
6+
use rayon::ThreadPool;
47

58
use crate::assembly::analysis_session::_helpers::catalog_state::SessionCatalog;
69
use crate::assembly::analysis_session::_helpers::mappings::ShapeTable;
@@ -61,18 +64,37 @@ pub struct ContractProof {
6164
pub bare_dynamic_pivot: bool,
6265
}
6366

64-
/// One model's dynamic pivot proof inputs, for a model the session does not analyse.
67+
/// The relation facts every dynamic pivot proof reads: Python's tables before analysis.
6568
#[derive(Debug, Clone, Default)]
66-
pub struct PivotRequest {
69+
pub struct PivotTables {
6770
pub dialect: String,
6871
pub column_types: Shapes,
6972
pub authoritative_types: Shapes,
7073
pub column_nullability: Shapes,
7174
pub families_by_table: Vec<(String, Vec<DynamicFamily>)>,
75+
}
76+
77+
/// One model's pivot SQL and declared families.
78+
#[derive(Debug, Clone, Default)]
79+
pub struct PivotModel {
7280
pub sql: String,
7381
pub families: Vec<DynamicFamily>,
7482
}
7583

84+
/// Dynamic pivot proofs for models no session analysed.
85+
#[derive(Debug, Clone, Default)]
86+
pub struct PivotBatchRequest {
87+
pub tables: PivotTables,
88+
pub models: Vec<PivotModel>,
89+
}
90+
91+
/// A finished session's pivot tables and analysis pool, kept to prove models outside it.
92+
#[derive(Debug)]
93+
pub struct FinishedSession {
94+
pub(crate) tables: PivotTables,
95+
pub(crate) pool: Result<Arc<ThreadPool>, String>,
96+
}
97+
7698
/// A model's dynamic pivot proof: none declared, left to Python, or proven natively.
7799
#[derive(Debug, Clone, PartialEq, Eq)]
78100
pub enum PivotOutcome {

‎crates/sqlbuild-analysis/src/assembly/analysis_session/tests/helpers.rs‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use crate::assembly::analysis_session::main::run_analysis_session::run_analysis_
55
use crate::assembly::analysis_session::main::start_analysis_session::start_analysis_session;
66
use crate::assembly::analysis_session::models::{
77
AnalysisSession, ColumnFact, ContractProof, Deferral, DeferredAnalysis, DynamicFamily,
8-
ModelOutcome, ModelReference, ModelRequest, PivotOutcome, PivotRequest, SessionRequest,
9-
SessionStep,
8+
FinishedSession, ModelOutcome, ModelReference, ModelRequest, PivotBatchRequest, PivotModel,
9+
PivotOutcome, PivotTables, SessionRequest, SessionStep,
1010
};
1111
use crate::assembly::analysis_session::tests::test_types::ModelSpec;
1212
use crate::assembly::analysis_session::types::{Pairs, Shapes};
@@ -131,7 +131,7 @@ pub(crate) fn completed(
131131
})
132132
})
133133
.collect();
134-
let outcome = finish_analysis_session(session).expect("the session finished");
134+
let (outcome, _) = finish_analysis_session(session).expect("the session finished");
135135
(steps, outcome.models)
136136
}
137137

@@ -215,12 +215,12 @@ pub(crate) fn session_lines(models: &[ModelSpec]) -> (Vec<Vec<String>>, Vec<Vec<
215215
)
216216
}
217217

218-
/// A pivot proof request over typed `raw_orders`, whose upstream `status_amounts` is a pivot.
218+
/// Proofs over typed `raw_orders` and pivot `status_amounts`: the model, then a family-less one.
219219
pub(crate) fn pivot_request(
220220
dialect: &str,
221221
sql: &str,
222222
family: Option<(&str, &str, &str)>,
223-
) -> PivotRequest {
223+
) -> PivotBatchRequest {
224224
let raw_orders: Shapes = shapes(&[(
225225
"raw_orders",
226226
&[
@@ -237,17 +237,35 @@ pub(crate) fn pivot_request(
237237
})
238238
.into_iter()
239239
.collect();
240-
PivotRequest {
241-
dialect: dialect.to_owned(),
242-
column_types: [raw_orders.clone(), upstream].concat(),
243-
authoritative_types: raw_orders,
244-
column_nullability: shapes(&[("raw_orders", &[("customer_id", "non_null")])]),
245-
families_by_table: vec![(
246-
"status_amounts".to_owned(),
247-
vec![amounts("status", "amount", "MAX")],
248-
)],
249-
sql: sql.to_owned(),
250-
families,
240+
PivotBatchRequest {
241+
tables: PivotTables {
242+
dialect: dialect.to_owned(),
243+
column_types: [raw_orders.clone(), upstream].concat(),
244+
authoritative_types: raw_orders,
245+
column_nullability: shapes(&[("raw_orders", &[("customer_id", "non_null")])]),
246+
families_by_table: vec![(
247+
"status_amounts".to_owned(),
248+
vec![amounts("status", "amount", "MAX")],
249+
)],
250+
},
251+
models: vec![
252+
PivotModel {
253+
sql: sql.to_owned(),
254+
families,
255+
},
256+
PivotModel {
257+
sql: "SELECT 1".to_owned(),
258+
families: Vec::new(),
259+
},
260+
],
261+
}
262+
}
263+
264+
/// A finished session holding `tables` and an analysis pool.
265+
pub(crate) fn finished_session(tables: PivotTables) -> FinishedSession {
266+
FinishedSession {
267+
tables,
268+
pool: catalog("duckdb", &Vec::new()).analysis_pool(),
251269
}
252270
}
253271

‎crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use crate::assembly::analysis_session::main::prove_dynamic_contract::prove_dynamic_contract;
1+
use crate::assembly::analysis_session::main::prove_dynamic_contracts::prove_dynamic_contracts;
2+
use crate::assembly::analysis_session::main::prove_finished_dynamic_contracts::prove_finished_dynamic_contracts;
23
use crate::assembly::analysis_session::main::start_analysis_session::start_analysis_session;
34
use crate::assembly::analysis_session::models::PivotOutcome;
45
use crate::assembly::analysis_session::tests::helpers::{
5-
catalog, failed, model_requests, orders_request, pivot_request, proven, session_lines,
6+
catalog, failed, finished_session, model_requests, orders_request, pivot_request, proven,
7+
session_lines,
68
};
79
use crate::assembly::analysis_session::tests::test_types::{
810
PivotTestCase, SessionTestCase, UnscheduledTestCase,
@@ -281,11 +283,21 @@ fn given_dynamic_pivots_when_proving_then_matches_python_or_defers() {
281283
];
282284
for test_case in test_cases {
283285
let request = pivot_request(test_case.dialect, test_case.sql, test_case.family);
286+
let session = finished_session(request.tables.clone());
284287

285-
let outcome = prove_dynamic_contract(&request).expect("the proof runs");
288+
let standalone = prove_dynamic_contracts(&request).expect("the proofs run");
289+
let in_session =
290+
prove_finished_dynamic_contracts(&session, &request.models).expect("the proofs run");
286291

287292
assert_eq!(
288-
outcome, test_case.expected_outcome,
293+
standalone,
294+
vec![test_case.expected_outcome.clone(), PivotOutcome::Absent],
295+
"{}",
296+
test_case.description
297+
);
298+
assert_eq!(
299+
in_session,
300+
vec![test_case.expected_outcome, PivotOutcome::Absent],
289301
"{}",
290302
test_case.description
291303
);

0 commit comments

Comments
 (0)