1use super::context::PhysicalPlanBuildContext;
7use std::collections::HashMap;
8
9use super::types::{
10 CapabilitySet, FragmentGraph, FragmentSpec, LogicalNodeId, OutputContract, PhysicalOperatorId,
11 PhysicalOperatorIdAllocator, PhysicalOperatorSpec, PhysicalPlan, PlanCompatibility,
12 PlanFingerprint,
13};
14use crate::executor::base::ExecutionContext;
15use crate::executor::build_error::PlanBuildError;
16use crate::planning::plan::core::nodes::base::plan_node_enum::PlanNodeEnum;
17
18mod assembler;
19mod metadata;
20mod partition;
21mod specs;
22
23use assembler::{ArenaFragmentAllocator, ArenaPlanAssembler};
24
25pub struct PhysicalPlanBuilder;
27
28impl PhysicalPlanBuilder {
29 pub fn build(
38 node: &PlanNodeEnum,
39 ctx: &mut PhysicalPlanBuildContext,
40 exec_ctx: &ExecutionContext,
41 ) -> Result<PhysicalPlan, PlanBuildError> {
42 let mut partitioned = None;
43 if let Some(spec) = ctx.partition_spec.clone() {
44 match partition::build_partitioned(node, &spec, exec_ctx) {
45 Ok(Some(result)) => {
46 ctx.parallel_fallback_reason.clear();
47 partitioned = Some(result);
48 }
49 Ok(None) => {
50 if ctx.parallel_fallback_reason.is_empty() {
51 ctx.parallel_fallback_reason =
52 "plan shape does not support partitioned execution".to_string();
53 }
54 }
55 Err(error) => {
56 if ctx.parallel_fallback_reason.is_empty() {
57 ctx.parallel_fallback_reason =
58 format!("partitioned execution unavailable ({error})");
59 }
60 }
61 }
62 }
63
64 let (mut operators, mut fragments, root_fid, root_op_id) = match partitioned {
65 Some((operators, fragments, root_fid, root_op_id)) => {
66 (operators, fragments, root_fid, root_op_id)
67 }
68 None => Self::build_serial(node, exec_ctx)?,
69 };
70
71 Self::finalize(&mut operators, &mut fragments, root_fid, root_op_id, ctx)
72 }
73
74 fn build_serial(
76 node: &PlanNodeEnum,
77 exec_ctx: &ExecutionContext,
78 ) -> Result<
79 (
80 Vec<PhysicalOperatorSpec>,
81 Vec<FragmentSpec>,
82 super::types::FragmentId,
83 PhysicalOperatorId,
84 ),
85 PlanBuildError,
86 > {
87 let mut operators: Vec<PhysicalOperatorSpec> = Vec::new();
88 let mut fragments: Vec<FragmentSpec> = Vec::new();
89 let mut op_alloc = PhysicalOperatorIdAllocator::new();
90 let mut frag_alloc = ArenaFragmentAllocator::new();
91
92 let (root_fid, root_op_id) = ArenaPlanAssembler::convert_node(
93 node,
94 &mut operators,
95 &mut fragments,
96 &mut op_alloc,
97 &mut frag_alloc,
98 exec_ctx,
99 )?;
100
101 Ok((operators, fragments, root_fid, root_op_id))
102 }
103
104 fn finalize(
106 operators: &mut Vec<PhysicalOperatorSpec>,
107 fragments: &mut Vec<FragmentSpec>,
108 root_fid: super::types::FragmentId,
109 root_op_id: PhysicalOperatorId,
110 ctx: &PhysicalPlanBuildContext,
111 ) -> Result<PhysicalPlan, PlanBuildError> {
112 metadata::propagate_layouts(operators, fragments)?;
113 metadata::populate_input_contracts(operators, fragments)?;
114 metadata::populate_runtime_metadata(operators);
115 metadata::populate_choice_reasons(operators);
116 metadata::populate_estimated_rows(
117 operators,
118 fragments,
119 &ctx.statistics.per_node_row_estimates,
120 );
121
122 let output = operators
123 .iter()
124 .find(|op| op.operator_id == root_op_id)
125 .map(|op| metadata::output_contract(&op.spec, op.output_layout.clone()))
126 .unwrap_or_else(|| OutputContract {
127 output_layout: super::super::slot::SlotLayout::new(vec![]),
128 always_produces_row: false,
129 nullability: Vec::new(),
130 ordering: Vec::new(),
131 delivery_streamable: true,
132 pipeline_mode: super::types::PipelineMode::Pipelined,
133 });
134
135 let mut logical_to_physical: HashMap<LogicalNodeId, Vec<PhysicalOperatorId>> =
136 HashMap::new();
137 for operator in &*operators {
138 if let Some(logical_id) = operator.logical_node_id {
139 logical_to_physical
140 .entry(logical_id)
141 .or_default()
142 .push(operator.operator_id);
143 }
144 }
145 let fingerprint = PlanFingerprint::compute(operators);
146
147 let mut required_capabilities = CapabilitySet::EMPTY;
148 for operator in &*operators {
149 required_capabilities.insert(metadata::capability_for_operator(&operator.spec));
150 }
151
152 let plan = PhysicalPlan {
153 operators: std::mem::take(operators),
154 logical_to_physical,
155 fragments: FragmentGraph::new(std::mem::take(fragments), root_fid),
156 root_fragment: root_fid,
157 output,
158 compatibility: PlanCompatibility {
159 fingerprint,
160 layout_version: ctx.schema.as_ref().map(|s| s.layout_version),
161 required_capabilities: required_capabilities.clone(),
162 planning_config_hash: ctx.config.config_hash,
163 optimizer_version: ctx.config.optimizer_version,
164 },
165 required_capabilities,
166 parameter_schema: ctx.parameter_schema.clone(),
167 parallel_fallback_reason: ctx.parallel_fallback_reason.clone(),
168 cbo_notes: ctx.cbo_notes.clone(),
169 statistics_summary: ctx.statistics_summary.clone(),
170 partition_spec: ctx.partition_spec.clone(),
171 };
172
173 Ok(plan)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::executor::base::ExecutionContext;
181 use crate::executor::streaming::plan::types::{InputContract, OperatorKindSpec};
182 use crate::executor::streaming::SourceSpec;
183 use crate::planning::plan::core::nodes::control_flow::start_node::StartNode;
184 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
185 use std::sync::Arc;
186
187 fn test_context() -> ExecutionContext {
188 ExecutionContext::new(Arc::new(ExpressionAnalysisContext::new()))
189 }
190
191 #[test]
192 fn test_build_start() {
193 let start = StartNode::new();
194 let node = PlanNodeEnum::Start(start);
195 let mut ctx = PhysicalPlanBuildContext::new();
196 let exec_ctx = test_context();
197
198 let plan = PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).unwrap();
199 assert_eq!(plan.operator_count(), 1);
200 assert!(matches!(
201 plan.operator(PhysicalOperatorId(0)).unwrap().spec,
202 OperatorKindSpec::Source(SourceSpec::Start)
203 ));
204 }
205
206 #[test]
207 fn test_build_start_has_explain_name() {
208 let start = StartNode::new();
209 let node = PlanNodeEnum::Start(start);
210 let mut ctx = PhysicalPlanBuildContext::new();
211 let exec_ctx = test_context();
212
213 let plan = PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).unwrap();
214 let op = plan.operator(PhysicalOperatorId(0)).unwrap();
215 assert_eq!(op.explain_name, "Start");
216 }
217
218 #[test]
219 fn test_build_then_materialize_start() {
220 use crate::executor::base::MemoryBudget;
221 use crate::executor::streaming::instance::{
222 QueryBindings, QueryExecutionInstance, ResultSink,
223 };
224 use crate::executor::streaming::transaction_scope::TransactionScope;
225 use std::sync::Arc;
226
227 let start = StartNode::new();
228 let node = PlanNodeEnum::Start(start);
229 let mut ctx = PhysicalPlanBuildContext::new();
230 let exec_ctx = test_context();
231
232 let plan = PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).unwrap();
233 let plan_arc = Arc::new(plan);
234
235 crate::executor::streaming::plan::validator::PhysicalPlanValidator::validate(&plan_arc)
236 .unwrap();
237
238 let bindings = QueryBindings {
239 parameters: Arc::new(std::collections::HashMap::new()),
240 session_variables: Arc::new(std::collections::HashMap::new()),
241 parameter_frame: None,
242 space_name: None,
243 storage: None,
244 bound_snapshot: None,
245 memory_budget: MemoryBudget::default_budget(),
246 max_workers: 1,
247 chunk_size: 2048,
248 max_buffered_chunks: 4,
249 query_id: 1,
250 cancel_token: None,
251 session_id: None,
252 user_name: None,
253 query_text: None,
254 transaction: TransactionScope::None,
255 shared_scheduler: None,
256 partition_count: 0,
257 arena: None,
258 feedback_history: None,
259 columnar_policy: None,
260 macro_manager: None,
261 type_alias_manager: None,
262 sequence_manager: None,
263 search: crate::executor::base::SearchContext::default(),
264 };
265
266 let result = QueryExecutionInstance::instantiate_plan(
267 plan_arc,
268 bindings,
269 ResultSink::Discard,
270 None,
271 None,
272 );
273 assert!(result.is_ok());
274 }
275
276 #[test]
277 fn propagates_layout_through_blocking_pipeline_and_output_contract() {
278 use crate::planning::plan::core::nodes::control_flow::start_node::StartNode;
279 use crate::planning::plan::core::nodes::operation::sort_node::LimitNode;
280
281 let start = StartNode::new();
282 let limit =
283 LimitNode::new(PlanNodeEnum::Start(start), 0, 10).expect("limit plan should build");
284 let node = PlanNodeEnum::Limit(limit);
285 let mut ctx = PhysicalPlanBuildContext::new();
286 let exec_ctx = test_context();
287
288 let plan = PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).unwrap();
289 let start_op = plan.operator(PhysicalOperatorId(0)).unwrap();
290 let limit_op = plan.operator(PhysicalOperatorId(1)).unwrap();
291
292 assert_eq!(start_op.explain_name, "Start");
293 assert_eq!(limit_op.explain_name, "Limit");
294 assert_eq!(plan.fragment_count(), 1);
295 }
296
297 #[test]
298 fn transaction_command_has_start_input_fragment() {
299 use crate::planning::plan::core::nodes::control_flow::BeginTransactionNode;
300
301 let node = PlanNodeEnum::BeginTransaction(BeginTransactionNode::new(42));
302 let mut ctx = PhysicalPlanBuildContext::new();
303 let exec_ctx = test_context();
304
305 let plan = PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx)
306 .expect("transaction plan should build");
307 assert_eq!(plan.operator_count(), 2);
308 assert_eq!(plan.fragment_count(), 2);
309
310 let root = plan
311 .fragments
312 .get(plan.root_fragment)
313 .expect("root fragment should exist");
314 assert_eq!(root.inputs.len(), 1);
315 assert!(matches!(
316 plan.operator(root.root_operator)
317 .expect("transaction root operator should exist")
318 .input_contract,
319 InputContract::UnaryInput(_)
320 ));
321 crate::executor::streaming::plan::validator::PhysicalPlanValidator::validate(&Arc::new(
322 plan,
323 ))
324 .expect("transaction plan should validate");
325 }
326
327 #[test]
328 fn correlated_apply_builds_nested_sub_plan_rooted_at_argument() {
329 use crate::executor::streaming::operators::spec::ApplySpec;
330 use crate::planning::plan::core::nodes::base::plan_node_traits::PlanNode;
331 use crate::planning::plan::core::nodes::control_flow::ArgumentNode;
332 use crate::planning::plan::core::nodes::graph_operations::graph_operations_node::CorrelatedApplyNode;
333 use crate::planning::plan::core::nodes::join::CrossJoinNode;
334 use crate::planning::plan::core::nodes::operation::filter_node::FilterNode;
335 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
336 use linkrs_core::types::{ContextualExpression, ExpressionMeta};
337
338 let left = PlanNodeEnum::Start(StartNode::new());
340
341 let mut argument = ArgumentNode::new(-2, "_correlated_apply");
344 argument.set_col_names(vec!["t".to_string()]);
345 let cross = CrossJoinNode::new(PlanNodeEnum::Argument(argument), left.clone())
346 .expect("cross join should build");
347 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
348 let expr_id = expr_ctx.register_expression(ExpressionMeta::new(
349 linkrs_core::Expression::Literal(linkrs_core::Value::Bool(true)),
350 ));
351 let condition = ContextualExpression::new(expr_id, expr_ctx);
352 let filter = FilterNode::new(cross.into_enum(), condition).expect("filter should build");
353 let right = PlanNodeEnum::Filter(filter);
354
355 let apply =
356 CorrelatedApplyNode::new(left, right, false).expect("correlated apply should build");
357 let node = PlanNodeEnum::CorrelatedApply(apply);
358
359 let mut ctx = PhysicalPlanBuildContext::new();
360 let exec_ctx = test_context();
361 let plan =
362 PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).expect("plan should build");
363
364 let apply_op = plan
365 .operators
366 .iter()
367 .find(|op| {
368 matches!(
369 op.spec,
370 OperatorKindSpec::Apply(ApplySpec::CorrelatedApply { .. })
371 )
372 })
373 .expect("plan contains a CorrelatedApply operator");
374
375 let sub_plan = match &apply_op.spec {
376 OperatorKindSpec::Apply(ApplySpec::CorrelatedApply { sub_plan, anti }) => {
377 assert!(!anti, "correlated apply defaults to semi");
378 sub_plan
379 }
380 _ => unreachable!("spec match earlier guarantees the variant"),
381 };
382
383 let argument_source = sub_plan
386 .operators
387 .iter()
388 .find(|op| {
389 matches!(
390 op.spec,
391 OperatorKindSpec::Source(SourceSpec::Argument { .. })
392 )
393 })
394 .expect("sub-plan must be rooted at an Argument source");
395 match &argument_source.spec {
396 OperatorKindSpec::Source(SourceSpec::Argument { col_names }) => {
397 assert_eq!(
398 col_names,
399 &["t".to_string()],
400 "Argument col_names mirror the outer layout"
401 );
402 }
403 _ => unreachable!(),
404 }
405 }
406}