From 190e046df0d07e5b396507235fa32d4761cc27e0 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:13:52 -0700 Subject: [PATCH 1/7] (experimental): add a converter for logical plan to sql --- rust/lance-graph/src/lib.rs | 1 + rust/lance-graph/src/sql_converter.rs | 425 ++++++++++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 rust/lance-graph/src/sql_converter.rs diff --git a/rust/lance-graph/src/lib.rs b/rust/lance-graph/src/lib.rs index 1001f11c5..c35169412 100644 --- a/rust/lance-graph/src/lib.rs +++ b/rust/lance-graph/src/lib.rs @@ -46,6 +46,7 @@ pub mod query; pub mod query_processor; pub mod semantic; pub mod source_catalog; +pub mod sql_converter; /// Maximum allowed hops for variable-length relationship expansion (e.g., *1..N) pub const MAX_VARIABLE_LENGTH_HOPS: u32 = 20; diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs new file mode 100644 index 000000000..357a0813d --- /dev/null +++ b/rust/lance-graph/src/sql_converter.rs @@ -0,0 +1,425 @@ +//! SQL converter for logical plans +//! +//! This module converts our logical plan representation to optimized SQL +//! that can be executed by DataFusion or LanceDB for better query optimization. +//! +//! # Target Platforms +//! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface +//! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions + +use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; +use crate::config::GraphConfig; +use crate::error::{GraphError, Result}; +use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; +use std::collections::HashMap; + +/// Converts logical plans to SQL for DataFusion or LanceDB execution +pub struct LogicalPlanToSqlConverter<'a> { + config: &'a Option, + variable_counter: u32, + table_aliases: HashMap, +} + +impl<'a> LogicalPlanToSqlConverter<'a> { + pub fn new(config: &'a Option) -> Self { + Self { + config, + variable_counter: 0, + table_aliases: HashMap::new(), + } + } + + /// Convert a logical plan to SQL compatible with DataFusion and LanceDB + pub fn convert(&mut self, plan: &LogicalOperator) -> Result { + match plan { + LogicalOperator::Project { input, projections } => { + self.convert_project(input, projections) + } + + LogicalOperator::Filter { input, predicate } => { + self.convert_filter(input, predicate) + } + + LogicalOperator::ScanByLabel { variable, label, properties } => { + self.convert_scan(variable, label, properties) + } + + LogicalOperator::Expand { + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties, + } => { + self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties + ) + } + + LogicalOperator::Distinct { input } => { + self.convert_distinct(input) + } + + LogicalOperator::Limit { input, count } => { + self.convert_limit(input, *count as i64) + } + + LogicalOperator::Offset { input, offset } => { + self.convert_offset(input, *offset as i64) + } + + LogicalOperator::Sort { input, sort_items } => { + self.convert_sort(input, sort_items) + } + + // Complex operations that should fall back to legacy execution + LogicalOperator::VariableLengthExpand { .. } => { + Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } + + LogicalOperator::Join { .. } => { + Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } + } + } + + fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { + let input_sql = self.convert(input)?; + + if projections.is_empty() { + return Ok(format!("SELECT * FROM ({})", input_sql)); + } + + let proj_list = projections.iter() + .map(|p| self.projection_to_sql(p)) + .collect::>>()? + .join(", "); + + Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) + } + + fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { + let input_sql = self.convert(input)?; + let where_clause = self.boolean_expr_to_sql(predicate)?; + Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) + } + + fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { + // Store table alias for this variable - use the variable name as the alias + self.table_aliases.insert(variable.to_string(), variable.to_string()); + + let mut sql = format!("SELECT * FROM {} AS {}", label, variable); + + if !properties.is_empty() { + let filters = properties.iter() + .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) + .collect::>>()? + .join(" AND "); + sql = format!("{} WHERE {}", sql, filters); + } + + Ok(sql) + } + + fn convert_expand( + &mut self, + input: &LogicalOperator, + source_variable: &str, + target_variable: &str, + relationship_types: &[String], + direction: &RelationshipDirection, + relationship_variable: &Option, + properties: &HashMap, + ) -> Result { + let input_sql = self.convert(input)?; + let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; + + let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { + message: "Config required for relationship queries".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; + + let rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; + + // Generate unique aliases + let src_alias = format!("src_{}", self.variable_counter); + let rel_alias = format!("rel_{}", self.variable_counter); + let tgt_alias = format!("tgt_{}", self.variable_counter); + self.variable_counter += 1; + + // Store aliases for variables + self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); + if let Some(rel_var) = relationship_variable { + self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); + } + + let join_sql = match direction { + RelationshipDirection::Outgoing => format!( + "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", + input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.source_id_field, + target_variable, tgt_alias, rel_alias, rel_mapping.target_id_field, tgt_alias + ), + RelationshipDirection::Incoming => format!( + "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", + input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.target_id_field, + target_variable, tgt_alias, rel_alias, rel_mapping.source_id_field, tgt_alias + ), + RelationshipDirection::Undirected => { + // For undirected, we need a UNION of both directions + let outgoing = format!( + "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", + input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.source_id_field, + target_variable, tgt_alias, rel_alias, rel_mapping.target_id_field, tgt_alias + ); + let incoming = format!( + "({}) AS {}_2 JOIN {} AS {}_2 ON {}_2.id = {}_2.{} JOIN {} AS {}_2 ON {}_2.{} = {}_2.id", + input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.target_id_field, + target_variable, tgt_alias, rel_alias, rel_mapping.source_id_field, tgt_alias + ); + format!("({}) UNION ALL ({})", outgoing, incoming) + } + }; + + let mut result_sql = format!("SELECT * FROM {}", join_sql); + + // Add relationship property filters if any + if !properties.is_empty() { + let rel_filters = properties.iter() + .map(|(k, v)| Ok(format!("{}.{} = {}", rel_alias, k, self.property_value_to_sql(v)?))) + .collect::>>()? + .join(" AND "); + result_sql = format!("{} WHERE {}", result_sql, rel_filters); + } + + Ok(result_sql) + } + + fn convert_distinct(&mut self, input: &LogicalOperator) -> Result { + let input_sql = self.convert(input)?; + Ok(format!("SELECT DISTINCT * FROM ({})", input_sql)) + } + + fn convert_limit(&mut self, input: &LogicalOperator, count: i64) -> Result { + let input_sql = self.convert(input)?; + Ok(format!("SELECT * FROM ({}) LIMIT {}", input_sql, count)) + } + + fn convert_offset(&mut self, input: &LogicalOperator, offset: i64) -> Result { + let input_sql = self.convert(input)?; + Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) + } + + fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { + // For now, just pass through the input (ORDER BY is complex to implement) + // TODO: Implement proper ORDER BY conversion + self.convert(input) + } + + fn projection_to_sql(&self, projection: &ProjectionItem) -> Result { + let expr_sql = self.value_expr_to_sql(&projection.expression)?; + + if let Some(alias) = &projection.alias { + Ok(format!("{} AS {}", expr_sql, alias)) + } else { + Ok(expr_sql) + } + } + + fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { + match expr { + BooleanExpression::Comparison { left, operator, right } => { + let left_sql = self.value_expr_to_sql(left)?; + let right_sql = self.value_expr_to_sql(right)?; + let op_sql = match operator { + ComparisonOperator::Equal => "=", + ComparisonOperator::NotEqual => "!=", + ComparisonOperator::LessThan => "<", + ComparisonOperator::LessThanOrEqual => "<=", + ComparisonOperator::GreaterThan => ">", + ComparisonOperator::GreaterThanOrEqual => ">=", + }; + Ok(format!("{} {} {}", left_sql, op_sql, right_sql)) + } + + BooleanExpression::In { expression, list } => { + let expr_sql = self.value_expr_to_sql(expression)?; + let list_sql = list.iter() + .map(|v| self.value_expr_to_sql(v)) + .collect::>>()? + .join(", "); + Ok(format!("{} IN ({})", expr_sql, list_sql)) + } + + BooleanExpression::And(left, right) => { + let left_sql = self.boolean_expr_to_sql(left)?; + let right_sql = self.boolean_expr_to_sql(right)?; + Ok(format!("({}) AND ({})", left_sql, right_sql)) + } + + BooleanExpression::Or(left, right) => { + let left_sql = self.boolean_expr_to_sql(left)?; + let right_sql = self.boolean_expr_to_sql(right)?; + Ok(format!("({}) OR ({})", left_sql, right_sql)) + } + + BooleanExpression::Not(inner) => { + let inner_sql = self.boolean_expr_to_sql(inner)?; + Ok(format!("NOT ({})", inner_sql)) + } + + BooleanExpression::Exists(prop) => { + let prop_sql = self.property_ref_to_sql(prop)?; + Ok(format!("{} IS NOT NULL", prop_sql)) + } + + _ => Err(GraphError::PlanError { + message: "Unsupported boolean expression in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } + } + + fn value_expr_to_sql(&self, expr: &ValueExpression) -> Result { + match expr { + ValueExpression::Property(prop) => self.property_ref_to_sql(prop), + ValueExpression::Variable(var) => Ok(var.clone()), + ValueExpression::Literal(value) => self.property_value_to_sql(value), + _ => Err(GraphError::PlanError { + message: "Unsupported value expression in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } + } + + fn property_ref_to_sql(&self, prop: &PropertyRef) -> Result { + if let Some(table_alias) = self.table_aliases.get(&prop.variable) { + Ok(format!("{}.{}", table_alias, prop.property)) + } else { + // Fallback to unqualified column name + Ok(prop.property.clone()) + } + } + + fn property_value_to_sql(&self, value: &PropertyValue) -> Result { + match value { + PropertyValue::String(s) => Ok(format!("'{}'", s.replace('\'', "''"))), // Escape single quotes + PropertyValue::Integer(i) => Ok(i.to_string()), + PropertyValue::Float(f) => Ok(f.to_string()), + PropertyValue::Boolean(b) => Ok(b.to_string()), + PropertyValue::Null => Ok("NULL".to_string()), + PropertyValue::Parameter(p) => Ok(format!("${}", p)), // Parameter placeholder + PropertyValue::Property(prop) => self.property_ref_to_sql(prop), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; + use crate::logical_plan::{LogicalOperator, ProjectionItem}; + use std::collections::HashMap; + + #[test] + fn test_simple_scan_conversion() { + let mut converter = LogicalPlanToSqlConverter::new(&None); + + let scan = LogicalOperator::ScanByLabel { + variable: "n".to_string(), + label: "Person".to_string(), + properties: HashMap::new(), + }; + + let sql = converter.convert(&scan).unwrap(); + assert_eq!(sql, "SELECT * FROM Person AS n"); + } + + #[test] + fn test_scan_with_properties() { + let mut converter = LogicalPlanToSqlConverter::new(&None); + + let mut properties = HashMap::new(); + properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); + + let scan = LogicalOperator::ScanByLabel { + variable: "n".to_string(), + label: "Person".to_string(), + properties, + }; + + let sql = converter.convert(&scan).unwrap(); + assert_eq!(sql, "SELECT * FROM Person AS n WHERE n.name = 'Alice'"); + } + + #[test] + fn test_project_conversion() { + let mut converter = LogicalPlanToSqlConverter::new(&None); + + let scan = LogicalOperator::ScanByLabel { + variable: "n".to_string(), + label: "Person".to_string(), + properties: HashMap::new(), + }; + + let project = LogicalOperator::Project { + input: Box::new(scan), + projections: vec![ProjectionItem { + expression: ValueExpression::Property(PropertyRef { + variable: "n".to_string(), + property: "name".to_string(), + }), + alias: None, + }], + }; + + let sql = converter.convert(&project).unwrap(); + assert_eq!(sql, "SELECT n.name FROM (SELECT * FROM Person AS n)"); + } + + #[test] + fn test_filter_conversion() { + let mut converter = LogicalPlanToSqlConverter::new(&None); + + let scan = LogicalOperator::ScanByLabel { + variable: "n".to_string(), + label: "Person".to_string(), + properties: HashMap::new(), + }; + + let filter = LogicalOperator::Filter { + input: Box::new(scan), + predicate: BooleanExpression::Comparison { + left: ValueExpression::Property(PropertyRef { + variable: "n".to_string(), + property: "age".to_string(), + }), + operator: ComparisonOperator::GreaterThan, + right: ValueExpression::Literal(PropertyValue::Integer(30)), + }, + }; + + let sql = converter.convert(&filter).unwrap(); + assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); + } +} From 35382d7b33e3f3260d8f2b994f14f739803b6c23 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:19:28 -0700 Subject: [PATCH 2/7] format code --- rust/lance-graph/src/sql_converter.rs | 230 +++++++++++++++++--------- 1 file changed, 153 insertions(+), 77 deletions(-) diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index 357a0813d..fafcf2486 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -7,7 +7,10 @@ //! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface //! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions -use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; +use crate::ast::{ + BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection, + ValueExpression, +}; use crate::config::GraphConfig; use crate::error::{GraphError, Result}; use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; @@ -36,13 +39,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.convert_project(input, projections) } - LogicalOperator::Filter { input, predicate } => { - self.convert_filter(input, predicate) - } + LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate), - LogicalOperator::ScanByLabel { variable, label, properties } => { - self.convert_scan(variable, label, properties) - } + LogicalOperator::ScanByLabel { + variable, + label, + properties, + } => self.convert_scan(variable, label, properties), LogicalOperator::Expand { input, @@ -52,59 +55,50 @@ impl<'a> LogicalPlanToSqlConverter<'a> { direction, relationship_variable, properties, - } => { - self.convert_expand( - input, - source_variable, - target_variable, - relationship_types, - direction, - relationship_variable, - properties - ) - } + } => self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties, + ), - LogicalOperator::Distinct { input } => { - self.convert_distinct(input) - } + LogicalOperator::Distinct { input } => self.convert_distinct(input), - LogicalOperator::Limit { input, count } => { - self.convert_limit(input, *count as i64) - } + LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64), - LogicalOperator::Offset { input, offset } => { - self.convert_offset(input, *offset as i64) - } + LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64), - LogicalOperator::Sort { input, sort_items } => { - self.convert_sort(input, sort_items) - } + LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), // Complex operations that should fall back to legacy execution - LogicalOperator::VariableLengthExpand { .. } => { - Err(GraphError::PlanError { - message: "Variable length paths not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), - LogicalOperator::Join { .. } => { - Err(GraphError::PlanError { - message: "Complex joins not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::Join { .. } => Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), } } - fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { + fn convert_project( + &mut self, + input: &LogicalOperator, + projections: &[ProjectionItem], + ) -> Result { let input_sql = self.convert(input)?; if projections.is_empty() { return Ok(format!("SELECT * FROM ({})", input_sql)); } - let proj_list = projections.iter() + let proj_list = projections + .iter() .map(|p| self.projection_to_sql(p)) .collect::>>()? .join(", "); @@ -112,21 +106,42 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) } - fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { + fn convert_filter( + &mut self, + input: &LogicalOperator, + predicate: &BooleanExpression, + ) -> Result { let input_sql = self.convert(input)?; let where_clause = self.boolean_expr_to_sql(predicate)?; - Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) + Ok(format!( + "SELECT * FROM ({}) WHERE {}", + input_sql, where_clause + )) } - fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { + fn convert_scan( + &mut self, + variable: &str, + label: &str, + properties: &HashMap, + ) -> Result { // Store table alias for this variable - use the variable name as the alias - self.table_aliases.insert(variable.to_string(), variable.to_string()); + self.table_aliases + .insert(variable.to_string(), variable.to_string()); let mut sql = format!("SELECT * FROM {} AS {}", label, variable); if !properties.is_empty() { - let filters = properties.iter() - .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) + let filters = properties + .iter() + .map(|(k, v)| { + Ok(format!( + "{}.{} = {}", + variable, + k, + self.property_value_to_sql(v)? + )) + }) .collect::>>()? .join(" AND "); sql = format!("{} WHERE {}", sql, filters); @@ -146,20 +161,25 @@ impl<'a> LogicalPlanToSqlConverter<'a> { properties: &HashMap, ) -> Result { let input_sql = self.convert(input)?; - let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { - message: "No relationship type specified".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_type = relationship_types + .first() + .ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { message: "Config required for relationship queries".to_string(), location: snafu::Location::new(file!(), line!(), column!()), })?; - let rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { - message: format!("No relationship mapping for {}", rel_type), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_mapping = + config + .get_relationship_mapping(rel_type) + .ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; // Generate unique aliases let src_alias = format!("src_{}", self.variable_counter); @@ -168,29 +188,62 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.variable_counter += 1; // Store aliases for variables - self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); - self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); + self.table_aliases + .insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases + .insert(target_variable.to_string(), tgt_alias.clone()); if let Some(rel_var) = relationship_variable { - self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); + self.table_aliases + .insert(rel_var.clone(), rel_alias.clone()); } let join_sql = match direction { RelationshipDirection::Outgoing => format!( "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.source_id_field, - target_variable, tgt_alias, rel_alias, rel_mapping.target_id_field, tgt_alias + input_sql, + src_alias, + rel_type, + rel_alias, + src_alias, + rel_alias, + rel_mapping.source_id_field, + target_variable, + tgt_alias, + rel_alias, + rel_mapping.target_id_field, + tgt_alias ), RelationshipDirection::Incoming => format!( "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.target_id_field, - target_variable, tgt_alias, rel_alias, rel_mapping.source_id_field, tgt_alias + input_sql, + src_alias, + rel_type, + rel_alias, + src_alias, + rel_alias, + rel_mapping.target_id_field, + target_variable, + tgt_alias, + rel_alias, + rel_mapping.source_id_field, + tgt_alias ), RelationshipDirection::Undirected => { // For undirected, we need a UNION of both directions let outgoing = format!( "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.source_id_field, - target_variable, tgt_alias, rel_alias, rel_mapping.target_id_field, tgt_alias + input_sql, + src_alias, + rel_type, + rel_alias, + src_alias, + rel_alias, + rel_mapping.source_id_field, + target_variable, + tgt_alias, + rel_alias, + rel_mapping.target_id_field, + tgt_alias ); let incoming = format!( "({}) AS {}_2 JOIN {} AS {}_2 ON {}_2.id = {}_2.{} JOIN {} AS {}_2 ON {}_2.{} = {}_2.id", @@ -205,8 +258,16 @@ impl<'a> LogicalPlanToSqlConverter<'a> { // Add relationship property filters if any if !properties.is_empty() { - let rel_filters = properties.iter() - .map(|(k, v)| Ok(format!("{}.{} = {}", rel_alias, k, self.property_value_to_sql(v)?))) + let rel_filters = properties + .iter() + .map(|(k, v)| { + Ok(format!( + "{}.{} = {}", + rel_alias, + k, + self.property_value_to_sql(v)? + )) + }) .collect::>>()? .join(" AND "); result_sql = format!("{} WHERE {}", result_sql, rel_filters); @@ -230,7 +291,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) } - fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { + fn convert_sort( + &mut self, + input: &LogicalOperator, + _sort_items: &[SortItem], + ) -> Result { // For now, just pass through the input (ORDER BY is complex to implement) // TODO: Implement proper ORDER BY conversion self.convert(input) @@ -248,7 +313,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { match expr { - BooleanExpression::Comparison { left, operator, right } => { + BooleanExpression::Comparison { + left, + operator, + right, + } => { let left_sql = self.value_expr_to_sql(left)?; let right_sql = self.value_expr_to_sql(right)?; let op_sql = match operator { @@ -264,7 +333,8 @@ impl<'a> LogicalPlanToSqlConverter<'a> { BooleanExpression::In { expression, list } => { let expr_sql = self.value_expr_to_sql(expression)?; - let list_sql = list.iter() + let list_sql = list + .iter() .map(|v| self.value_expr_to_sql(v)) .collect::>>()? .join(", "); @@ -296,7 +366,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported boolean expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -308,7 +378,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported value expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -337,7 +407,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; + use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression}; use crate::logical_plan::{LogicalOperator, ProjectionItem}; use std::collections::HashMap; @@ -360,7 +430,10 @@ mod tests { let mut converter = LogicalPlanToSqlConverter::new(&None); let mut properties = HashMap::new(); - properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); + properties.insert( + "name".to_string(), + PropertyValue::String("Alice".to_string()), + ); let scan = LogicalOperator::ScanByLabel { variable: "n".to_string(), @@ -420,6 +493,9 @@ mod tests { }; let sql = converter.convert(&filter).unwrap(); - assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); + assert_eq!( + sql, + "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30" + ); } } From 9c97852eeb2af3a360983dfb1b7eb18f20539c03 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:31:32 -0700 Subject: [PATCH 3/7] feat(logical): add relationship variables in logical plan --- rust/lance-graph/src/datafusion_planner.rs | 1 + rust/lance-graph/src/logical_plan.rs | 5 + rust/lance-graph/src/sql_converter.rs | 260 ++++++--------------- 3 files changed, 82 insertions(+), 184 deletions(-) diff --git a/rust/lance-graph/src/datafusion_planner.rs b/rust/lance-graph/src/datafusion_planner.rs index c7668ab46..c97f04c5e 100644 --- a/rust/lance-graph/src/datafusion_planner.rs +++ b/rust/lance-graph/src/datafusion_planner.rs @@ -468,6 +468,7 @@ mod tests { target_variable: "b".to_string(), relationship_types: vec!["KNOWS".to_string()], direction: crate::ast::RelationshipDirection::Outgoing, + relationship_variable: None, properties: Default::default(), }; let project = LogicalOperator::Project { diff --git a/rust/lance-graph/src/logical_plan.rs b/rust/lance-graph/src/logical_plan.rs index 754c53310..cc6dc225b 100644 --- a/rust/lance-graph/src/logical_plan.rs +++ b/rust/lance-graph/src/logical_plan.rs @@ -36,6 +36,7 @@ pub enum LogicalOperator { target_variable: String, relationship_types: Vec, direction: RelationshipDirection, + relationship_variable: Option, properties: HashMap, }, @@ -46,6 +47,7 @@ pub enum LogicalOperator { target_variable: String, relationship_types: Vec, direction: RelationshipDirection, + relationship_variable: Option, min_length: Option, max_length: Option, }, @@ -308,6 +310,7 @@ impl LogicalPlanner { target_variable: target_variable.clone(), relationship_types: segment.relationship.types.clone(), direction: segment.relationship.direction.clone(), + relationship_variable: segment.relationship.variable.clone(), properties: segment.relationship.properties.clone(), } } @@ -317,6 +320,7 @@ impl LogicalPlanner { target_variable: target_variable.clone(), relationship_types: segment.relationship.types.clone(), direction: segment.relationship.direction.clone(), + relationship_variable: segment.relationship.variable.clone(), min_length: length_range.min, max_length: length_range.max, }, @@ -326,6 +330,7 @@ impl LogicalPlanner { target_variable: target_variable.clone(), relationship_types: segment.relationship.types.clone(), direction: segment.relationship.direction.clone(), + relationship_variable: segment.relationship.variable.clone(), properties: segment.relationship.properties.clone(), }, }; diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index fafcf2486..0a3f4dcea 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -7,10 +7,7 @@ //! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface //! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions -use crate::ast::{ - BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection, - ValueExpression, -}; +use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; use crate::config::GraphConfig; use crate::error::{GraphError, Result}; use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; @@ -39,13 +36,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.convert_project(input, projections) } - LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate), + LogicalOperator::Filter { input, predicate } => { + self.convert_filter(input, predicate) + } - LogicalOperator::ScanByLabel { - variable, - label, - properties, - } => self.convert_scan(variable, label, properties), + LogicalOperator::ScanByLabel { variable, label, properties } => { + self.convert_scan(variable, label, properties) + } LogicalOperator::Expand { input, @@ -55,50 +52,59 @@ impl<'a> LogicalPlanToSqlConverter<'a> { direction, relationship_variable, properties, - } => self.convert_expand( - input, - source_variable, - target_variable, - relationship_types, - direction, - relationship_variable, - properties, - ), + } => { + self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties + ) + } - LogicalOperator::Distinct { input } => self.convert_distinct(input), + LogicalOperator::Distinct { input } => { + self.convert_distinct(input) + } - LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64), + LogicalOperator::Limit { input, count } => { + self.convert_limit(input, *count as i64) + } - LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64), + LogicalOperator::Offset { input, offset } => { + self.convert_offset(input, *offset as i64) + } - LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), + LogicalOperator::Sort { input, sort_items } => { + self.convert_sort(input, sort_items) + } // Complex operations that should fall back to legacy execution - LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { - message: "Variable length paths not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }), + LogicalOperator::VariableLengthExpand { .. } => { + Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } - LogicalOperator::Join { .. } => Err(GraphError::PlanError { - message: "Complex joins not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }), + LogicalOperator::Join { .. } => { + Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } } } - fn convert_project( - &mut self, - input: &LogicalOperator, - projections: &[ProjectionItem], - ) -> Result { + fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { let input_sql = self.convert(input)?; if projections.is_empty() { return Ok(format!("SELECT * FROM ({})", input_sql)); } - let proj_list = projections - .iter() + let proj_list = projections.iter() .map(|p| self.projection_to_sql(p)) .collect::>>()? .join(", "); @@ -106,42 +112,21 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) } - fn convert_filter( - &mut self, - input: &LogicalOperator, - predicate: &BooleanExpression, - ) -> Result { + fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { let input_sql = self.convert(input)?; let where_clause = self.boolean_expr_to_sql(predicate)?; - Ok(format!( - "SELECT * FROM ({}) WHERE {}", - input_sql, where_clause - )) + Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) } - fn convert_scan( - &mut self, - variable: &str, - label: &str, - properties: &HashMap, - ) -> Result { + fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { // Store table alias for this variable - use the variable name as the alias - self.table_aliases - .insert(variable.to_string(), variable.to_string()); + self.table_aliases.insert(variable.to_string(), variable.to_string()); let mut sql = format!("SELECT * FROM {} AS {}", label, variable); if !properties.is_empty() { - let filters = properties - .iter() - .map(|(k, v)| { - Ok(format!( - "{}.{} = {}", - variable, - k, - self.property_value_to_sql(v)? - )) - }) + let filters = properties.iter() + .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) .collect::>>()? .join(" AND "); sql = format!("{} WHERE {}", sql, filters); @@ -161,25 +146,20 @@ impl<'a> LogicalPlanToSqlConverter<'a> { properties: &HashMap, ) -> Result { let input_sql = self.convert(input)?; - let rel_type = relationship_types - .first() - .ok_or_else(|| GraphError::PlanError { - message: "No relationship type specified".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { message: "Config required for relationship queries".to_string(), location: snafu::Location::new(file!(), line!(), column!()), })?; - let rel_mapping = - config - .get_relationship_mapping(rel_type) - .ok_or_else(|| GraphError::PlanError { - message: format!("No relationship mapping for {}", rel_type), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; // Generate unique aliases let src_alias = format!("src_{}", self.variable_counter); @@ -188,92 +168,19 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.variable_counter += 1; // Store aliases for variables - self.table_aliases - .insert(source_variable.to_string(), src_alias.clone()); - self.table_aliases - .insert(target_variable.to_string(), tgt_alias.clone()); + self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); if let Some(rel_var) = relationship_variable { - self.table_aliases - .insert(rel_var.clone(), rel_alias.clone()); + self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); } - let join_sql = match direction { - RelationshipDirection::Outgoing => format!( - "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, - src_alias, - rel_type, - rel_alias, - src_alias, - rel_alias, - rel_mapping.source_id_field, - target_variable, - tgt_alias, - rel_alias, - rel_mapping.target_id_field, - tgt_alias - ), - RelationshipDirection::Incoming => format!( - "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, - src_alias, - rel_type, - rel_alias, - src_alias, - rel_alias, - rel_mapping.target_id_field, - target_variable, - tgt_alias, - rel_alias, - rel_mapping.source_id_field, - tgt_alias - ), - RelationshipDirection::Undirected => { - // For undirected, we need a UNION of both directions - let outgoing = format!( - "({}) AS {} JOIN {} AS {} ON {}.id = {}.{} JOIN {} AS {} ON {}.{} = {}.id", - input_sql, - src_alias, - rel_type, - rel_alias, - src_alias, - rel_alias, - rel_mapping.source_id_field, - target_variable, - tgt_alias, - rel_alias, - rel_mapping.target_id_field, - tgt_alias - ); - let incoming = format!( - "({}) AS {}_2 JOIN {} AS {}_2 ON {}_2.id = {}_2.{} JOIN {} AS {}_2 ON {}_2.{} = {}_2.id", - input_sql, src_alias, rel_type, rel_alias, src_alias, rel_alias, rel_mapping.target_id_field, - target_variable, tgt_alias, rel_alias, rel_mapping.source_id_field, tgt_alias - ); - format!("({}) UNION ALL ({})", outgoing, incoming) - } - }; - - let mut result_sql = format!("SELECT * FROM {}", join_sql); - - // Add relationship property filters if any - if !properties.is_empty() { - let rel_filters = properties - .iter() - .map(|(k, v)| { - Ok(format!( - "{}.{} = {}", - rel_alias, - k, - self.property_value_to_sql(v)? - )) - }) - .collect::>>()? - .join(" AND "); - result_sql = format!("{} WHERE {}", result_sql, rel_filters); - } - - Ok(result_sql) + // TODO: This code hardcodes "id" but should use configured ID fields + // The problem is we need to track variable -> label mappings to get the right ID field + // For now, we'll return an error since relationship queries are unsupported anyway + Err(GraphError::PlanError { + message: "Relationship traversal not supported in SQL conversion - would require proper ID field mapping".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) } fn convert_distinct(&mut self, input: &LogicalOperator) -> Result { @@ -291,11 +198,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) } - fn convert_sort( - &mut self, - input: &LogicalOperator, - _sort_items: &[SortItem], - ) -> Result { + fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { // For now, just pass through the input (ORDER BY is complex to implement) // TODO: Implement proper ORDER BY conversion self.convert(input) @@ -313,11 +216,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { match expr { - BooleanExpression::Comparison { - left, - operator, - right, - } => { + BooleanExpression::Comparison { left, operator, right } => { let left_sql = self.value_expr_to_sql(left)?; let right_sql = self.value_expr_to_sql(right)?; let op_sql = match operator { @@ -333,8 +232,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { BooleanExpression::In { expression, list } => { let expr_sql = self.value_expr_to_sql(expression)?; - let list_sql = list - .iter() + let list_sql = list.iter() .map(|v| self.value_expr_to_sql(v)) .collect::>>()? .join(", "); @@ -366,7 +264,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported boolean expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }), + }) } } @@ -378,7 +276,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported value expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }), + }) } } @@ -407,7 +305,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression}; + use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; use crate::logical_plan::{LogicalOperator, ProjectionItem}; use std::collections::HashMap; @@ -430,10 +328,7 @@ mod tests { let mut converter = LogicalPlanToSqlConverter::new(&None); let mut properties = HashMap::new(); - properties.insert( - "name".to_string(), - PropertyValue::String("Alice".to_string()), - ); + properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); let scan = LogicalOperator::ScanByLabel { variable: "n".to_string(), @@ -493,9 +388,6 @@ mod tests { }; let sql = converter.convert(&filter).unwrap(); - assert_eq!( - sql, - "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30" - ); + assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); } } From 101388e7672c074b6c436f0fd42540a3a8e19075 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:32:06 -0700 Subject: [PATCH 4/7] format code --- rust/lance-graph/src/sql_converter.rs | 176 ++++++++++++++++---------- 1 file changed, 107 insertions(+), 69 deletions(-) diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index 0a3f4dcea..ded24abab 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -7,7 +7,10 @@ //! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface //! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions -use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; +use crate::ast::{ + BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection, + ValueExpression, +}; use crate::config::GraphConfig; use crate::error::{GraphError, Result}; use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; @@ -36,13 +39,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.convert_project(input, projections) } - LogicalOperator::Filter { input, predicate } => { - self.convert_filter(input, predicate) - } + LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate), - LogicalOperator::ScanByLabel { variable, label, properties } => { - self.convert_scan(variable, label, properties) - } + LogicalOperator::ScanByLabel { + variable, + label, + properties, + } => self.convert_scan(variable, label, properties), LogicalOperator::Expand { input, @@ -52,59 +55,50 @@ impl<'a> LogicalPlanToSqlConverter<'a> { direction, relationship_variable, properties, - } => { - self.convert_expand( - input, - source_variable, - target_variable, - relationship_types, - direction, - relationship_variable, - properties - ) - } + } => self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties, + ), - LogicalOperator::Distinct { input } => { - self.convert_distinct(input) - } + LogicalOperator::Distinct { input } => self.convert_distinct(input), - LogicalOperator::Limit { input, count } => { - self.convert_limit(input, *count as i64) - } + LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64), - LogicalOperator::Offset { input, offset } => { - self.convert_offset(input, *offset as i64) - } + LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64), - LogicalOperator::Sort { input, sort_items } => { - self.convert_sort(input, sort_items) - } + LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), // Complex operations that should fall back to legacy execution - LogicalOperator::VariableLengthExpand { .. } => { - Err(GraphError::PlanError { - message: "Variable length paths not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), - LogicalOperator::Join { .. } => { - Err(GraphError::PlanError { - message: "Complex joins not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::Join { .. } => Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), } } - fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { + fn convert_project( + &mut self, + input: &LogicalOperator, + projections: &[ProjectionItem], + ) -> Result { let input_sql = self.convert(input)?; if projections.is_empty() { return Ok(format!("SELECT * FROM ({})", input_sql)); } - let proj_list = projections.iter() + let proj_list = projections + .iter() .map(|p| self.projection_to_sql(p)) .collect::>>()? .join(", "); @@ -112,21 +106,42 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) } - fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { + fn convert_filter( + &mut self, + input: &LogicalOperator, + predicate: &BooleanExpression, + ) -> Result { let input_sql = self.convert(input)?; let where_clause = self.boolean_expr_to_sql(predicate)?; - Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) + Ok(format!( + "SELECT * FROM ({}) WHERE {}", + input_sql, where_clause + )) } - fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { + fn convert_scan( + &mut self, + variable: &str, + label: &str, + properties: &HashMap, + ) -> Result { // Store table alias for this variable - use the variable name as the alias - self.table_aliases.insert(variable.to_string(), variable.to_string()); + self.table_aliases + .insert(variable.to_string(), variable.to_string()); let mut sql = format!("SELECT * FROM {} AS {}", label, variable); if !properties.is_empty() { - let filters = properties.iter() - .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) + let filters = properties + .iter() + .map(|(k, v)| { + Ok(format!( + "{}.{} = {}", + variable, + k, + self.property_value_to_sql(v)? + )) + }) .collect::>>()? .join(" AND "); sql = format!("{} WHERE {}", sql, filters); @@ -146,20 +161,25 @@ impl<'a> LogicalPlanToSqlConverter<'a> { properties: &HashMap, ) -> Result { let input_sql = self.convert(input)?; - let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { - message: "No relationship type specified".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_type = relationship_types + .first() + .ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { message: "Config required for relationship queries".to_string(), location: snafu::Location::new(file!(), line!(), column!()), })?; - let rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { - message: format!("No relationship mapping for {}", rel_type), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_mapping = + config + .get_relationship_mapping(rel_type) + .ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; // Generate unique aliases let src_alias = format!("src_{}", self.variable_counter); @@ -168,10 +188,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.variable_counter += 1; // Store aliases for variables - self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); - self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); + self.table_aliases + .insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases + .insert(target_variable.to_string(), tgt_alias.clone()); if let Some(rel_var) = relationship_variable { - self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); + self.table_aliases + .insert(rel_var.clone(), rel_alias.clone()); } // TODO: This code hardcodes "id" but should use configured ID fields @@ -198,7 +221,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) } - fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { + fn convert_sort( + &mut self, + input: &LogicalOperator, + _sort_items: &[SortItem], + ) -> Result { // For now, just pass through the input (ORDER BY is complex to implement) // TODO: Implement proper ORDER BY conversion self.convert(input) @@ -216,7 +243,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { match expr { - BooleanExpression::Comparison { left, operator, right } => { + BooleanExpression::Comparison { + left, + operator, + right, + } => { let left_sql = self.value_expr_to_sql(left)?; let right_sql = self.value_expr_to_sql(right)?; let op_sql = match operator { @@ -232,7 +263,8 @@ impl<'a> LogicalPlanToSqlConverter<'a> { BooleanExpression::In { expression, list } => { let expr_sql = self.value_expr_to_sql(expression)?; - let list_sql = list.iter() + let list_sql = list + .iter() .map(|v| self.value_expr_to_sql(v)) .collect::>>()? .join(", "); @@ -264,7 +296,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported boolean expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -276,7 +308,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported value expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -305,7 +337,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; + use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression}; use crate::logical_plan::{LogicalOperator, ProjectionItem}; use std::collections::HashMap; @@ -328,7 +360,10 @@ mod tests { let mut converter = LogicalPlanToSqlConverter::new(&None); let mut properties = HashMap::new(); - properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); + properties.insert( + "name".to_string(), + PropertyValue::String("Alice".to_string()), + ); let scan = LogicalOperator::ScanByLabel { variable: "n".to_string(), @@ -388,6 +423,9 @@ mod tests { }; let sql = converter.convert(&filter).unwrap(); - assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); + assert_eq!( + sql, + "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30" + ); } } From 0f4f5c37a9bdc06aad2c3bd67260194625078ca3 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:36:57 -0700 Subject: [PATCH 5/7] fix warnings --- rust/lance-graph/src/sql_converter.rs | 186 ++++++++++---------------- 1 file changed, 74 insertions(+), 112 deletions(-) diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index ded24abab..2e112bca6 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -7,10 +7,7 @@ //! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface //! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions -use crate::ast::{ - BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection, - ValueExpression, -}; +use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; use crate::config::GraphConfig; use crate::error::{GraphError, Result}; use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; @@ -39,13 +36,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.convert_project(input, projections) } - LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate), + LogicalOperator::Filter { input, predicate } => { + self.convert_filter(input, predicate) + } - LogicalOperator::ScanByLabel { - variable, - label, - properties, - } => self.convert_scan(variable, label, properties), + LogicalOperator::ScanByLabel { variable, label, properties } => { + self.convert_scan(variable, label, properties) + } LogicalOperator::Expand { input, @@ -55,50 +52,59 @@ impl<'a> LogicalPlanToSqlConverter<'a> { direction, relationship_variable, properties, - } => self.convert_expand( - input, - source_variable, - target_variable, - relationship_types, - direction, - relationship_variable, - properties, - ), + } => { + self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties + ) + } - LogicalOperator::Distinct { input } => self.convert_distinct(input), + LogicalOperator::Distinct { input } => { + self.convert_distinct(input) + } - LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64), + LogicalOperator::Limit { input, count } => { + self.convert_limit(input, *count as i64) + } - LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64), + LogicalOperator::Offset { input, offset } => { + self.convert_offset(input, *offset as i64) + } - LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), + LogicalOperator::Sort { input, sort_items } => { + self.convert_sort(input, sort_items) + } // Complex operations that should fall back to legacy execution - LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { - message: "Variable length paths not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }), + LogicalOperator::VariableLengthExpand { .. } => { + Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } - LogicalOperator::Join { .. } => Err(GraphError::PlanError { - message: "Complex joins not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }), + LogicalOperator::Join { .. } => { + Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }) + } } } - fn convert_project( - &mut self, - input: &LogicalOperator, - projections: &[ProjectionItem], - ) -> Result { + fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { let input_sql = self.convert(input)?; if projections.is_empty() { return Ok(format!("SELECT * FROM ({})", input_sql)); } - let proj_list = projections - .iter() + let proj_list = projections.iter() .map(|p| self.projection_to_sql(p)) .collect::>>()? .join(", "); @@ -106,42 +112,21 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) } - fn convert_filter( - &mut self, - input: &LogicalOperator, - predicate: &BooleanExpression, - ) -> Result { + fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { let input_sql = self.convert(input)?; let where_clause = self.boolean_expr_to_sql(predicate)?; - Ok(format!( - "SELECT * FROM ({}) WHERE {}", - input_sql, where_clause - )) + Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) } - fn convert_scan( - &mut self, - variable: &str, - label: &str, - properties: &HashMap, - ) -> Result { + fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { // Store table alias for this variable - use the variable name as the alias - self.table_aliases - .insert(variable.to_string(), variable.to_string()); + self.table_aliases.insert(variable.to_string(), variable.to_string()); let mut sql = format!("SELECT * FROM {} AS {}", label, variable); if !properties.is_empty() { - let filters = properties - .iter() - .map(|(k, v)| { - Ok(format!( - "{}.{} = {}", - variable, - k, - self.property_value_to_sql(v)? - )) - }) + let filters = properties.iter() + .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) .collect::>>()? .join(" AND "); sql = format!("{} WHERE {}", sql, filters); @@ -152,34 +137,29 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn convert_expand( &mut self, - input: &LogicalOperator, + _input: &LogicalOperator, source_variable: &str, target_variable: &str, relationship_types: &[String], - direction: &RelationshipDirection, + _direction: &RelationshipDirection, relationship_variable: &Option, - properties: &HashMap, + _properties: &HashMap, ) -> Result { - let input_sql = self.convert(input)?; - let rel_type = relationship_types - .first() - .ok_or_else(|| GraphError::PlanError { - message: "No relationship type specified".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let _input_sql = self.convert(_input)?; + let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { message: "Config required for relationship queries".to_string(), location: snafu::Location::new(file!(), line!(), column!()), })?; - let rel_mapping = - config - .get_relationship_mapping(rel_type) - .ok_or_else(|| GraphError::PlanError { - message: format!("No relationship mapping for {}", rel_type), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let _rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; // Generate unique aliases let src_alias = format!("src_{}", self.variable_counter); @@ -188,16 +168,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.variable_counter += 1; // Store aliases for variables - self.table_aliases - .insert(source_variable.to_string(), src_alias.clone()); - self.table_aliases - .insert(target_variable.to_string(), tgt_alias.clone()); + self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); if let Some(rel_var) = relationship_variable { - self.table_aliases - .insert(rel_var.clone(), rel_alias.clone()); + self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); } - // TODO: This code hardcodes "id" but should use configured ID fields + // TODO: CRITICAL BUG - This code hardcodes "id" but should use configured ID fields // The problem is we need to track variable -> label mappings to get the right ID field // For now, we'll return an error since relationship queries are unsupported anyway Err(GraphError::PlanError { @@ -221,11 +198,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) } - fn convert_sort( - &mut self, - input: &LogicalOperator, - _sort_items: &[SortItem], - ) -> Result { + fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { // For now, just pass through the input (ORDER BY is complex to implement) // TODO: Implement proper ORDER BY conversion self.convert(input) @@ -243,11 +216,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { match expr { - BooleanExpression::Comparison { - left, - operator, - right, - } => { + BooleanExpression::Comparison { left, operator, right } => { let left_sql = self.value_expr_to_sql(left)?; let right_sql = self.value_expr_to_sql(right)?; let op_sql = match operator { @@ -263,8 +232,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { BooleanExpression::In { expression, list } => { let expr_sql = self.value_expr_to_sql(expression)?; - let list_sql = list - .iter() + let list_sql = list.iter() .map(|v| self.value_expr_to_sql(v)) .collect::>>()? .join(", "); @@ -296,7 +264,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported boolean expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }), + }) } } @@ -308,7 +276,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported value expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }), + }) } } @@ -337,7 +305,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression}; + use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; use crate::logical_plan::{LogicalOperator, ProjectionItem}; use std::collections::HashMap; @@ -360,10 +328,7 @@ mod tests { let mut converter = LogicalPlanToSqlConverter::new(&None); let mut properties = HashMap::new(); - properties.insert( - "name".to_string(), - PropertyValue::String("Alice".to_string()), - ); + properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); let scan = LogicalOperator::ScanByLabel { variable: "n".to_string(), @@ -423,9 +388,6 @@ mod tests { }; let sql = converter.convert(&filter).unwrap(); - assert_eq!( - sql, - "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30" - ); + assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); } } From fea2fee3cbeaa06ca4488947d68a981f1375492a Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:37:44 -0700 Subject: [PATCH 6/7] format code --- rust/lance-graph/src/sql_converter.rs | 176 ++++++++++++++++---------- 1 file changed, 107 insertions(+), 69 deletions(-) diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index 2e112bca6..5b9dc216e 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -7,7 +7,10 @@ //! - **DataFusion**: Direct SQL execution via DataFusion's SQL interface //! - **LanceDB**: Native SQL execution with potential for vector/full-text extensions -use crate::ast::{PropertyValue, RelationshipDirection, ValueExpression, BooleanExpression, ComparisonOperator, PropertyRef}; +use crate::ast::{ + BooleanExpression, ComparisonOperator, PropertyRef, PropertyValue, RelationshipDirection, + ValueExpression, +}; use crate::config::GraphConfig; use crate::error::{GraphError, Result}; use crate::logical_plan::{LogicalOperator, ProjectionItem, SortItem}; @@ -36,13 +39,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.convert_project(input, projections) } - LogicalOperator::Filter { input, predicate } => { - self.convert_filter(input, predicate) - } + LogicalOperator::Filter { input, predicate } => self.convert_filter(input, predicate), - LogicalOperator::ScanByLabel { variable, label, properties } => { - self.convert_scan(variable, label, properties) - } + LogicalOperator::ScanByLabel { + variable, + label, + properties, + } => self.convert_scan(variable, label, properties), LogicalOperator::Expand { input, @@ -52,59 +55,50 @@ impl<'a> LogicalPlanToSqlConverter<'a> { direction, relationship_variable, properties, - } => { - self.convert_expand( - input, - source_variable, - target_variable, - relationship_types, - direction, - relationship_variable, - properties - ) - } + } => self.convert_expand( + input, + source_variable, + target_variable, + relationship_types, + direction, + relationship_variable, + properties, + ), - LogicalOperator::Distinct { input } => { - self.convert_distinct(input) - } + LogicalOperator::Distinct { input } => self.convert_distinct(input), - LogicalOperator::Limit { input, count } => { - self.convert_limit(input, *count as i64) - } + LogicalOperator::Limit { input, count } => self.convert_limit(input, *count as i64), - LogicalOperator::Offset { input, offset } => { - self.convert_offset(input, *offset as i64) - } + LogicalOperator::Offset { input, offset } => self.convert_offset(input, *offset as i64), - LogicalOperator::Sort { input, sort_items } => { - self.convert_sort(input, sort_items) - } + LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), // Complex operations that should fall back to legacy execution - LogicalOperator::VariableLengthExpand { .. } => { - Err(GraphError::PlanError { - message: "Variable length paths not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { + message: "Variable length paths not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), - LogicalOperator::Join { .. } => { - Err(GraphError::PlanError { - message: "Complex joins not supported in SQL conversion".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - }) - } + LogicalOperator::Join { .. } => Err(GraphError::PlanError { + message: "Complex joins not supported in SQL conversion".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + }), } } - fn convert_project(&mut self, input: &LogicalOperator, projections: &[ProjectionItem]) -> Result { + fn convert_project( + &mut self, + input: &LogicalOperator, + projections: &[ProjectionItem], + ) -> Result { let input_sql = self.convert(input)?; if projections.is_empty() { return Ok(format!("SELECT * FROM ({})", input_sql)); } - let proj_list = projections.iter() + let proj_list = projections + .iter() .map(|p| self.projection_to_sql(p)) .collect::>>()? .join(", "); @@ -112,21 +106,42 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT {} FROM ({})", proj_list, input_sql)) } - fn convert_filter(&mut self, input: &LogicalOperator, predicate: &BooleanExpression) -> Result { + fn convert_filter( + &mut self, + input: &LogicalOperator, + predicate: &BooleanExpression, + ) -> Result { let input_sql = self.convert(input)?; let where_clause = self.boolean_expr_to_sql(predicate)?; - Ok(format!("SELECT * FROM ({}) WHERE {}", input_sql, where_clause)) + Ok(format!( + "SELECT * FROM ({}) WHERE {}", + input_sql, where_clause + )) } - fn convert_scan(&mut self, variable: &str, label: &str, properties: &HashMap) -> Result { + fn convert_scan( + &mut self, + variable: &str, + label: &str, + properties: &HashMap, + ) -> Result { // Store table alias for this variable - use the variable name as the alias - self.table_aliases.insert(variable.to_string(), variable.to_string()); + self.table_aliases + .insert(variable.to_string(), variable.to_string()); let mut sql = format!("SELECT * FROM {} AS {}", label, variable); if !properties.is_empty() { - let filters = properties.iter() - .map(|(k, v)| Ok(format!("{}.{} = {}", variable, k, self.property_value_to_sql(v)?))) + let filters = properties + .iter() + .map(|(k, v)| { + Ok(format!( + "{}.{} = {}", + variable, + k, + self.property_value_to_sql(v)? + )) + }) .collect::>>()? .join(" AND "); sql = format!("{} WHERE {}", sql, filters); @@ -146,20 +161,25 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _properties: &HashMap, ) -> Result { let _input_sql = self.convert(_input)?; - let rel_type = relationship_types.first().ok_or_else(|| GraphError::PlanError { - message: "No relationship type specified".to_string(), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let rel_type = relationship_types + .first() + .ok_or_else(|| GraphError::PlanError { + message: "No relationship type specified".to_string(), + location: snafu::Location::new(file!(), line!(), column!()), + })?; let config = self.config.as_ref().ok_or_else(|| GraphError::PlanError { message: "Config required for relationship queries".to_string(), location: snafu::Location::new(file!(), line!(), column!()), })?; - let _rel_mapping = config.get_relationship_mapping(rel_type).ok_or_else(|| GraphError::PlanError { - message: format!("No relationship mapping for {}", rel_type), - location: snafu::Location::new(file!(), line!(), column!()), - })?; + let _rel_mapping = + config + .get_relationship_mapping(rel_type) + .ok_or_else(|| GraphError::PlanError { + message: format!("No relationship mapping for {}", rel_type), + location: snafu::Location::new(file!(), line!(), column!()), + })?; // Generate unique aliases let src_alias = format!("src_{}", self.variable_counter); @@ -168,10 +188,13 @@ impl<'a> LogicalPlanToSqlConverter<'a> { self.variable_counter += 1; // Store aliases for variables - self.table_aliases.insert(source_variable.to_string(), src_alias.clone()); - self.table_aliases.insert(target_variable.to_string(), tgt_alias.clone()); + self.table_aliases + .insert(source_variable.to_string(), src_alias.clone()); + self.table_aliases + .insert(target_variable.to_string(), tgt_alias.clone()); if let Some(rel_var) = relationship_variable { - self.table_aliases.insert(rel_var.clone(), rel_alias.clone()); + self.table_aliases + .insert(rel_var.clone(), rel_alias.clone()); } // TODO: CRITICAL BUG - This code hardcodes "id" but should use configured ID fields @@ -198,7 +221,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(format!("SELECT * FROM ({}) OFFSET {}", input_sql, offset)) } - fn convert_sort(&mut self, input: &LogicalOperator, _sort_items: &[SortItem]) -> Result { + fn convert_sort( + &mut self, + input: &LogicalOperator, + _sort_items: &[SortItem], + ) -> Result { // For now, just pass through the input (ORDER BY is complex to implement) // TODO: Implement proper ORDER BY conversion self.convert(input) @@ -216,7 +243,11 @@ impl<'a> LogicalPlanToSqlConverter<'a> { fn boolean_expr_to_sql(&self, expr: &BooleanExpression) -> Result { match expr { - BooleanExpression::Comparison { left, operator, right } => { + BooleanExpression::Comparison { + left, + operator, + right, + } => { let left_sql = self.value_expr_to_sql(left)?; let right_sql = self.value_expr_to_sql(right)?; let op_sql = match operator { @@ -232,7 +263,8 @@ impl<'a> LogicalPlanToSqlConverter<'a> { BooleanExpression::In { expression, list } => { let expr_sql = self.value_expr_to_sql(expression)?; - let list_sql = list.iter() + let list_sql = list + .iter() .map(|v| self.value_expr_to_sql(v)) .collect::>>()? .join(", "); @@ -264,7 +296,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported boolean expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -276,7 +308,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { _ => Err(GraphError::PlanError { message: "Unsupported value expression in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), - }) + }), } } @@ -305,7 +337,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{PropertyRef, ValueExpression, BooleanExpression, ComparisonOperator}; + use crate::ast::{BooleanExpression, ComparisonOperator, PropertyRef, ValueExpression}; use crate::logical_plan::{LogicalOperator, ProjectionItem}; use std::collections::HashMap; @@ -328,7 +360,10 @@ mod tests { let mut converter = LogicalPlanToSqlConverter::new(&None); let mut properties = HashMap::new(); - properties.insert("name".to_string(), PropertyValue::String("Alice".to_string())); + properties.insert( + "name".to_string(), + PropertyValue::String("Alice".to_string()), + ); let scan = LogicalOperator::ScanByLabel { variable: "n".to_string(), @@ -388,6 +423,9 @@ mod tests { }; let sql = converter.convert(&filter).unwrap(); - assert_eq!(sql, "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30"); + assert_eq!( + sql, + "SELECT * FROM (SELECT * FROM Person AS n) WHERE n.age > 30" + ); } } From a47b5a15e7838bb5c1b7c4e571231d456253cae1 Mon Sep 17 00:00:00 2001 From: JoshuaTang <1240604020@qq.com> Date: Sat, 25 Oct 2025 20:44:34 -0700 Subject: [PATCH 7/7] fix warning --- rust/lance-graph/src/sql_converter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/lance-graph/src/sql_converter.rs b/rust/lance-graph/src/sql_converter.rs index 5b9dc216e..6180b7f83 100644 --- a/rust/lance-graph/src/sql_converter.rs +++ b/rust/lance-graph/src/sql_converter.rs @@ -73,7 +73,6 @@ impl<'a> LogicalPlanToSqlConverter<'a> { LogicalOperator::Sort { input, sort_items } => self.convert_sort(input, sort_items), - // Complex operations that should fall back to legacy execution LogicalOperator::VariableLengthExpand { .. } => Err(GraphError::PlanError { message: "Variable length paths not supported in SQL conversion".to_string(), location: snafu::Location::new(file!(), line!(), column!()), @@ -150,6 +149,7 @@ impl<'a> LogicalPlanToSqlConverter<'a> { Ok(sql) } + #[allow(clippy::too_many_arguments)] fn convert_expand( &mut self, _input: &LogicalOperator,