1use std::collections::HashMap;
15use std::sync::Arc;
16
17use super::super::executor::StreamingExecutor;
18use super::super::operators::apply_operator::ApplyOperator;
19use super::super::operators::apply_operator::ApplyOperatorKind;
20use super::super::operators::base::OperatorBase;
21use super::super::operators::blocking::BlockingOperator;
22use super::super::operators::ddl_operator::DdlOperator;
23use super::super::operators::exchange_operator::ExchangeOperator;
24use super::super::operators::fulltext_operator::FulltextOperator;
25use super::super::operators::graph_operator::GraphOperator;
26use super::super::operators::join_operator::JoinOperator;
27use super::super::operators::recursive_fragment_operator::RecursiveFragmentOperator;
28use super::super::operators::set_operator::SetOperator;
29use super::super::operators::sink_operator::SinkOperator;
30use super::super::operators::source_operator::SourceOperator;
31use super::super::operators::source_operator::SourceOperatorKind;
32use super::super::operators::spec::ApplySpec;
33use super::super::operators::txn_operator::TxnOperator;
34use super::super::operators::unary_operator::UnaryOperator;
35use super::super::operators::vector_operator::VectorOperator;
36use super::super::operators::wco_operator::WcoIntersectOperator;
37use super::super::runtime::ExecutionRuntime;
38use super::super::subquery::SubqueryExecutor;
39use super::types::{
40 FragmentGraph, FragmentId, FragmentSpec, InputContract, OperatorKindSpec, PhysicalPlan,
41};
42use crate::executor::base::MemoryTracker;
43use linkrs_core::error::QueryError;
44
45use super::super::instance::QueryBindings;
46
47pub struct PhysicalPlanMaterializer;
57
58impl PhysicalPlanMaterializer {
59 pub fn materialize(
64 plan: &PhysicalPlan,
65 bindings: &QueryBindings,
66 ) -> Result<(StreamingExecutor, Arc<ExecutionRuntime>), QueryError> {
67 Self::validate_bindings(plan, bindings)?;
68
69 let mut mutable_bindings = bindings.clone();
73 if !plan.parameter_schema.is_empty() {
74 mutable_bindings.build_parameter_frame(&plan.parameter_schema);
75 }
76
77 let parameter_values = if let Some(ref frame) = mutable_bindings.parameter_frame {
78 let map: std::collections::HashMap<String, linkrs_core::Value> = plan
79 .parameter_schema
80 .params
81 .iter()
82 .filter_map(|p| frame.get(p.slot).map(|v| (p.name.clone(), v.clone())))
83 .collect();
84 if map.is_empty() {
85 None
86 } else {
87 Some(std::sync::Arc::new(map))
88 }
89 } else {
90 None
91 };
92
93 let runtime = Self::create_runtime(&mutable_bindings, parameter_values);
94
95 let topo_order = Self::topological_order(&plan.fragments)?;
96
97 let mut fragment_roots: HashMap<FragmentId, StreamingExecutor> = HashMap::new();
99
100 for &fid in &topo_order {
101 let fragment = plan
102 .fragments
103 .get(fid)
104 .ok_or_else(|| QueryError::execution(format!("Fragment {:?} not found", fid)))?;
105
106 let executor = Self::build_fragment_pipeline(
107 fragment,
108 plan,
109 &mut fragment_roots,
110 &runtime,
111 &mutable_bindings,
112 )?;
113 fragment_roots.insert(fid, executor);
114 }
115
116 let root_executor = fragment_roots.remove(&plan.root_fragment).ok_or_else(|| {
117 QueryError::execution(format!(
118 "Root fragment {:?} has no materialized executor",
119 plan.root_fragment
120 ))
121 })?;
122
123 Ok((root_executor, runtime))
124 }
125
126 fn build_fragment_pipeline(
134 fragment: &FragmentSpec,
135 plan: &PhysicalPlan,
136 fragment_roots: &mut HashMap<FragmentId, StreamingExecutor>,
137 runtime: &Arc<ExecutionRuntime>,
138 bindings: &QueryBindings,
139 ) -> Result<StreamingExecutor, QueryError> {
140 let mut previous = None;
141
142 for (operator_index, &op_id) in fragment.operators.iter().enumerate() {
143 let op_spec = plan.operator(op_id).ok_or_else(|| {
144 QueryError::execution(format!("Operator {:?} not found in plan arena", op_id))
145 })?;
146
147 let plan_node_id = op_spec
148 .logical_node_id
149 .map(|logical_id| logical_id.0)
150 .unwrap_or(op_spec.operator_id.0 as i64);
151 let output_layout = Arc::new(op_spec.output_layout.clone());
152 let base = OperatorBase::new(plan_node_id)
153 .with_physical_operator_id(op_spec.operator_id)
154 .with_output_layout(output_layout.clone());
155
156 let mut inputs = if operator_index == 0 {
157 take_external_inputs(&op_spec.input_contract, fragment_roots)?
158 } else {
159 vec![previous.take().ok_or_else(|| {
160 QueryError::execution(format!(
161 "Fragment {:?} lost its linear pipeline before operator {:?}",
162 fragment.id, op_id
163 ))
164 })?]
165 };
166
167 let exec = match &op_spec.spec {
168 OperatorKindSpec::Source(src_spec) => {
169 require_input_count(fragment.id, op_id, &inputs, 0)?;
170 let storage = bindings.storage.clone();
171 let op = SourceOperator::from_spec(src_spec, storage, output_layout.clone());
172 StreamingExecutor::Source(base, op)
173 }
174 OperatorKindSpec::Unary(unary_spec) => {
175 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
176 let mut op = UnaryOperator::from_spec(unary_spec, output_layout.clone());
177 let runner_specs = unary_spec.subquery_runners();
181 if !runner_specs.is_empty() {
182 let executor = Arc::new(SubqueryExecutor::from_specs(
183 runtime.clone(),
184 Arc::new(stripped_bindings(bindings)),
185 runner_specs,
186 ));
187 op.set_subquery_executor(executor);
188 }
189 StreamingExecutor::Unary(base, Box::new(child), op)
190 }
191 OperatorKindSpec::Blocking(blocking_spec) => {
192 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
193 let op = BlockingOperator::from_spec(
194 blocking_spec,
195 &bindings.memory_budget,
196 output_layout.clone(),
197 );
198 StreamingExecutor::Blocking(base, Box::new(child), op)
199 }
200 OperatorKindSpec::Join(join_spec) => {
201 let (left, right) = take_binary_inputs(fragment.id, op_id, inputs)?;
202 let op = JoinOperator::from_spec(
203 join_spec,
204 &bindings.memory_budget,
205 output_layout.clone(),
206 );
207 StreamingExecutor::Join(base, Box::new(left), Box::new(right), Box::new(op))
208 }
209 OperatorKindSpec::Wco(wco_spec) => {
210 require_input_count(fragment.id, op_id, &inputs, 1 + wco_spec.num_builds())?;
211 let mut inputs = inputs.into_iter();
212 let probe = inputs.next().ok_or_else(|| {
213 QueryError::execution(format!(
214 "Wco operator {:?} in fragment {:?} has no probe input",
215 op_id, fragment.id
216 ))
217 })?;
218 let builds: Vec<StreamingExecutor> = inputs.collect();
219 let op = WcoIntersectOperator::from_spec(
220 wco_spec,
221 &bindings.memory_budget,
222 output_layout.clone(),
223 );
224 StreamingExecutor::Wco(base, Box::new(probe), builds, op)
225 }
226 OperatorKindSpec::Graph(graph_spec) => {
227 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
228 let storage = runtime.storage.clone();
229 let space_name = runtime.query_id().space_name.clone().unwrap_or_default();
230 let op = GraphOperator::from_spec(
231 graph_spec,
232 storage,
233 space_name,
234 output_layout.clone(),
235 );
236 StreamingExecutor::Graph(base, Box::new(child), op)
237 }
238 OperatorKindSpec::RecursiveFragment(rf_spec) => {
239 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
240 let storage = runtime.storage.clone();
241 let space_name = runtime.query_id().space_name.clone().unwrap_or_default();
242 let op = RecursiveFragmentOperator::from_spec(
243 rf_spec,
244 storage,
245 space_name,
246 output_layout.clone(),
247 );
248 StreamingExecutor::RecursiveFragment(base, Box::new(child), op)
249 }
250 OperatorKindSpec::Sink(sink_spec) => {
251 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
252 let storage = runtime.storage.clone();
253 let op = SinkOperator::from_spec(sink_spec, storage, output_layout.clone());
254 StreamingExecutor::Sink(base, Box::new(child), op)
255 }
256 OperatorKindSpec::Set(set_spec) => {
257 let (left, right) = take_binary_inputs(fragment.id, op_id, inputs)?;
258 let op = SetOperator::from_spec(
259 set_spec,
260 &bindings.memory_budget,
261 output_layout.clone(),
262 );
263 StreamingExecutor::Set(base, Box::new(left), Box::new(right), op)
264 }
265 OperatorKindSpec::Apply(apply_spec) => match apply_spec {
266 ApplySpec::CorrelatedApply { sub_plan, anti } => {
267 let left = take_unary_input(fragment.id, op_id, &mut inputs)?;
268 let right = StreamingExecutor::Source(
269 OperatorBase::new(0),
270 SourceOperator::new(SourceOperatorKind::Start, output_layout.clone()),
271 );
272 let op = ApplyOperator::new(
273 ApplyOperatorKind::CorrelatedApply {
274 sub_plan: sub_plan.clone(),
275 bindings: Box::new(stripped_bindings(bindings)),
276 sub_executor: None,
277 anti: *anti,
278 right_rows: None,
279 right_layout: None,
280 memory_tracker: MemoryTracker::new(bindings.memory_budget.clone()),
281 },
282 output_layout.clone(),
283 );
284 StreamingExecutor::Apply(base, Box::new(left), Box::new(right), op)
285 }
286 _ => {
287 let (left, right) = take_binary_inputs(fragment.id, op_id, inputs)?;
288 let op = ApplyOperator::from_spec(
289 apply_spec,
290 &bindings.memory_budget,
291 output_layout.clone(),
292 );
293 StreamingExecutor::Apply(base, Box::new(left), Box::new(right), op)
294 }
295 },
296 OperatorKindSpec::Exchange(exchange_spec) => {
297 if inputs.is_empty() {
298 return Err(QueryError::execution(format!(
299 "Exchange operator {:?} in fragment {:?} has no inputs",
300 op_id, fragment.id
301 )));
302 }
303 let children = inputs;
304 let op = ExchangeOperator::from_spec(exchange_spec, output_layout.clone());
305 StreamingExecutor::Exchange(base, children, op)
306 }
307 OperatorKindSpec::Ddl(ddl_spec) => {
308 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
309 let storage = runtime.storage.clone();
310 let op = DdlOperator::from_spec(ddl_spec, storage, output_layout.clone());
311 StreamingExecutor::Ddl(base, Box::new(child), op)
312 }
313 OperatorKindSpec::Fulltext(ft_spec) => {
314 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
315 let storage = runtime.storage.clone();
316 #[cfg(feature = "fulltext")]
317 let ft_mgr = runtime.search.fulltext_manager.clone();
318 let op = FulltextOperator::from_spec(
319 ft_spec,
320 storage,
321 #[cfg(feature = "fulltext")]
322 ft_mgr,
323 output_layout.clone(),
324 );
325 StreamingExecutor::Fulltext(base, Box::new(child), op)
326 }
327 OperatorKindSpec::Vector(vector_spec) => {
328 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
329 let storage = runtime.storage.clone();
330 #[cfg(feature = "vector")]
331 let coord = runtime.search.vector_coordinator.clone();
332 let op = VectorOperator::from_spec(
333 vector_spec,
334 storage,
335 #[cfg(feature = "vector")]
336 coord,
337 output_layout.clone(),
338 );
339 StreamingExecutor::Vector(base, Box::new(child), op)
340 }
341 OperatorKindSpec::Txn(txn_spec) => {
342 let child = take_unary_input(fragment.id, op_id, &mut inputs)?;
343 let op = TxnOperator::from_spec(txn_spec, output_layout.clone());
344 StreamingExecutor::Txn(base, Box::new(child), op)
345 }
346 };
347
348 previous = Some(exec);
349 }
350
351 let mut root_executor = previous.ok_or_else(|| {
352 QueryError::execution(format!("Fragment {:?} produced no operators", fragment.id))
353 })?;
354
355 root_executor.set_chunk_size(bindings.chunk_size);
356 root_executor.set_runtime(Some(runtime.clone()));
357
358 Ok(root_executor)
359 }
360
361 fn topological_order(fragments: &FragmentGraph) -> Result<Vec<FragmentId>, QueryError> {
364 let root = fragments.root();
365 let mut order = Vec::new();
366 let mut visited = std::collections::HashSet::new();
367 let mut stack = vec![root];
368
369 while let Some(fid) = stack.pop() {
370 if !visited.insert(fid) {
371 continue;
372 }
373 if let Some(frag) = fragments.get(fid) {
374 for &input in &frag.inputs {
375 stack.push(input);
376 }
377 order.push(fid);
378 }
379 }
380
381 order.reverse();
382
383 if order.is_empty() {
384 return Err(QueryError::execution("Fragment graph is empty".to_string()));
385 }
386
387 Ok(order)
388 }
389
390 fn validate_bindings(plan: &PhysicalPlan, bindings: &QueryBindings) -> Result<(), QueryError> {
397 let schema = &plan.parameter_schema;
398 let mut errors: Vec<String> = Vec::new();
399
400 for param in &schema.params {
402 if !bindings.parameters.contains_key(¶m.name) && param.default.is_none() {
403 errors.push(format!("Missing required parameter: {}", param.name));
404 }
405 }
406
407 for name in bindings.parameters.keys() {
409 if schema.slot(name).is_none() {
410 errors.push(format!("Unknown parameter: {}", name));
411 }
412 }
413
414 for param in &schema.params {
416 if let Some(value) = bindings.parameters.get(¶m.name) {
417 if let Some(ref expected_type) = param.value_type {
418 if !Self::type_compatible(value, expected_type) {
419 errors.push(format!(
420 "Parameter '{}': expected type {:?}, got value {:?}",
421 param.name, expected_type, value
422 ));
423 }
424 }
425 }
426 }
427
428 if errors.is_empty() {
429 Ok(())
430 } else {
431 Err(QueryError::execution(errors.join("; ")))
432 }
433 }
434
435 fn type_compatible(value: &linkrs_core::Value, expected_type: &linkrs_core::DataType) -> bool {
439 use linkrs_core::DataType;
440 use linkrs_core::Value;
441 matches!(
442 (value, expected_type),
443 (Value::Null(_), _)
444 | (Value::Bool(_), DataType::Bool)
445 | (Value::Int(_), DataType::Int | DataType::BigInt)
446 | (Value::BigInt(_), DataType::BigInt | DataType::Int)
447 | (Value::Float(_), DataType::Float | DataType::Double)
448 | (Value::Double(_), DataType::Double | DataType::Float)
449 | (Value::String(_), DataType::String)
450 | (Value::Date(_), DataType::Date)
451 | (Value::Time(_), DataType::Time)
452 | (Value::DateTime(_), DataType::DateTime)
453 ) || Self::type_compatible_composite(value, expected_type)
454 }
455
456 fn type_compatible_composite(
459 value: &linkrs_core::Value,
460 expected_type: &linkrs_core::DataType,
461 ) -> bool {
462 use linkrs_core::DataType;
463 use linkrs_core::Value;
464 match (value, expected_type) {
465 (Value::Struct(s), DataType::Struct(expected)) => {
466 s.fields.iter().all(|(name, field_value)| {
469 expected
470 .fields
471 .iter()
472 .find(|(field_name, _)| field_name == name)
473 .is_some_and(|(_, field_type)| {
474 Self::type_compatible(field_value, field_type)
475 })
476 })
477 }
478 (Value::Array(a), DataType::Array(expected)) => {
479 let length_ok = expected
480 .len
481 .is_none_or(|declared_len| a.values.len() == declared_len);
482 length_ok
483 && a.values
484 .iter()
485 .all(|element| Self::type_compatible(element, &expected.element))
486 }
487 _ => false,
488 }
489 }
490
491 fn create_runtime(
499 bindings: &QueryBindings,
500 parameter_values: Option<
501 std::sync::Arc<std::collections::HashMap<String, linkrs_core::Value>>,
502 >,
503 ) -> Arc<ExecutionRuntime> {
504 let mut runtime = ExecutionRuntime::new(
505 crate::executor::streaming::runtime::QueryIdentity {
506 query_id: bindings.query_id,
507 session_id: None,
508 space_name: bindings.space_name.clone(),
509 },
510 bindings.memory_budget.clone(),
511 bindings.storage.clone(),
512 bindings.search.clone(),
513 );
514
515 match bindings.transaction {
517 crate::executor::streaming::transaction_scope::TransactionScope::None => {
518 }
521 ref scope => {
522 runtime.set_transaction_scope(scope.clone());
523 }
524 }
525
526 if let Some(values) = parameter_values {
528 runtime.set_parameter_values(values);
529 }
530
531 if !bindings.session_variables.is_empty() {
534 runtime.set_session_variable_values(bindings.session_variables.clone());
535 }
536
537 if bindings.macro_manager.is_some() {
540 runtime.set_macro_manager(bindings.macro_manager.clone());
541 }
542 if bindings.type_alias_manager.is_some() {
543 runtime.set_type_alias_manager(bindings.type_alias_manager.clone());
544 }
545 if bindings.sequence_manager.is_some() {
546 runtime.set_sequence_manager(bindings.sequence_manager.clone());
547 }
548
549 if let Some(ref ss) = bindings.shared_scheduler {
551 runtime.set_shared_scheduler(Some(ss.clone()));
552 } else if bindings.max_workers > 1 {
553 let pool =
554 crate::executor::streaming::pool::MorselWorkerPool::new(bindings.max_workers);
555 runtime.set_worker_pool(Some(pool));
556 }
557
558 runtime.feedback_history = bindings.feedback_history.clone();
562
563 runtime.set_columnar_policy(bindings.columnar_policy.clone());
567
568 if bindings.partition_count > 0 {
571 runtime.set_partition_count(bindings.partition_count + 1);
572 }
573
574 runtime.arena = bindings.arena.clone();
576
577 Arc::new(runtime)
578 }
579}
580
581fn stripped_bindings(bindings: &QueryBindings) -> QueryBindings {
590 let mut stripped = bindings.clone();
591 stripped.parameters = Arc::new(HashMap::new());
592 stripped.parameter_frame = None;
593 stripped
594}
595
596fn take_external_inputs(
597 contract: &InputContract,
598 fragment_roots: &mut HashMap<FragmentId, StreamingExecutor>,
599) -> Result<Vec<StreamingExecutor>, QueryError> {
600 let ids: Vec<FragmentId> = match contract {
601 InputContract::NoInput => Vec::new(),
602 InputContract::UnaryInput(input) => vec![input.fragment],
603 InputContract::BinaryInputs { left, right } => vec![left.fragment, right.fragment],
604 InputContract::PartitionedInputs { members, .. } => {
605 let mut ordered = members.iter().collect::<Vec<_>>();
606 ordered.sort_by_key(|member| member.partition_id);
607 ordered.into_iter().map(|member| member.fragment).collect()
608 }
609 InputContract::WcoInputs { probe, builds } => {
610 let mut ids = Vec::with_capacity(builds.len() + 1);
611 ids.push(probe.fragment);
612 ids.extend(builds.iter().map(|build| build.fragment));
613 ids
614 }
615 };
616
617 ids.into_iter()
618 .map(|fragment_id| {
619 fragment_roots.remove(&fragment_id).ok_or_else(|| {
620 QueryError::execution(format!(
621 "Input contract references unavailable producer fragment {:?}",
622 fragment_id
623 ))
624 })
625 })
626 .collect()
627}
628
629fn require_input_count(
630 fragment_id: FragmentId,
631 operator_id: super::types::PhysicalOperatorId,
632 inputs: &[StreamingExecutor],
633 expected: usize,
634) -> Result<(), QueryError> {
635 if inputs.len() == expected {
636 Ok(())
637 } else {
638 Err(QueryError::execution(format!(
639 "Operator {:?} in fragment {:?} received {} inputs, expected {}",
640 operator_id,
641 fragment_id,
642 inputs.len(),
643 expected
644 )))
645 }
646}
647
648fn take_unary_input(
649 fragment_id: FragmentId,
650 operator_id: super::types::PhysicalOperatorId,
651 inputs: &mut Vec<StreamingExecutor>,
652) -> Result<StreamingExecutor, QueryError> {
653 require_input_count(fragment_id, operator_id, inputs, 1)?;
654 inputs.pop().ok_or_else(|| {
655 QueryError::execution(format!("Operator {:?} has no unary input", operator_id))
656 })
657}
658
659fn take_binary_inputs(
660 fragment_id: FragmentId,
661 operator_id: super::types::PhysicalOperatorId,
662 mut inputs: Vec<StreamingExecutor>,
663) -> Result<(StreamingExecutor, StreamingExecutor), QueryError> {
664 require_input_count(fragment_id, operator_id, &inputs, 2)?;
665 let right = inputs.pop().ok_or_else(|| {
666 QueryError::execution(format!("Operator {:?} has no right input", operator_id))
667 })?;
668 let left = inputs.pop().ok_or_else(|| {
669 QueryError::execution(format!("Operator {:?} has no left input", operator_id))
670 })?;
671 Ok((left, right))
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::executor::base::ExecutionContext;
678 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
679 use linkrs_core::types::{ContextualExpression, ExpressionMeta};
680
681 use crate::executor::streaming::parameters::ParameterSchema;
682 use crate::executor::streaming::plan::arena_builder::PhysicalPlanBuilder;
683 use crate::executor::streaming::plan::context::PhysicalPlanBuildContext;
684 use crate::executor::streaming::transaction_scope::TransactionScope;
685 use crate::planning::plan::core::nodes::base::plan_node_enum::PlanNodeEnum;
686 use crate::planning::plan::core::nodes::base::plan_node_traits::PlanNode;
687 use crate::planning::plan::core::nodes::control_flow::start_node::StartNode;
688 use crate::planning::plan::core::nodes::control_flow::ArgumentNode;
689 use crate::planning::plan::core::nodes::graph_operations::graph_operations_node::CorrelatedApplyNode;
690 use crate::planning::plan::core::nodes::join::CrossJoinNode;
691 use crate::planning::plan::core::nodes::operation::filter_node::FilterNode;
692 use std::collections::HashMap;
693
694 fn build_correlated_apply_plan() -> Arc<PhysicalPlan> {
695 let left = PlanNodeEnum::Start(StartNode::new());
696
697 let mut argument = ArgumentNode::new(-2, "_correlated_apply");
698 argument.set_col_names(vec!["t".to_string()]);
699 let cross = CrossJoinNode::new(PlanNodeEnum::Argument(argument), left.clone())
700 .expect("cross join should build");
701 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
702 let expr_id = expr_ctx.register_expression(ExpressionMeta::new(
703 linkrs_core::Expression::Literal(linkrs_core::Value::Bool(true)),
704 ));
705 let condition = ContextualExpression::new(expr_id, expr_ctx);
706 let filter = FilterNode::new(cross.into_enum(), condition).expect("filter should build");
707 let right = PlanNodeEnum::Filter(filter);
708
709 let apply = CorrelatedApplyNode::new(left, right, false).expect("apply should build");
710 let node = PlanNodeEnum::CorrelatedApply(apply);
711
712 let mut ctx = PhysicalPlanBuildContext::new();
713 let exec_ctx = ExecutionContext::new(Arc::new(ExpressionAnalysisContext::new()));
714 let plan =
715 PhysicalPlanBuilder::build(&node, &mut ctx, &exec_ctx).expect("plan should build");
716 let plan = Arc::new(plan);
717 crate::executor::streaming::plan::validator::PhysicalPlanValidator::validate(&plan)
718 .expect("plan should validate");
719 plan
720 }
721
722 fn bindings() -> QueryBindings {
723 QueryBindings {
724 parameters: Arc::new(HashMap::new()),
725 session_variables: Arc::new(HashMap::new()),
726 parameter_frame: None,
727 space_name: None,
728 storage: None,
729 bound_snapshot: None,
730 memory_budget: crate::executor::base::MemoryBudget::default_budget(),
731 max_workers: 1,
732 chunk_size: 2048,
733 max_buffered_chunks: 4,
734 query_id: 1,
735 cancel_token: None,
736 session_id: None,
737 user_name: None,
738 query_text: None,
739 transaction: TransactionScope::None,
740 shared_scheduler: None,
741 partition_count: 0,
742 arena: None,
743 feedback_history: None,
744 columnar_policy: None,
745 macro_manager: None,
746 type_alias_manager: None,
747 sequence_manager: None,
748 search: crate::executor::base::SearchContext::default(),
749 }
750 }
751
752 #[test]
753 fn materialize_correlated_apply_uses_unary_input_and_start_placeholder() {
754 let plan = build_correlated_apply_plan();
755 let (root, _runtime) =
756 PhysicalPlanMaterializer::materialize(&plan, &bindings()).expect("should materialize");
757
758 let (left, right, op) = match root {
759 StreamingExecutor::Apply(_, left, right, op) => (left, right, op),
760 other => panic!("expected Apply executor, got {:?}", other.operator_name()),
761 };
762 assert!(
763 matches!(op.kind, ApplyOperatorKind::CorrelatedApply { .. }),
764 "CorrelatedApply spec must materialize into the CorrelatedApply operator"
765 );
766 assert!(
767 matches!(&*left, StreamingExecutor::Source(_, op) if matches!(op.kind, SourceOperatorKind::Start)),
768 "left input comes from the unary input fragment (Start source)"
769 );
770 assert!(
771 matches!(&*right, StreamingExecutor::Source(_, op) if matches!(op.kind, SourceOperatorKind::Start)),
772 "right input is the Start placeholder (never executed)"
773 );
774 }
775
776 #[test]
777 fn stripped_bindings_clears_parameters_for_nested_materialize() {
778 let plan = build_correlated_apply_plan();
779 let apply_spec = plan
780 .operators
781 .iter()
782 .find_map(|op| match &op.spec {
783 OperatorKindSpec::Apply(
784 super::super::super::operators::spec::ApplySpec::CorrelatedApply {
785 sub_plan,
786 ..
787 },
788 ) => Some(sub_plan.clone()),
789 _ => None,
790 })
791 .expect("plan contains CorrelatedApply");
792
793 assert!(
795 apply_spec.parameter_schema.is_empty(),
796 "nested sub-plan must have an empty parameter schema"
797 );
798
799 let schema = ParameterSchema::new(vec![
802 crate::executor::streaming::parameters::ParameterDesc {
803 name: "p".to_string(),
804 slot: crate::executor::streaming::parameters::ParameterSlot(0),
805 value_type: Some(linkrs_core::DataType::String),
806 nullable: true,
807 default: None,
808 },
809 ]);
810 let mut raw = bindings();
811 raw.parameters = Arc::new(
812 [("p".into(), linkrs_core::Value::string("value"))]
813 .into_iter()
814 .collect(),
815 );
816 raw.build_parameter_frame(&schema);
817 assert!(!raw.parameters.is_empty());
818 assert!(raw.parameter_frame.is_some());
819
820 let stripped = stripped_bindings(&raw);
821 assert!(
822 stripped.parameters.is_empty(),
823 "stripped bindings must drop the parameter map"
824 );
825 assert!(
826 stripped.parameter_frame.is_none(),
827 "stripped bindings must drop the parameter frame"
828 );
829
830 let (sub_root, _runtime) = PhysicalPlanMaterializer::materialize(&apply_spec, &stripped)
832 .expect("nested sub-plan should materialize without Unknown parameter");
833 assert!(sub_root.operator_name() == "Filter" || sub_root.operator_name() == "CrossJoin");
834 }
835}