Skip to main content

linkrs_query/executor/streaming/plan/
context.rs

1//! PhysicalPlanBuildContext: read-only context for building physical plans.
2//!
3//! Contains only schema catalog, statistics, capability information, and
4//! planning configuration — never runtime handles, storage clients,
5//! transaction handles, or per-query mutable state.
6//!
7//! This ensures the resulting [`PhysicalPlan`](super::types::PhysicalPlan)
8//! is immutable, cacheable, and safe to share across concurrent executions.
9
10use super::types::{FragmentIdAllocator, PhysicalOperatorIdAllocator};
11use crate::executor::base::ExecutionContext;
12use crate::executor::streaming::parameters::ParameterSchema;
13use crate::executor::streaming::slot::SlotLayout;
14use crate::planning::plan::PartitionSpec;
15use linkrs_core::error::QueryError;
16
17/// Schema identifier for catalog lookups during plan building.
18#[derive(Debug, Clone)]
19pub struct SchemaRef {
20    pub space_name: String,
21    pub layout_version: u64,
22}
23
24/// Snapshot of table/index statistics used for cardinality estimation.
25#[derive(Debug, Clone, Default)]
26pub struct StatisticsSnapshot {
27    pub row_count_estimates: Vec<(String, u64)>,
28    /// Per-logical-node output row estimates from the cost-based phase,
29    /// keyed by logical node id. Consumed by the `estimated_rows` metadata
30    /// pass to write `estimated_cardinality` onto physical operators.
31    pub per_node_row_estimates: std::collections::HashMap<i64, u64>,
32}
33
34/// Planning configuration flags and thresholds.
35#[derive(Debug, Clone)]
36pub struct PlanningConfig {
37    /// Maximum number of partitions to generate.
38    pub max_partitions: usize,
39    /// Whether to enable hash join.
40    pub enable_hash_join: bool,
41    /// Optimizer rule set version.
42    pub optimizer_version: u64,
43    /// Hash of this config for cache compatibility.
44    pub config_hash: u64,
45}
46
47impl Default for PlanningConfig {
48    fn default() -> Self {
49        Self {
50            max_partitions: 4,
51            enable_hash_join: true,
52            optimizer_version: 1,
53            config_hash: 0,
54        }
55    }
56}
57
58/// Read-only context used during [`PhysicalPlan`] construction.
59///
60/// Intentionally free of:
61/// - `QueryStorage`, transaction/session handles
62/// - Runtime, memory tracker, cursor, buffer, emitted state
63/// - Per-execution parameter values, auth context, current snapshot
64/// - Temporary space/storage references that belong in bindings
65#[derive(Debug, Clone)]
66pub struct PhysicalPlanBuildContext {
67    /// Schema catalog reference (space schema + layout version).
68    pub schema: Option<SchemaRef>,
69    /// Statistics snapshot for cardinality estimation.
70    pub statistics: StatisticsSnapshot,
71    /// Planning configuration.
72    pub config: PlanningConfig,
73    /// The output slot layout expected by the parent (if known at build time).
74    pub expected_output_layout: Option<SlotLayout>,
75
76    /// Parameter schema for prepared-statement parameters.
77    pub parameter_schema: ParameterSchema,
78
79    /// optional partition spec for partitioned execution.
80    /// When set, the builder produces a PhysicalPlan with multiple source
81    /// fragments and exchange/gather fragments instead of a single linear chain.
82    pub partition_spec: Option<PartitionSpec>,
83
84    /// Why parallel partitioning was not applied (empty = partitioning
85    /// active or not requested). Copied into the built [`PhysicalPlan`] so
86    /// EXPLAIN / PROFILE diagnostics can surface it.
87    pub parallel_fallback_reason: String,
88
89    /// Cost-based decision notes (unnest / join order) copied from the
90    /// optimized [`ExecutionPlan`] into the built [`PhysicalPlan`] so
91    /// EXPLAIN diagnostics can surface why the optimizer chose a shape.
92    pub cbo_notes: Vec<String>,
93
94    /// Effective statistics collection settings (sampling window and
95    /// refresh threshold) copied from the pipeline into the built
96    /// [`PhysicalPlan`] so EXPLAIN diagnostics can surface them.
97    pub statistics_summary: String,
98
99    // ── Allocators ──
100    pub(crate) operator_id_alloc: PhysicalOperatorIdAllocator,
101    pub(crate) fragment_id_alloc: FragmentIdAllocator,
102    // NOTE: capacity planning and spill config is not yet wired.
103}
104
105impl PhysicalPlanBuildContext {
106    /// Create a new build context from an [`ExecutionContext`], extracting
107    /// only the immutable, plan-relevant portions.
108    ///
109    /// Note: `ExecutionContext` still carries runtime-bound state that
110    /// should eventually be separated.  This constructor extracts what it
111    /// needs and leaves the rest behind.
112    pub fn from_execution_context(context: &ExecutionContext) -> Self {
113        Self {
114            schema: context.space_name.as_ref().map(|space_name| SchemaRef {
115                space_name: space_name.clone(),
116                layout_version: 0,
117            }),
118            statistics: StatisticsSnapshot::default(),
119            config: PlanningConfig {
120                max_partitions: context.max_workers,
121                ..PlanningConfig::default()
122            },
123            expected_output_layout: None,
124            parameter_schema: ParameterSchema::default(),
125            partition_spec: None,
126            parallel_fallback_reason: String::new(),
127            cbo_notes: Vec::new(),
128            statistics_summary: String::new(),
129            operator_id_alloc: PhysicalOperatorIdAllocator::new(),
130            fragment_id_alloc: FragmentIdAllocator::new(),
131        }
132    }
133
134    /// Create a minimal context for testing or simple scans.
135    pub fn new() -> Self {
136        Self {
137            schema: None,
138            statistics: StatisticsSnapshot::default(),
139            config: PlanningConfig::default(),
140            expected_output_layout: None,
141            parameter_schema: ParameterSchema::default(),
142            partition_spec: None,
143            parallel_fallback_reason: String::new(),
144            cbo_notes: Vec::new(),
145            statistics_summary: String::new(),
146            operator_id_alloc: PhysicalOperatorIdAllocator::new(),
147            fragment_id_alloc: FragmentIdAllocator::new(),
148        }
149    }
150
151    /// Allocate a new physical operator ID from the unified arena.
152    pub fn allocate_operator_id(
153        &mut self,
154    ) -> crate::executor::streaming::plan::types::PhysicalOperatorId {
155        self.operator_id_alloc.allocate()
156    }
157
158    /// Allocate a new fragment ID.
159    pub fn allocate_fragment_id(&mut self) -> crate::executor::streaming::plan::types::FragmentId {
160        self.fragment_id_alloc.allocate()
161    }
162
163    /// Peek at the next operator ID without consuming it.
164    pub fn peek_operator_id(&self) -> crate::executor::streaming::plan::types::PhysicalOperatorId {
165        self.operator_id_alloc.peek()
166    }
167
168    /// Check that this context has been configured correctly.
169    pub fn validate(&self) -> Result<(), QueryError> {
170        Ok(())
171    }
172}
173
174impl Default for PhysicalPlanBuildContext {
175    fn default() -> Self {
176        Self::new()
177    }
178}