Skip to main content

linkrs_query/planning/plan/logical/
conversion.rs

1//! Conversion from `PlanNodeEnum` to `LogicalNodeEnum`.
2//!
3//! This module implements the physical-to-logical compatibility bridge used
4//! only when a planner did not emit a native logical tree. Factorized
5//! planners attach the logical tree directly, so this reverse pass runs
6//! solely for statements that stay physical-only by design.
7//! - Physical operators (IndexScan, InnerJoin, LeftJoin)
8//!   are mapped to their logical equivalents.
9//! - All other operators are recursively converted.
10
11use crate::planning::plan::core::nodes::base::plan_node_enum::PlanNodeEnum;
12use crate::planning::plan::logical::logical_node_enum::LogicalNodeEnum;
13
14/// Errors that can occur during physical-to-logical conversion.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ConversionError {
17    /// The plan contains a node type not yet supported in conversion.
18    NotYetImplemented(String),
19}
20
21impl std::fmt::Display for ConversionError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::NotYetImplemented(name) => {
25                write!(f, "conversion not yet implemented for: {}", name)
26            }
27        }
28    }
29}
30
31impl std::error::Error for ConversionError {}
32
33/// Recursively convert a `PlanNodeEnum` tree to a `LogicalNodeEnum` tree.
34///
35/// # Physical-to-Logical Mapping
36/// - `IndexScan` → `ScanVertices`
37/// - `InnerJoin` → `InnerJoin`
38/// - `LeftJoin` → `LeftJoin`
39/// - All other logical operators are preserved with recursive child conversion.
40///
41/// # Limitations
42/// Statements that remain physical-only by design return `NotYetImplemented`
43/// and keep flat execution without factorization.
44/// - `IndexScan` → `ScanVertices`
45/// - `InnerJoin` → `InnerJoin`
46/// - `LeftJoin` → `LeftJoin`
47/// - All other logical operators are preserved with recursive child conversion.
48///
49/// # Limitations
50/// Statements that remain physical-only by design return `NotYetImplemented`
51/// and keep flat execution without factorization.
52pub fn convert_plan(node: &PlanNodeEnum) -> Result<LogicalNodeEnum, ConversionError> {
53    match node {
54        // === Access nodes ===
55        PlanNodeEnum::Start(n) => access::convert_start(n),
56        PlanNodeEnum::ScanVertices(n) => access::convert_scan_vertices(n),
57        PlanNodeEnum::ScanEdges(n) => access::convert_scan_edges(n),
58        PlanNodeEnum::GetVertices(n) => access::convert_get_vertices(n),
59        PlanNodeEnum::GetNeighbors(n) => access::convert_get_neighbors(n),
60        PlanNodeEnum::IndexScan(n) => access::convert_index_scan(n),
61        // === Operation nodes ===
62        PlanNodeEnum::Project(n) => operation::convert_project(n),
63        PlanNodeEnum::Filter(n) => operation::convert_filter(n),
64        PlanNodeEnum::Sort(n) => operation::convert_sort(n),
65        PlanNodeEnum::Limit(n) => operation::convert_limit(n),
66        PlanNodeEnum::TopN(n) => operation::convert_top_n(n),
67        PlanNodeEnum::Sample(n) => operation::convert_sample(n),
68        PlanNodeEnum::Dedup(n) => operation::convert_dedup(n),
69        PlanNodeEnum::Aggregate(n) => operation::convert_aggregate(n),
70        PlanNodeEnum::Window(n) => operation::convert_window(n),
71        // === Join nodes ===
72        PlanNodeEnum::InnerJoin(n) => join::convert_inner_join(n),
73        PlanNodeEnum::LeftJoin(n) => join::convert_left_join(n),
74        PlanNodeEnum::RightJoin(n) => join::convert_right_join(n),
75        PlanNodeEnum::CrossJoin(n) => join::convert_cross_join(n),
76        PlanNodeEnum::FullOuterJoin(n) => join::convert_full_outer_join(n),
77        PlanNodeEnum::SemiJoin(n) => join::convert_semi_join(n),
78        PlanNodeEnum::Flatten(n) => join::convert_flatten(n),
79        // === Traversal nodes ===
80        PlanNodeEnum::Expand(n) => traversal::convert_expand(n),
81        PlanNodeEnum::ExpandAll(n) => traversal::convert_expand_all(n),
82        PlanNodeEnum::Traverse(n) => traversal::convert_traverse(n),
83        PlanNodeEnum::AppendVertices(n) => traversal::convert_append_vertices(n),
84        PlanNodeEnum::BiExpand(n) => traversal::convert_bi_expand(n),
85        PlanNodeEnum::BiTraverse(n) => traversal::convert_bi_traverse(n),
86        // === Control flow nodes ===
87        PlanNodeEnum::Select(n) => control_flow::convert_select(n),
88        PlanNodeEnum::Loop(n) => control_flow::convert_loop(n),
89        PlanNodeEnum::Argument(n) => control_flow::convert_argument(n),
90        PlanNodeEnum::PassThrough(n) => control_flow::convert_pass_through(n),
91        // === Pipeline nodes ===
92        PlanNodeEnum::Assign(n) => pipeline::convert_assign(n),
93        PlanNodeEnum::DataCollect(n) => pipeline::convert_data_collect(n),
94        PlanNodeEnum::Materialize(n) => pipeline::convert_materialize(n),
95        // === Set operation nodes ===
96        PlanNodeEnum::Union(n) => setop::convert_union(n),
97        PlanNodeEnum::Minus(n) => setop::convert_minus(n),
98        PlanNodeEnum::Intersect(n) => setop::convert_intersect(n),
99        PlanNodeEnum::Unwind(n) => setop::convert_unwind(n),
100        // === Apply nodes ===
101        PlanNodeEnum::Apply(n) => apply::convert_apply(n),
102        PlanNodeEnum::PatternApply(n) => apply::convert_pattern_apply(n),
103        PlanNodeEnum::RollUpApply(n) => apply::convert_roll_up_apply(n),
104        PlanNodeEnum::CorrelatedApply(n) => apply::convert_correlated_apply(n),
105        PlanNodeEnum::WcoIntersect(n) => apply::convert_wco_intersect(n),
106        // === Write nodes ===
107        PlanNodeEnum::GetEdges(n) => write::convert_get_edges(n),
108        PlanNodeEnum::InsertVertices(n) => write::convert_insert_vertices(n),
109        PlanNodeEnum::InsertEdges(n) => write::convert_insert_edges(n),
110        PlanNodeEnum::Update(n) => write::convert_update(n),
111        PlanNodeEnum::DeleteVertices(n) => write::convert_delete_vertices(n),
112        PlanNodeEnum::DeleteEdges(n) => write::convert_delete_edges(n),
113        PlanNodeEnum::DeleteIndex(n) => write::convert_delete_index(n),
114        PlanNodeEnum::PipeDeleteVertices(n) => write::convert_pipe_delete_vertices(n),
115        PlanNodeEnum::PipeDeleteEdges(n) => write::convert_pipe_delete_edges(n),
116        PlanNodeEnum::CopyFrom(n) => write::convert_copy_from(n),
117        PlanNodeEnum::CopyTo(n) => write::convert_copy_to(n),
118        // === Fallback for unsupported nodes ===
119        // === Fallback for unsupported nodes ===
120        // Intentionally not mapped: Algorithm nodes (MultiShortestPath/BFSShortest/AllPaths/ShortestPath)
121        // and Fulltext/Vector search remain `NotYetImplemented`.
122        // They are non-factorized operators not on the factorization / partitioning chain,
123        // so reverse mapping is optional; `ensure_logical_plan` will record the failure in
124        // `cbo_notes` and fall back to flat execution without factorization.
125        _ => Err(ConversionError::NotYetImplemented(
126            node.type_name().to_string(),
127        )),
128    }
129}
130
131mod access;
132mod apply;
133mod control_flow;
134mod join;
135mod operation;
136mod pipeline;
137mod setop;
138mod traversal;
139mod write;
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_convert_start() {
147        let start = crate::planning::plan::core::nodes::control_flow::start_node::StartNode::new();
148        let plan_enum = PlanNodeEnum::Start(start);
149        let result = convert_plan(&plan_enum);
150        assert!(result.is_ok());
151        let logical = result.unwrap();
152        assert_eq!(logical.type_name(), "Start");
153    }
154
155    #[test]
156    fn test_convert_filter_with_start() {
157        let start = crate::planning::plan::core::nodes::control_flow::start_node::StartNode::new();
158        let start_enum = PlanNodeEnum::Start(start);
159
160        let ctx = std::sync::Arc::new(
161            linkrs_core::types::expr::expression_context::ExpressionAnalysisContext::new(),
162        );
163        let expr = linkrs_core::Expression::Variable("test".to_string());
164        let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
165        let id = ctx.register_expression(expr_meta);
166        let condition = linkrs_core::types::expr::contextual::ContextualExpression::new(id, ctx);
167
168        let filter = crate::planning::plan::core::nodes::operation::filter_node::FilterNode::new(
169            start_enum, condition,
170        )
171        .expect("Filter node creation failed");
172
173        let filter_enum = PlanNodeEnum::Filter(filter);
174        let result = convert_plan(&filter_enum);
175        assert!(result.is_ok());
176        let logical = result.unwrap();
177        assert_eq!(logical.type_name(), "Filter");
178    }
179
180    #[test]
181    fn test_convert_unsupported() {
182        // Argument is now supported; use a management node that remains unsupported
183        // as the negative case (e.g., Tag management not yet bridged).
184        let arg =
185            crate::planning::plan::core::nodes::control_flow::control_flow_node::ArgumentNode::new(
186                -1, "x",
187            );
188        let plan_enum = PlanNodeEnum::Argument(arg);
189        let result = convert_plan(&plan_enum);
190        assert!(result.is_ok());
191        assert_eq!(result.unwrap().type_name(), "Argument");
192    }
193}