1use crate::ast::{
11 BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection,
12 ValueExpression,
13};
14use crate::config::GraphConfig;
15use crate::error::{GraphError, Result};
16use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem};
17use std::collections::HashMap;
18
19pub struct LogicalPlanToSqlConverter<'a> {
21 config: &'a Option<GraphConfig>,
22 variable_counter: u32,
23 table_aliases: HashMap<String, String>,
24}
25
26impl<'a> LogicalPlanToSqlConverter<'a> {
27 pub fn new(config: &'a Option<GraphConfig>) -> Self {
28 Self {
29 config,
30 variable_counter: 0,
31 table_aliases: HashMap::new(),
32 }
33 }
34
35 pub fn convert(&mut self, plan: &LogicalOperator) -> Result<String> {
37 match plan {
38 LogicalOperator::Project { input, projections } => {
39 self.convert_project(input, projections)
40 }
41
42 LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate),
43
44 LogicalOperator::ScanByLabel {
45 variable,
46 label,
47 properties,
48 } => self.convert_scan(variable, label, properties),
49
50 LogicalOperator::Expand {
51 input,
52 source_variable,
53 target_variable,
54 relationship_types,
55 direction,
56 relationship_variable,
57 properties,
58 ..
59 } => self.convert_expand(
60 input,
61 source_variable,
62 target_variable,
63 relationship_types,
64 direction,
65 relationship_variable,
66 properties,
67 ),
68
69 LogicalOperator::Distinct { input } => self.convert_distinct(input),
70
71 LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64),
72
73 LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64),
74
75 LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items),
76
77 LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError {
78 message: "Variable length paths not supported in SQL conversion".to_string(),
79 location: snafu::Location::new(file!(), line!(), column!()),
80 }),
81
82 LogicalOperator::Join { .. } => Err(GraphError::PlanError {
83 message: "Complex joins not supported in SQL conversion".to_string(),
84 location: snafu::Location::new(file!(), line!(), column!()),
85 }),
86 }
87 }
88
89 fn convert_project(
90 &mut self,
91 input: &LogicalOperator,
92 projections: &[ProjectionItem],
93 ) -> Result<String> {
94 let input_sql = self.convert(input)?;
95
96 if projections.is_empty() {
97 return Ok(format!("SELECT * FROM ({})", input_sql));
98 }
99
100 let proj_list = projections
101 .iter()
102 .map(|p| self.projection_to_sql(p))
103 .collect::<Result<Vec<_>>>()?
104 .join(", ");
105
106 Ok(format!("SELECT {} FROM ({})", proj_list, input_sql))
107 }
108
109 fn convert_filter(
110 &mut self,
111 input: &LogicalOperator,
112 predicate: &BooleanExpression,
113 ) -> Result<String> {
114 let input_sql = self.convert(input)?;
115 let where_clause = self.boolean_expr_to_sql(predicate)?;
116 Ok(format!(
117 "SELECT * FROM ({}) WHERE {}",
118 input_sql, where_clause
119 ))
120 }
121
122 fn convert_scan(
123 &mut self,
124 variable: &str,
125 label: &str,
126 properties: &HashMap<String, PropertyValue>,
127 ) -> Result<String> {
128 self.table_aliases
130 .insert(variable.to_string(), variable.to_string());
131
132 let mut sql = format!("SELECT * FROM {} AS {}", label, variable);
133
134 if !properties.is_empty() {
135 let filters = properties
136 .iter()
137 .map(|(k, v)| {
138 Ok(format!(
139 "{}.{} = {}",
140 variable,
141 k,
142 self.property_value_to_sql(v)?
143 ))
144 })
145 .collect::<Result<Vec<_>>>()?
146 .join(" AND ");
147 sql = format!("{} WHERE {}", sql, filters);
148 }
149
150 Ok(sql)
151 }
152
153 #[allow(clippy::too_many_arguments)]
154 fn convert_expand(
155 &mut self,
156 _input: &LogicalOperator,
157 source_variable: &str,
158 target_variable: &str,
159 relationship_types: &[String],
160 _direction: &RelationshipDirection,
161 relationship_variable: &Option<String>,
162 _properties: &HashMap<String, PropertyValue>,
163 ) -> Result<String> {
164 let _input_sql = self.convert(_input)?;
165 let rel_type = relationship_types
166 .first()
167 .ok_or_else(|| GraphError::PlanError {
168 message: "No relationship type specified".to_string(),
169 location: snafu::Location::new(file!(), line!(), column!()),
170 })?;
171
172 let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError {
173 message: "Config required for relationship queries".to_string(),
174 location: snafu::Location::new(file!(), line!(), column!()),
175 })?;
176
177 let _rel_mapping =
178 config
179 .get_relationship_mapping(rel_type)
180 .ok_or_else(|| GraphError::PlanError {
181 message: format!("No relationship mapping for {}", rel_type),
182 location: snafu::Location::new(file!(), line!(), column!()),
183 })?;
184
185 let src_alias = format!("src_{}", self.variable_counter);
187 let rel_alias = format!("rel_{}", self.variable_counter);
188 let tgt_alias = format!("tgt_{}", self.variable_counter);
189 self.variable_counter += 1;
190
191 self.table_aliases
193 .insert(source_variable.to_string(), src_alias.clone());
194 self.table_aliases
195 .insert(target_variable.to_string(), tgt_alias.clone());
196 if let Some(rel_var) = relationship_variable {
197 self.table_aliases
198 .insert(rel_var.clone(), rel_alias.clone());
199 }
200
201 Err(GraphError::PlanError {
205 message: "Relationship traversal not supported in SQL conversion - would require proper ID field mapping".to_string(),
206 location: snafu::Location::new(file!(), line!(), column!()),
207 })
208 }
209
210 fn convert_distinct(&mut self, input: &LogicalOperator) -> Result<String> {
211 let input_sql = self.convert(input)?;
212 Ok(format!("SELECT DISTINCT * FROM ({})", input_sql))
213 }
214
215 fn convert_limit(&mut self, input: &LogicalOperator, count: i64) -> Result<String> {
216 let input_sql = self.convert(input)?;
217 Ok(format!("SELECT * FROM ({}) LIMIT {}", input_sql, count))
218 }
219
220 fn convert_offset(&mut self, input: &LogicalOperator, offset: i64) -> Result<String> {
221 let input_sql = self.convert(input)?;
222 Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset))
223 }
224
225 fn convert_sort(
226 &mut self,
227 input: &LogicalOperator,
228 _sort_items: &[SortItem],
229 ) -> Result<String> {
230 self.convert(input)
233 }
234
235 fn projection_to_sql(&self, projection: &ProjectionItem) -> Result<String> {
236 let expr_sql = self.value_expr_to_sql(&projection.expression)?;
237
238 if let Some(alias) = &projection.alias {
239 Ok(format!("{} AS {}", expr_sql, alias))
240 } else {
241 Ok(expr_sql)
242 }
243 }
244
245 fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result<String> {
246 match expr {
247 BooleanExpression::Comparison {
248 left,
249 operator,
250 right,
251 } => {
252 let left_sql = self.value_expr_to_sql(left)?;
253 let right_sql = self.value_expr_to_sql(right)?;
254 let op_sql = match operator {
255 ComparisonOperator::Equal => "=",
256 ComparisonOperator::NotEqual => "!=",
257 ComparisonOperator::LessThan => "<",
258 ComparisonOperator::LessThanOrEqual => "<=",
259 ComparisonOperator::GreaterThan => ">",
260 ComparisonOperator::GreaterThanOrEqual => ">=",
261 };
262 Ok(format!("{} {} {}", left_sql, op_sql, right_sql))
263 }
264
265 BooleanExpression::In { expression, list } => {
266 let expr_sql = self.value_expr_to_sql(expression)?;
267 let list_sql = list
268 .iter()
269 .map(|v| self.value_expr_to_sql(v))
270 .collect::<Result<Vec<_>>>()?
271 .join(", ");
272 Ok(format!("{} IN ({})", expr_sql, list_sql))
273 }
274
275 BooleanExpression::And(left, right) => {
276 let left_sql = self.boolean_expr_to_sql(left)?;
277 let right_sql = self.boolean_expr_to_sql(right)?;
278 Ok(format!("({}) AND ({})", left_sql, right_sql))
279 }
280
281 BooleanExpression::Or(left, right) => {
282 let left_sql = self.boolean_expr_to_sql(left)?;
283 let right_sql = self.boolean_expr_to_sql(right)?;
284 Ok(format!("({}) OR ({})", left_sql, right_sql))
285 }
286
287 BooleanExpression::Not(inner) => {
288 let inner_sql = self.boolean_expr_to_sql(inner)?;
289 Ok(format!("NOT ({})", inner_sql))
290 }
291
292 BooleanExpression::Exists(prop) => {
293 let prop_sql = self.property_ref_to_sql(prop)?;
294 Ok(format!("{} IS NOT NULL", prop_sql))
295 }
296
297 _ => Err(GraphError::PlanError {
298 message: "Unsupported boolean expression in SQL conversion".to_string(),
299 location: snafu::Location::new(file!(), line!(), column!()),
300 }),
301 }
302 }
303
304 fn value_expr_to_sql(&self, expr: &ValueExpression) -> Result<String> {
305 match expr {
306 ValueExpression::Property(prop) => self.property_ref_to_sql(prop),
307 ValueExpression::Variable(var) => Ok(var.clone()),
308 ValueExpression::Literal(value) => self.property_value_to_sql(value),
309 _ => Err(GraphError::PlanError {
310 message: "Unsupported value expression in SQL conversion".to_string(),
311 location: snafu::Location::new(file!(), line!(), column!()),
312 }),
313 }
314 }
315
316 fn property_ref_to_sql(&self, prop: &PropertyRef) -> Result<String> {
317 if let Some(table_alias) = self.table_aliases.get(&prop.variable) {
318 Ok(format!("{}.{}", table_alias, prop.property))
319 } else {
320 Ok(prop.property.clone())
322 }
323 }
324
325 fn property_value_to_sql(&self, value: &PropertyValue) -> Result<String> {
326 match value {
327 PropertyValue::String(s) => Ok(format!("'{}'", s.replace('\'', "''"))), PropertyValue::Integer(i) => Ok(i.to_string()),
329 PropertyValue::Float(f) => Ok(f.to_string()),
330 PropertyValue::Boolean(b) => Ok(b.to_string()),
331 PropertyValue::Null => Ok("NULL".to_string()),
332 PropertyValue::Parameter(p) => Ok(format!("${}", p)), PropertyValue::Property(prop) => self.property_ref_to_sql(prop),
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression};
342 use crate::logical_plan::{LogicalOperator, ProjectionItem};
343 use std::collections::HashMap;
344
345 #[test]
346 fn test_simple_scan_conversion() {
347 let mut converter = LogicalPlanToSqlConverter::new(&None);
348
349 let scan = LogicalOperator::ScanByLabel {
350 variable: "n".to_string(),
351 label: "Person".to_string(),
352 properties: HashMap::new(),
353 };
354
355 let sql = converter.convert(&scan).unwrap();
356 assert_eq!(sql, "SELECT * FROM Person AS n");
357 }
358
359 #[test]
360 fn test_scan_with_properties() {
361 let mut converter = LogicalPlanToSqlConverter::new(&None);
362
363 let mut properties = HashMap::new();
364 properties.insert(
365 "name".to_string(),
366 PropertyValue::String("Alice".to_string()),
367 );
368
369 let scan = LogicalOperator::ScanByLabel {
370 variable: "n".to_string(),
371 label: "Person".to_string(),
372 properties,
373 };
374
375 let sql = converter.convert(&scan).unwrap();
376 assert_eq!(sql, "SELECT * FROM Person AS n WHERE n.name = 'Alice'");
377 }
378
379 #[test]
380 fn test_project_conversion() {
381 let mut converter = LogicalPlanToSqlConverter::new(&None);
382
383 let scan = LogicalOperator::ScanByLabel {
384 variable: "n".to_string(),
385 label: "Person".to_string(),
386 properties: HashMap::new(),
387 };
388
389 let project = LogicalOperator::Project {
390 input: Box::new(scan),
391 projections: vec![ProjectionItem {
392 expression: ValueExpression::Property(PropertyRef {
393 variable: "n".to_string(),
394 property: "name".to_string(),
395 }),
396 alias: None,
397 }],
398 };
399
400 let sql = converter.convert(&project).unwrap();
401 assert_eq!(sql, "SELECT n.name FROM (SELECT * FROM Person AS n)");
402 }
403
404 #[test]
405 fn test_filter_conversion() {
406 let mut converter = LogicalPlanToSqlConverter::new(&None);
407
408 let scan = LogicalOperator::ScanByLabel {
409 variable: "n".to_string(),
410 label: "Person".to_string(),
411 properties: HashMap::new(),
412 };
413
414 let filter = LogicalOperator::Filter {
415 input: Box::new(scan),
416 predicate: BooleanExpression::Comparison {
417 left: ValueExpression::Property(PropertyRef {
418 variable: "n".to_string(),
419 property: "age".to_string(),
420 }),
421 operator: ComparisonOperator::GreaterThan,
422 right: ValueExpression::Literal(PropertyValue::Integer(30)),
423 },
424 };
425
426 let sql = converter.convert(&filter).unwrap();
427 assert_eq!(
428 sql,
429 "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"
430 );
431 }
432}