lance_csr/datafusion_planner/
mod.rs1pub mod analysis;
18mod builder;
19mod config_helpers;
20mod cypher_udf;
21pub(crate) mod expression;
22mod join_ops;
23mod list_folds;
24mod math_functions;
25mod scalar_functions;
26mod scan_ops;
27mod udf;
28pub mod vector_ops;
29
30#[cfg(test)]
31mod test_fixtures;
32
33pub use analysis::{PlanningContext, QueryAnalysis, RelationshipInstance};
35pub(crate) use scalar_functions::sql_expr;
36
37use crate::config::GraphConfig;
38use crate::error::Result;
39use crate::logical_plan::LogicalOperator;
40use datafusion::logical_expr::LogicalPlan;
41use lance_csr_catalog::GraphSourceCatalog;
42use std::sync::Arc;
43
44pub trait GraphPhysicalPlanner {
46 fn plan(&self, logical_plan: &LogicalOperator) -> Result<LogicalPlan>;
47}
48
49pub struct DataFusionPlanner {
51 pub(crate) config: GraphConfig,
52 pub(crate) catalog: Option<Arc<dyn GraphSourceCatalog>>,
53 pub(crate) adjacency: Option<crate::csr_expand::AdjacencyCatalog>,
54 pub(crate) parameter_types: std::collections::HashMap<String, arrow_schema::DataType>,
55}
56
57impl DataFusionPlanner {
58 pub fn new(config: GraphConfig) -> Self {
59 Self {
60 config,
61 catalog: None,
62 adjacency: None,
63 parameter_types: Default::default(),
64 }
65 }
66
67 pub fn with_catalog(config: GraphConfig, catalog: Arc<dyn GraphSourceCatalog>) -> Self {
68 Self {
69 config,
70 catalog: Some(catalog),
71 adjacency: None,
72 parameter_types: Default::default(),
73 }
74 }
75
76 pub(crate) fn plan_error<E: std::fmt::Display>(
78 &self,
79 context: &str,
80 error: E,
81 ) -> crate::error::GraphError {
82 crate::error::GraphError::PlanError {
83 message: format!("{}: {}", context, error),
84 location: snafu::Location::new(file!(), line!(), column!()),
85 }
86 }
87}
88
89impl GraphPhysicalPlanner for DataFusionPlanner {
90 fn plan(&self, logical_plan: &LogicalOperator) -> Result<LogicalPlan> {
91 let analysis = analysis::analyze(logical_plan)?;
93
94 let mut ctx = PlanningContext::new(&analysis);
96 self.build_operator(&mut ctx, logical_plan)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::ast::{
104 BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection,
105 ValueExpression,
106 };
107 use crate::logical_plan::LogicalOperator;
108 use test_fixtures::{make_catalog, person_config, person_knows_config, person_scan};
109
110 #[test]
111 fn test_filter_preserves_error_context() {
112 let planner = DataFusionPlanner::with_catalog(person_config(), make_catalog());
114
115 let scan = person_scan("p");
116
117 let filter = LogicalOperator::Filter {
119 input: Box::new(scan),
120 predicate: BooleanExpression::Comparison {
121 left: ValueExpression::Property(PropertyRef {
122 variable: "p".to_string(),
123 property: "age".to_string(),
124 }),
125 operator: ComparisonOperator::GreaterThan,
126 right: ValueExpression::Literal(PropertyValue::Integer(30)),
127 },
128 };
129
130 let result = planner.plan(&filter);
131
132 assert!(result.is_ok(), "Valid filter should succeed");
134 }
135
136 #[test]
137 fn test_exists_on_relationship_property_is_qualified() {
138 let planner = DataFusionPlanner::with_catalog(person_knows_config(), make_catalog());
140
141 let scan_a = person_scan("a");
142 let expand = LogicalOperator::Expand {
143 input: Box::new(scan_a),
144 source_variable: "a".to_string(),
145 target_variable: "b".to_string(),
146 target_label: "Person".to_string(),
147 relationship_types: vec!["KNOWS".to_string()],
148 direction: RelationshipDirection::Outgoing,
149 relationship_variable: Some("r".to_string()),
150 properties: Default::default(),
151 target_properties: Default::default(),
152 };
153 let pred = BooleanExpression::Exists(PropertyRef {
154 variable: "r".into(),
155 property: "src_person_id".into(),
156 });
157 let filter = LogicalOperator::Filter {
158 input: Box::new(expand),
159 predicate: pred,
160 };
161 let df_plan = planner.plan(&filter).unwrap();
162 let s = format!("{:?}", df_plan);
163 assert!(s.contains("Filter"), "missing Filter: {}", s);
164 assert!(
165 s.contains("r__src_person_id") || s.contains("IsNotNull"),
166 "missing qualified rel column or IsNotNull in filter: {}",
167 s
168 );
169 }
170
171 #[test]
172 fn test_in_list_on_relationship_property_is_qualified() {
173 let planner = DataFusionPlanner::with_catalog(person_knows_config(), make_catalog());
175
176 let scan_a = person_scan("a");
177 let expand = LogicalOperator::Expand {
178 input: Box::new(scan_a),
179 source_variable: "a".to_string(),
180 target_variable: "b".to_string(),
181 target_label: "Person".to_string(),
182 relationship_types: vec!["KNOWS".to_string()],
183 direction: RelationshipDirection::Outgoing,
184 relationship_variable: Some("r".to_string()),
185 properties: Default::default(),
186 target_properties: Default::default(),
187 };
188 let filter = LogicalOperator::Filter {
189 input: Box::new(expand),
190 predicate: BooleanExpression::In {
191 expression: ValueExpression::Property(PropertyRef {
192 variable: "r".into(),
193 property: "src_person_id".into(),
194 }),
195 list: vec![
196 ValueExpression::Literal(PropertyValue::Integer(1)),
197 ValueExpression::Literal(PropertyValue::Integer(2)),
198 ],
199 },
200 };
201 let df_plan = planner.plan(&filter).unwrap();
202 let s = format!("{:?}", df_plan);
203 assert!(s.contains("Filter"), "missing Filter: {}", s);
204 assert!(
205 s.contains("r__src_person_id"),
206 "missing qualified rel column in IN list filter: {}",
207 s
208 );
209 }
210
211 #[test]
212 fn test_exists_and_in_on_node_props_materialized() {
213 let planner = DataFusionPlanner::with_catalog(person_config(), make_catalog());
215
216 let scan_a = person_scan("a");
217 let pred = BooleanExpression::And(
218 Box::new(BooleanExpression::Exists(PropertyRef {
219 variable: "a".into(),
220 property: "name".into(),
221 })),
222 Box::new(BooleanExpression::In {
223 expression: ValueExpression::Property(PropertyRef {
224 variable: "a".into(),
225 property: "age".into(),
226 }),
227 list: vec![
228 ValueExpression::Literal(PropertyValue::Integer(20)),
229 ValueExpression::Literal(PropertyValue::Integer(30)),
230 ],
231 }),
232 );
233 let filter = LogicalOperator::Filter {
234 input: Box::new(scan_a),
235 predicate: pred,
236 };
237 let df_plan = planner.plan(&filter).unwrap();
238 let s = format!("{:?}", df_plan);
239 assert!(s.contains("Filter"), "missing Filter: {}", s);
240 assert!(
241 s.contains("a__name") || s.contains("IsNotNull"),
242 "missing EXISTS on a__name: {}",
243 s
244 );
245 assert!(
246 s.contains("a__age") || s.contains("age"),
247 "missing IN on a.age: {}",
248 s
249 );
250 }
251}