@@ -23,6 +23,16 @@ pub enum LogicalOperator {
2323 properties : HashMap < String , PropertyValue > ,
2424 } ,
2525
26+ /// Unwind a list into a sequence of rows
27+ Unwind {
28+ /// The input operator
29+ input : Option < Box < LogicalOperator > > ,
30+ /// The expression to unwind (must yield a list)
31+ expression : ValueExpression ,
32+ /// The alias for the unwound element
33+ alias : String ,
34+ } ,
35+
2636 /// Apply a filter predicate (WHERE clause)
2737 Filter {
2838 input : Box < LogicalOperator > ,
@@ -153,8 +163,8 @@ impl LogicalPlanner {
153163
154164 /// Convert a Cypher AST to a logical plan
155165 pub fn plan ( & mut self , query : & CypherQuery ) -> Result < LogicalOperator > {
156- // Start with the MATCH clause(s)
157- let mut plan = self . plan_match_clauses ( & query. match_clauses ) ?;
166+ // Plan main MATCH clauses
167+ let mut plan = self . plan_reading_clauses ( None , & query. reading_clauses ) ?;
158168
159169 // Apply WHERE clause if present (before WITH)
160170 if let Some ( where_clause) = & query. where_clause {
@@ -169,9 +179,9 @@ impl LogicalPlanner {
169179 plan = self . plan_with_clause ( with_clause, plan) ?;
170180 }
171181
172- // Apply post-WITH MATCH clauses if present (query chaining)
173- for match_clause in & query. post_with_match_clauses {
174- plan = self . plan_match_clause_with_base ( Some ( plan) , match_clause ) ?;
182+ // Plan post-WITH MATCH clauses
183+ if ! query. post_with_reading_clauses . is_empty ( ) {
184+ plan = self . plan_reading_clauses ( Some ( plan) , & query . post_with_reading_clauses ) ?;
175185 }
176186
177187 // Apply post-WITH WHERE clause if present
@@ -219,25 +229,63 @@ impl LogicalPlanner {
219229 Ok ( plan)
220230 }
221231
222- /// Plan MATCH clauses - the core graph pattern matching
223- fn plan_match_clauses ( & mut self , match_clauses : & [ MatchClause ] ) -> Result < LogicalOperator > {
224- if match_clauses. is_empty ( ) {
232+ fn plan_reading_clauses (
233+ & mut self ,
234+ base_plan : Option < LogicalOperator > ,
235+ reading_clauses : & [ ReadingClause ] ,
236+ ) -> Result < LogicalOperator > {
237+ let mut plan = base_plan;
238+
239+ if reading_clauses. is_empty ( ) && plan. is_none ( ) {
225240 return Err ( GraphError :: PlanError {
226- message : "Query must have at least one MATCH clause" . to_string ( ) ,
241+ message : "Query must have at least one MATCH or UNWIND clause" . to_string ( ) ,
227242 location : snafu:: Location :: new ( file ! ( ) , line ! ( ) , column ! ( ) ) ,
228243 } ) ;
229244 }
230245
231- let plan = match_clauses . iter ( ) . try_fold ( None , |plan , clause| {
232- self . plan_match_clause_with_base ( plan, clause) . map ( Some )
233- } ) ? ;
246+ for clause in reading_clauses {
247+ plan = Some ( self . plan_reading_clause_with_base ( plan, clause) ? ) ;
248+ }
234249
235250 plan. ok_or_else ( || GraphError :: PlanError {
236- message : "Failed to plan MATCH clauses" . to_string ( ) ,
251+ message : "Failed to plan clauses" . to_string ( ) ,
237252 location : snafu:: Location :: new ( file ! ( ) , line ! ( ) , column ! ( ) ) ,
238253 } )
239254 }
240255
256+ /// Plan a single READING clause, optionally starting from an existing base plan
257+ fn plan_reading_clause_with_base (
258+ & mut self ,
259+ base : Option < LogicalOperator > ,
260+ clause : & ReadingClause ,
261+ ) -> Result < LogicalOperator > {
262+ match clause {
263+ ReadingClause :: Match ( match_clause) => {
264+ self . plan_match_clause_with_base ( base, match_clause)
265+ }
266+ ReadingClause :: Unwind ( unwind_clause) => {
267+ self . plan_unwind_clause_with_base ( base, unwind_clause)
268+ }
269+ }
270+ }
271+
272+ /// Plan an UNWIND clause
273+ fn plan_unwind_clause_with_base (
274+ & mut self ,
275+ base : Option < LogicalOperator > ,
276+ unwind_clause : & UnwindClause ,
277+ ) -> Result < LogicalOperator > {
278+ // Register the alias variable
279+ self . variables
280+ . insert ( unwind_clause. alias . clone ( ) , "Unwound" . to_string ( ) ) ;
281+
282+ Ok ( LogicalOperator :: Unwind {
283+ input : base. map ( Box :: new) ,
284+ expression : unwind_clause. expression . clone ( ) ,
285+ alias : unwind_clause. alias . clone ( ) ,
286+ } )
287+ }
288+
241289 /// Plan a single MATCH clause, optionally starting from an existing base plan
242290 fn plan_match_clause_with_base (
243291 & mut self ,
@@ -398,6 +446,7 @@ impl LogicalPlanner {
398446 fn extract_variable_from_plan ( & self , plan : & LogicalOperator ) -> Result < String > {
399447 match plan {
400448 LogicalOperator :: ScanByLabel { variable, .. } => Ok ( variable. clone ( ) ) ,
449+ LogicalOperator :: Unwind { alias, .. } => Ok ( alias. clone ( ) ) ,
401450 LogicalOperator :: Expand {
402451 target_variable, ..
403452 } => Ok ( target_variable. clone ( ) ) ,
0 commit comments