1+ use std:: collections:: HashMap ;
2+ use std:: fmt;
13use std:: sync:: Arc ;
24
35use crate :: {
@@ -7,13 +9,149 @@ use crate::{
79 analyzer_pass:: { AnalyzerPass , AnalyzerResult } ,
810 errors:: { AnalyzerError , Pass } ,
911 } ,
10- logical_expr:: Direction ,
11- logical_plan:: LogicalPlan ,
12+ logical_expr:: { Direction , Literal , LogicalExpr } ,
13+ logical_plan:: { LogicalPlan , ProjectionItem } ,
1214 plan_ctx:: PlanCtx ,
1315 transformed:: Transformed ,
1416 } ,
1517} ;
1618
19+ // ---------------------------------------------------------------------------
20+ // Array index type validation helpers
21+ // ---------------------------------------------------------------------------
22+
23+ /// Non-integer literal kinds that are illegal as array subscript indices.
24+ #[ derive( Debug ) ]
25+ enum NonIntegerKind {
26+ Boolean ,
27+ Float ,
28+ Str ,
29+ List ,
30+ Map ,
31+ }
32+
33+ impl fmt:: Display for NonIntegerKind {
34+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
35+ match self {
36+ NonIntegerKind :: Boolean => write ! ( f, "Boolean" ) ,
37+ NonIntegerKind :: Float => write ! ( f, "Float" ) ,
38+ NonIntegerKind :: Str => write ! ( f, "String" ) ,
39+ NonIntegerKind :: List => write ! ( f, "List" ) ,
40+ NonIntegerKind :: Map => write ! ( f, "Map" ) ,
41+ }
42+ }
43+ }
44+
45+ /// Returns `Some(kind)` when `expr` is a clearly non-integer literal; `None` otherwise.
46+ ///
47+ /// Note: `Literal::String` is intentionally excluded — bracket subscripts with string keys
48+ /// (`map['key']`, `node['prop']`) are valid Cypher map/property access and must not be
49+ /// rejected. String-keyed array subscripts (invalid) are caught at SQL execution time.
50+ fn infer_non_integer_kind ( expr : & LogicalExpr ) -> Option < NonIntegerKind > {
51+ match expr {
52+ LogicalExpr :: Literal ( lit) => match lit {
53+ Literal :: Boolean ( _) => Some ( NonIntegerKind :: Boolean ) ,
54+ Literal :: Float ( _) => Some ( NonIntegerKind :: Float ) ,
55+ Literal :: Integer ( _) | Literal :: String ( _) | Literal :: Null => None ,
56+ } ,
57+ LogicalExpr :: List ( _) => Some ( NonIntegerKind :: List ) ,
58+ LogicalExpr :: MapLiteral ( _) => Some ( NonIntegerKind :: Map ) ,
59+ _ => None ,
60+ }
61+ }
62+
63+ /// Build a map from alias name → non-integer kind using the projection items of a
64+ /// WITH clause. Only aliases whose expression is a known non-integer type are included.
65+ fn build_non_integer_alias_map ( items : & [ ProjectionItem ] ) -> HashMap < String , NonIntegerKind > {
66+ let mut map = HashMap :: new ( ) ;
67+ for item in items {
68+ if let Some ( col_alias) = & item. col_alias {
69+ if let Some ( kind) = infer_non_integer_kind ( & item. expression ) {
70+ map. insert ( col_alias. 0 . clone ( ) , kind) ;
71+ }
72+ }
73+ }
74+ map
75+ }
76+
77+ /// Walk `expr` recursively looking for an `ArraySubscript` whose index is a
78+ /// known non-integer type (either a direct literal or an alias bound to one).
79+ /// Returns `Some(error_message)` on the first violation found.
80+ ///
81+ /// Covers all expression variants that `walk_expression` traverses so that
82+ /// subscripts nested inside CASE branches, aggregate calls, reduce expressions,
83+ /// map literals, array slicings, etc. are also checked.
84+ fn find_invalid_array_subscript (
85+ expr : & LogicalExpr ,
86+ alias_types : & HashMap < String , NonIntegerKind > ,
87+ ) -> Option < String > {
88+ macro_rules! recurse {
89+ ( $e: expr) => {
90+ find_invalid_array_subscript( $e, alias_types)
91+ } ;
92+ }
93+ match expr {
94+ LogicalExpr :: ArraySubscript { array, index } => {
95+ // Direct non-integer literal index.
96+ if let Some ( kind) = infer_non_integer_kind ( index) {
97+ return Some ( format ! (
98+ "TypeError: invalid array index type {} — must be Integer" ,
99+ kind
100+ ) ) ;
101+ }
102+ // Alias-resolved non-integer index.
103+ let alias_name: Option < & str > = match index. as_ref ( ) {
104+ LogicalExpr :: ColumnAlias ( ca) => Some ( & ca. 0 ) ,
105+ LogicalExpr :: TableAlias ( ta) => Some ( & ta. 0 ) ,
106+ _ => None ,
107+ } ;
108+ if let Some ( name) = alias_name {
109+ if let Some ( kind) = alias_types. get ( name) {
110+ return Some ( format ! (
111+ "TypeError: invalid array index type {} (alias '{}') — must be Integer" ,
112+ kind, name
113+ ) ) ;
114+ }
115+ }
116+ recurse ! ( array) . or_else ( || recurse ! ( index) )
117+ }
118+ LogicalExpr :: OperatorApplicationExp ( op) | LogicalExpr :: Operator ( op) => {
119+ op. operands . iter ( ) . find_map ( |o| recurse ! ( o) )
120+ }
121+ LogicalExpr :: ScalarFnCall ( f) => f. args . iter ( ) . find_map ( |a| recurse ! ( a) ) ,
122+ LogicalExpr :: AggregateFnCall ( f) => f. args . iter ( ) . find_map ( |a| recurse ! ( a) ) ,
123+ LogicalExpr :: List ( items) => items. iter ( ) . find_map ( |i| recurse ! ( i) ) ,
124+ LogicalExpr :: MapLiteral ( entries) => entries. iter ( ) . find_map ( |( _, v) | recurse ! ( v) ) ,
125+ LogicalExpr :: Case ( c) => c
126+ . expr
127+ . as_ref ( )
128+ . and_then ( |e| recurse ! ( e) )
129+ . or_else ( || {
130+ c. when_then
131+ . iter ( )
132+ . find_map ( |( w, t) | recurse ! ( w) . or_else ( || recurse ! ( t) ) )
133+ } )
134+ . or_else ( || c. else_expr . as_ref ( ) . and_then ( |e| recurse ! ( e) ) ) ,
135+ LogicalExpr :: ReduceExpr ( r) => recurse ! ( & r. initial_value)
136+ . or_else ( || recurse ! ( & r. list) )
137+ . or_else ( || recurse ! ( & r. expression) ) ,
138+ LogicalExpr :: ArraySlicing { array, from, to } => recurse ! ( array)
139+ . or_else ( || from. as_ref ( ) . and_then ( |f| recurse ! ( f) ) )
140+ . or_else ( || to. as_ref ( ) . and_then ( |t| recurse ! ( t) ) ) ,
141+ _ => None ,
142+ }
143+ }
144+
145+ /// Check all expressions in a slice of projection items.
146+ fn check_items (
147+ items : & [ ProjectionItem ] ,
148+ alias_types : & HashMap < String , NonIntegerKind > ,
149+ ) -> Option < String > {
150+ items
151+ . iter ( )
152+ . find_map ( |item| find_invalid_array_subscript ( & item. expression , alias_types) )
153+ }
154+
17155pub struct QueryValidation ;
18156
19157impl AnalyzerPass for QueryValidation {
@@ -25,6 +163,23 @@ impl AnalyzerPass for QueryValidation {
25163 ) -> AnalyzerResult < Transformed < Arc < LogicalPlan > > > {
26164 let transformed_plan = match logical_plan. as_ref ( ) {
27165 LogicalPlan :: Projection ( projection) => {
166+ // Validate array subscript index types.
167+ // When the child is a WithClause, build a map of alias → non-integer kind
168+ // so we can detect `WITH true AS idx RETURN list[idx]` style errors.
169+ let alias_types = if let LogicalPlan :: WithClause ( wc) = projection. input . as_ref ( ) {
170+ build_non_integer_alias_map ( & wc. items )
171+ } else {
172+ HashMap :: new ( )
173+ } ;
174+
175+ for item in & projection. items {
176+ if let Some ( err_msg) =
177+ find_invalid_array_subscript ( & item. expression , & alias_types)
178+ {
179+ return Err ( AnalyzerError :: InvalidPlan ( err_msg) ) ;
180+ }
181+ }
182+
28183 let child_tf = self . analyze_with_graph_schema (
29184 projection. input . clone ( ) ,
30185 plan_ctx,
@@ -232,6 +387,11 @@ impl AnalyzerPass for QueryValidation {
232387 graph_joins. rebuild_or_clone ( child_tf, logical_plan. clone ( ) )
233388 }
234389 LogicalPlan :: Filter ( filter) => {
390+ // Validate subscript indices in WHERE predicates.
391+ let empty_map = HashMap :: new ( ) ;
392+ if let Some ( err) = find_invalid_array_subscript ( & filter. predicate , & empty_map) {
393+ return Err ( AnalyzerError :: InvalidPlan ( err) ) ;
394+ }
235395 let child_tf =
236396 self . analyze_with_graph_schema ( filter. input . clone ( ) , plan_ctx, graph_schema) ?;
237397 filter. rebuild_or_clone ( child_tf, logical_plan. clone ( ) )
@@ -311,6 +471,27 @@ impl AnalyzerPass for QueryValidation {
311471 }
312472 }
313473 LogicalPlan :: WithClause ( with_clause) => {
474+ // Validate subscript indices inside WITH projection expressions.
475+ let empty_map = HashMap :: new ( ) ;
476+ if let Some ( err) = check_items ( & with_clause. items , & empty_map) {
477+ return Err ( AnalyzerError :: InvalidPlan ( err) ) ;
478+ }
479+ // Build alias→type map so WHERE/ORDER BY can resolve alias references.
480+ let alias_types = build_non_integer_alias_map ( & with_clause. items ) ;
481+ if let Some ( ref wc) = with_clause. where_clause {
482+ if let Some ( err) = find_invalid_array_subscript ( wc, & alias_types) {
483+ return Err ( AnalyzerError :: InvalidPlan ( err) ) ;
484+ }
485+ }
486+ if let Some ( ref order_by) = with_clause. order_by {
487+ for item in order_by {
488+ if let Some ( err) =
489+ find_invalid_array_subscript ( & item. expression , & alias_types)
490+ {
491+ return Err ( AnalyzerError :: InvalidPlan ( err) ) ;
492+ }
493+ }
494+ }
314495 let child_tf = self . analyze_with_graph_schema (
315496 with_clause. input . clone ( ) ,
316497 plan_ctx,
0 commit comments