Skip to content

Commit af1e4bd

Browse files
authored
Support with_parameter and with_parameters in query engine (#125)
Support $param placeholder in query engine. Supported Scalar Types: Null Boolean Integer Float String Supported Complex Types: Float Vectors: Lists of numbers (e.g., [0.1, 0.2, 0.3]) are supported and converted to VectorLiteral (as Vec<f32>). Key Changes: 1. Stored user parameters in a HashMap<String, serde_json::Value> within the CypherQuery 2. Propagated parameters to the semantic analysis layer, where they are resolved and validated. 3. Implemented a parameter substitution pass (parameter_substitution.rs) that replaces parameter placeholders (e.g., $name) with their corresponding user-provided literal values (Scalar or Vector) directly in the AST before logical planning. 4. Added end-to-end tests in tests/test_parameter_examples.rs (in plan) and verified existing tests to ensure correct parsing and execution of parameterized queries. Closes #103.
1 parent c62edfb commit af1e4bd

8 files changed

Lines changed: 531 additions & 46 deletions

File tree

crates/lance-graph/src/datafusion_planner/expression.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ pub(crate) fn to_df_value_expr(expr: &ValueExpression) -> Expr {
122122
VE::Literal(PV::Null) => {
123123
datafusion::logical_expr::Expr::Literal(datafusion::scalar::ScalarValue::Null, None)
124124
}
125-
VE::Literal(PV::Parameter(_)) => lit(0),
125+
VE::Literal(PV::Parameter(name)) => {
126+
panic!(
127+
"Parameter ${} should have been substituted during semantic analysis",
128+
name
129+
);
130+
}
126131
VE::Literal(PV::Property(prop)) => {
127132
// Create qualified column name: variable__property (lowercase for case-insensitivity)
128133
col(qualify_column(&prop.variable, &prop.property))
@@ -316,18 +321,10 @@ pub(crate) fn to_df_value_expr(expr: &ValueExpression) -> Expr {
316321
lit(scalar)
317322
}
318323
VE::Parameter(name) => {
319-
// TODO: Implement proper parameter resolution
320-
// Parameters ($param) should be resolved to literal values from the query's
321-
// parameter map (CypherQuery::parameters()) before or during planning.
322-
//
323-
// Current limitation: This creates a column reference as a placeholder,
324-
// which will fail at execution if the column doesn't exist.
325-
//
326-
// Proper fix requires one of:
327-
// 1. Resolve parameters during semantic analysis (substitute before planning)
328-
// 2. Pass parameter map to to_df_value_expr and resolve here
329-
// 3. Use DataFusion's parameter binding mechanism
330-
col(format!("${}", name))
324+
panic!(
325+
"Parameter ${} should have been substituted during semantic analysis",
326+
name
327+
);
331328
}
332329
}
333330
}

crates/lance-graph/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub mod error;
4343
pub mod lance_native_planner;
4444
pub mod lance_vector_search;
4545
pub mod logical_plan;
46+
pub mod parameter_substitution;
4647
pub mod parser;
4748
pub mod query;
4849
pub mod semantic;
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
use crate::ast::*;
2+
use crate::error::{GraphError, Result};
3+
use std::collections::HashMap;
4+
5+
/// Substitute parameters with literal values in the AST
6+
pub fn substitute_parameters(
7+
query: &mut CypherQuery,
8+
parameters: &HashMap<String, serde_json::Value>,
9+
) -> Result<()> {
10+
// Substitute in READING clauses
11+
for reading_clause in &mut query.reading_clauses {
12+
substitute_in_reading_clause(reading_clause, parameters)?;
13+
}
14+
15+
// Substitute in WHERE clause
16+
if let Some(where_clause) = &mut query.where_clause {
17+
substitute_in_where_clause(where_clause, parameters)?;
18+
}
19+
20+
// Substitute in WITH clause
21+
if let Some(with_clause) = &mut query.with_clause {
22+
substitute_in_with_clause(with_clause, parameters)?;
23+
}
24+
25+
// Substitute in post-WITH READING clauses
26+
for reading_clause in &mut query.post_with_reading_clauses {
27+
substitute_in_reading_clause(reading_clause, parameters)?;
28+
}
29+
30+
// Substitute in post-WITH WHERE clause
31+
if let Some(post_where) = &mut query.post_with_where_clause {
32+
substitute_in_where_clause(post_where, parameters)?;
33+
}
34+
35+
// Substitute in RETURN clause
36+
substitute_in_return_clause(&mut query.return_clause, parameters)?;
37+
38+
// Substitute in ORDER BY clause
39+
if let Some(order_by) = &mut query.order_by {
40+
substitute_in_order_by_clause(order_by, parameters)?;
41+
}
42+
43+
Ok(())
44+
}
45+
46+
fn substitute_in_reading_clause(
47+
clause: &mut ReadingClause,
48+
parameters: &HashMap<String, serde_json::Value>,
49+
) -> Result<()> {
50+
match clause {
51+
ReadingClause::Match(match_clause) => {
52+
for pattern in &mut match_clause.patterns {
53+
substitute_in_graph_pattern(pattern, parameters)?;
54+
}
55+
}
56+
ReadingClause::Unwind(unwind_clause) => {
57+
substitute_in_value_expression(&mut unwind_clause.expression, parameters)?;
58+
}
59+
}
60+
Ok(())
61+
}
62+
63+
fn substitute_in_graph_pattern(
64+
pattern: &mut GraphPattern,
65+
parameters: &HashMap<String, serde_json::Value>,
66+
) -> Result<()> {
67+
match pattern {
68+
GraphPattern::Node(node) => {
69+
for value in node.properties.values_mut() {
70+
substitute_in_property_value(value, parameters)?;
71+
}
72+
}
73+
GraphPattern::Path(path) => {
74+
substitute_in_node_pattern(&mut path.start_node, parameters)?;
75+
for segment in &mut path.segments {
76+
substitute_in_relationship_pattern(&mut segment.relationship, parameters)?;
77+
substitute_in_node_pattern(&mut segment.end_node, parameters)?;
78+
}
79+
}
80+
}
81+
Ok(())
82+
}
83+
84+
fn substitute_in_node_pattern(
85+
node: &mut NodePattern,
86+
parameters: &HashMap<String, serde_json::Value>,
87+
) -> Result<()> {
88+
for value in node.properties.values_mut() {
89+
substitute_in_property_value(value, parameters)?;
90+
}
91+
Ok(())
92+
}
93+
94+
fn substitute_in_relationship_pattern(
95+
rel: &mut RelationshipPattern,
96+
parameters: &HashMap<String, serde_json::Value>,
97+
) -> Result<()> {
98+
for value in rel.properties.values_mut() {
99+
substitute_in_property_value(value, parameters)?;
100+
}
101+
Ok(())
102+
}
103+
104+
fn substitute_in_property_value(
105+
value: &mut PropertyValue,
106+
parameters: &HashMap<String, serde_json::Value>,
107+
) -> Result<()> {
108+
if let PropertyValue::Parameter(name) = value {
109+
let param_value =
110+
parameters
111+
.get(&name.to_lowercase())
112+
.ok_or_else(|| GraphError::PlanError {
113+
message: format!("Missing parameter: ${}", name),
114+
location: snafu::Location::new(file!(), line!(), column!()),
115+
})?;
116+
117+
*value = json_to_property_value(param_value)?;
118+
}
119+
Ok(())
120+
}
121+
122+
fn substitute_in_where_clause(
123+
where_clause: &mut WhereClause,
124+
parameters: &HashMap<String, serde_json::Value>,
125+
) -> Result<()> {
126+
substitute_in_boolean_expression(&mut where_clause.expression, parameters)
127+
}
128+
129+
fn substitute_in_with_clause(
130+
with_clause: &mut WithClause,
131+
parameters: &HashMap<String, serde_json::Value>,
132+
) -> Result<()> {
133+
for item in &mut with_clause.items {
134+
substitute_in_value_expression(&mut item.expression, parameters)?;
135+
}
136+
if let Some(order_by) = &mut with_clause.order_by {
137+
substitute_in_order_by_clause(order_by, parameters)?;
138+
}
139+
Ok(())
140+
}
141+
142+
fn substitute_in_return_clause(
143+
return_clause: &mut ReturnClause,
144+
parameters: &HashMap<String, serde_json::Value>,
145+
) -> Result<()> {
146+
for item in &mut return_clause.items {
147+
substitute_in_value_expression(&mut item.expression, parameters)?;
148+
}
149+
Ok(())
150+
}
151+
152+
fn substitute_in_order_by_clause(
153+
order_by: &mut OrderByClause,
154+
parameters: &HashMap<String, serde_json::Value>,
155+
) -> Result<()> {
156+
for item in &mut order_by.items {
157+
substitute_in_value_expression(&mut item.expression, parameters)?;
158+
}
159+
Ok(())
160+
}
161+
162+
fn substitute_in_boolean_expression(
163+
expr: &mut BooleanExpression,
164+
parameters: &HashMap<String, serde_json::Value>,
165+
) -> Result<()> {
166+
match expr {
167+
BooleanExpression::Comparison { left, right, .. } => {
168+
substitute_in_value_expression(left, parameters)?;
169+
substitute_in_value_expression(right, parameters)?;
170+
}
171+
BooleanExpression::And(left, right) | BooleanExpression::Or(left, right) => {
172+
substitute_in_boolean_expression(left, parameters)?;
173+
substitute_in_boolean_expression(right, parameters)?;
174+
}
175+
BooleanExpression::Not(inner) => {
176+
substitute_in_boolean_expression(inner, parameters)?;
177+
}
178+
BooleanExpression::Exists(_) => {}
179+
BooleanExpression::In { expression, list } => {
180+
substitute_in_value_expression(expression, parameters)?;
181+
for item in list {
182+
substitute_in_value_expression(item, parameters)?;
183+
}
184+
}
185+
BooleanExpression::Like { expression, .. }
186+
| BooleanExpression::ILike { expression, .. }
187+
| BooleanExpression::Contains { expression, .. }
188+
| BooleanExpression::StartsWith { expression, .. }
189+
| BooleanExpression::EndsWith { expression, .. }
190+
| BooleanExpression::IsNull(expression)
191+
| BooleanExpression::IsNotNull(expression) => {
192+
substitute_in_value_expression(expression, parameters)?;
193+
}
194+
}
195+
Ok(())
196+
}
197+
198+
fn substitute_in_value_expression(
199+
expr: &mut ValueExpression,
200+
parameters: &HashMap<String, serde_json::Value>,
201+
) -> Result<()> {
202+
match expr {
203+
ValueExpression::Parameter(name) => {
204+
let param_value =
205+
parameters
206+
.get(&name.to_lowercase())
207+
.ok_or_else(|| GraphError::PlanError {
208+
message: format!("Missing parameter: ${}", name),
209+
location: snafu::Location::new(file!(), line!(), column!()),
210+
})?;
211+
212+
// Check for array to VectorLiteral conversion
213+
if let serde_json::Value::Array(arr) = param_value {
214+
let mut floats = Vec::new();
215+
for v in arr {
216+
if let Some(f) = v.as_f64() {
217+
floats.push(f as f32);
218+
} else {
219+
return Err(GraphError::PlanError {
220+
message: format!(
221+
"Parameter ${} is a list but contains non-numeric values. Only float vectors are supported as list parameters currently.",
222+
name
223+
),
224+
location: snafu::Location::new(file!(), line!(), column!()),
225+
});
226+
}
227+
}
228+
*expr = ValueExpression::VectorLiteral(floats);
229+
return Ok(());
230+
}
231+
232+
// Scalar conversion
233+
let prop_val = json_to_property_value(param_value)?;
234+
*expr = ValueExpression::Literal(prop_val);
235+
}
236+
ValueExpression::ScalarFunction { args, .. }
237+
| ValueExpression::AggregateFunction { args, .. } => {
238+
for arg in args {
239+
substitute_in_value_expression(arg, parameters)?;
240+
}
241+
}
242+
ValueExpression::Arithmetic { left, right, .. } => {
243+
substitute_in_value_expression(left, parameters)?;
244+
substitute_in_value_expression(right, parameters)?;
245+
}
246+
ValueExpression::VectorDistance { left, right, .. }
247+
| ValueExpression::VectorSimilarity { left, right, .. } => {
248+
substitute_in_value_expression(left, parameters)?;
249+
substitute_in_value_expression(right, parameters)?;
250+
}
251+
_ => {}
252+
}
253+
Ok(())
254+
}
255+
256+
fn json_to_property_value(value: &serde_json::Value) -> Result<PropertyValue> {
257+
match value {
258+
serde_json::Value::Null => Ok(PropertyValue::Null),
259+
serde_json::Value::Bool(b) => Ok(PropertyValue::Boolean(*b)),
260+
serde_json::Value::Number(n) => {
261+
if let Some(i) = n.as_i64() {
262+
Ok(PropertyValue::Integer(i))
263+
} else if let Some(f) = n.as_f64() {
264+
Ok(PropertyValue::Float(f))
265+
} else {
266+
Err(GraphError::PlanError {
267+
message: format!("Number parameter could not be converted to i64 or f64: {}", n),
268+
location: snafu::Location::new(file!(), line!(), column!()),
269+
})
270+
}
271+
}
272+
serde_json::Value::String(s) => Ok(PropertyValue::String(s.clone())),
273+
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
274+
Err(GraphError::PlanError {
275+
message: "Complex types (List, Map) are not fully supported as parameters yet (except float vectors).".to_string(),
276+
location: snafu::Location::new(file!(), line!(), column!()),
277+
})
278+
}
279+
}
280+
}

0 commit comments

Comments
 (0)