gqlite 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/db-index/Cargo.toml +2 -2
  4. data/ext/gqlitedb/Cargo.toml +0 -2
  5. data/ext/gqlitedb/src/aggregators.rs +28 -0
  6. data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
  7. data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
  8. data/ext/gqlitedb/src/compiler.rs +506 -48
  9. data/ext/gqlitedb/src/connection.rs +14 -2
  10. data/ext/gqlitedb/src/consts.rs +1 -1
  11. data/ext/gqlitedb/src/error.rs +31 -0
  12. data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
  13. data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
  14. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  15. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  16. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  17. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  18. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  19. data/ext/gqlitedb/src/planner.rs +231 -6
  20. data/ext/gqlitedb/src/query_result.rs +19 -4
  21. data/ext/gqlitedb/src/store/redb.rs +1 -1
  22. data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
  23. data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
  24. data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
  25. data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
  26. data/ext/gqlitedb/src/tests/connection.rs +101 -0
  27. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  28. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  29. data/ext/gqlitedb/src/tests/planner.rs +433 -2
  30. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  31. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  32. data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
  33. data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
  34. data/ext/gqliterb/Cargo.toml +0 -1
  35. data/ext/gqlparser/src/oc/ast.rs +72 -3
  36. data/ext/gqlparser/src/oc/error.rs +18 -0
  37. data/ext/gqlparser/src/oc/lexer.rs +157 -2
  38. data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
  39. data/ext/gqlparser/src/oc/parser.rs +1312 -182
  40. data/ext/gqlparser/src/oc/string.rs +172 -0
  41. data/ext/gqlparser/src/oc.rs +1 -0
  42. data/ext/graphcore/src/value/value_map.rs +2 -2
  43. data/ext/graphcore/src/value.rs +13 -1
  44. metadata +6 -2
  45. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -313,7 +313,7 @@ fn parameter_name(input: Input<'_>) -> OCPResult<Input<'_>, &str>
313
313
  }
314
314
  // Literals
315
315
 
316
- fn string_literal(input: Input<'_>) -> OCPResult<Input<'_>, &str>
316
+ fn string_literal(input: Input<'_>) -> OCPResult<Input<'_>, String>
317
317
  {
318
318
  match input.0
319
319
  {
@@ -327,7 +327,10 @@ fn string_literal(input: Input<'_>) -> OCPResult<Input<'_>, &str>
327
327
  {
328
328
  let lit = input.1.extract_text(span);
329
329
 
330
- Ok((Input(rest, input.1), &lit[1..lit.len() - 1]))
330
+ Ok((
331
+ Input(rest, input.1),
332
+ super::string::unescape_cypher(&lit[1..lit.len() - 1]),
333
+ ))
331
334
  }
332
335
  _ => Err(nom::Err::Error(Error::new(
333
336
  ErrorKind::ExpectedTokens(vec![Token::String].into()),
@@ -440,11 +443,21 @@ fn function_call(input: Input) -> OCPResult<Input, ast::Expression>
440
443
  to_string(ident),
441
444
  delimited(
442
445
  token(Token::StartParenthesis),
443
- separated_list0(token(Token::Comma), expression),
446
+ pair(
447
+ map(opt(token(Token::Distinct)), |d| d.is_some()),
448
+ separated_list0(token(Token::Comma), expression),
449
+ ),
444
450
  token(Token::EndParenthesis),
445
451
  ),
446
452
  ),
447
- |(name, arguments)| ast::FunctionCall { name, arguments }.into(),
453
+ |(name, (distinct, arguments))| {
454
+ ast::FunctionCall {
455
+ name,
456
+ arguments,
457
+ distinct,
458
+ }
459
+ .into()
460
+ },
448
461
  )
449
462
  .parse(input)
450
463
  }
@@ -483,9 +496,9 @@ fn primary(input: Input) -> OCPResult<Input, ast::Expression>
483
496
  .into()
484
497
  }),
485
498
  // String
486
- map(string_literal, |s: &str| {
499
+ map(string_literal, |s| {
487
500
  ast::Value {
488
- value: graphcore::Value::String(s.to_string()),
501
+ value: graphcore::Value::String(s),
489
502
  }
490
503
  .into()
491
504
  }),
@@ -522,7 +535,7 @@ fn member_access(input: Input) -> OCPResult<Input, ast::Expression>
522
535
  primary,
523
536
  many0(preceded(
524
537
  token(Token::Dot),
525
- to_string(alt((ident, string_literal))),
538
+ alt((to_string(ident), string_literal)),
526
539
  )),
527
540
  ),
528
541
  |(left, path)| {
@@ -617,6 +630,7 @@ fn label_check(input: Input) -> OCPResult<Input, ast::Expression>
617
630
  .into()
618
631
  }))
619
632
  .collect(),
633
+ distinct: false,
620
634
  }
621
635
  .into()
622
636
  }
@@ -625,6 +639,57 @@ fn label_check(input: Input) -> OCPResult<Input, ast::Expression>
625
639
  .parse(input)
626
640
  }
627
641
 
642
+ #[derive(Clone, Copy)]
643
+ enum StringOp
644
+ {
645
+ StartsWith,
646
+ EndsWith,
647
+ Contains,
648
+ }
649
+
650
+ fn string_operators(input: Input) -> OCPResult<Input, ast::Expression>
651
+ {
652
+ map(
653
+ (
654
+ label_check,
655
+ opt(
656
+ alt((
657
+ value(
658
+ StringOp::StartsWith,
659
+ (token(Token::Starts), token(Token::With)),
660
+ ),
661
+ value(StringOp::EndsWith, (token(Token::Ends), token(Token::With))),
662
+ value(StringOp::Contains, token(Token::Contains)),
663
+ ))
664
+ .and(label_check),
665
+ ),
666
+ ),
667
+ |(lhs, op)| match op
668
+ {
669
+ None => lhs,
670
+ Some(op) => match op.0
671
+ {
672
+ StringOp::StartsWith => ast::StartsWith {
673
+ left: lhs,
674
+ right: op.1,
675
+ }
676
+ .into(),
677
+ StringOp::EndsWith => ast::EndsWith {
678
+ left: lhs,
679
+ right: op.1,
680
+ }
681
+ .into(),
682
+ StringOp::Contains => ast::Contains {
683
+ left: lhs,
684
+ right: op.1,
685
+ }
686
+ .into(),
687
+ },
688
+ },
689
+ )
690
+ .parse(input)
691
+ }
692
+
628
693
  fn unary(input: Input) -> OCPResult<Input, ast::Expression>
629
694
  {
630
695
  alt((
@@ -671,9 +736,6 @@ fn unary(input: Input) -> OCPResult<Input, ast::Expression>
671
736
  )
672
737
  },
673
738
  ),
674
- map(preceded(token(Token::Not), unary), |e| {
675
- ast::LogicalNegation { value: e }.into()
676
- }),
677
739
  map(preceded(token(Token::Minus), unary), |e| match e
678
740
  {
679
741
  ast::Expression::Value(ast::Value {
@@ -691,7 +753,7 @@ fn unary(input: Input) -> OCPResult<Input, ast::Expression>
691
753
  _ => ast::Negation { value: e }.into(),
692
754
  }),
693
755
  preceded(token(Token::Plus), unary),
694
- label_check,
756
+ string_operators,
695
757
  ))
696
758
  .parse(input)
697
759
  }
@@ -811,17 +873,42 @@ fn relational(input: Input) -> OCPResult<Input, ast::Expression>
811
873
  }
812
874
  else
813
875
  {
876
+ // Check for NOT IN pattern
877
+ if let Ok((after_not, _)) = token(Token::Not).parse(input.clone())
878
+ && let Ok((after_in, _)) = token(Token::In).parse(after_not)
879
+ {
880
+ let (final_input, right) = is_not_null(after_in)?;
881
+ let in_expr = ast::RelationalIn { left, right }.into();
882
+ let negated = ast::LogicalNegation { value: in_expr }.into();
883
+ return Ok((final_input, negated));
884
+ }
814
885
  Ok((input, left))
815
886
  }
816
887
  }
817
888
 
889
+ fn not_expression(input: Input) -> OCPResult<Input, ast::Expression>
890
+ {
891
+ map(
892
+ pair(many0(token(Token::Not)), relational),
893
+ |(nots, expr)| {
894
+ nots
895
+ .into_iter()
896
+ .fold(expr, |e, _| ast::LogicalNegation { value: e }.into())
897
+ },
898
+ )
899
+ .parse(input)
900
+ }
901
+
818
902
  fn logical_and(input: Input) -> OCPResult<Input, ast::Expression>
819
903
  {
820
- map(separated_list1(token(Token::And), relational), |mut v| {
821
- let first = v.remove(0);
822
- v.into_iter()
823
- .fold(first, |l, r| ast::LogicalAnd { left: l, right: r }.into())
824
- })
904
+ map(
905
+ separated_list1(token(Token::And), not_expression),
906
+ |mut v| {
907
+ let first = v.remove(0);
908
+ v.into_iter()
909
+ .fold(first, |l, r| ast::LogicalAnd { left: l, right: r }.into())
910
+ },
911
+ )
825
912
  .parse(input)
826
913
  }
827
914
 
@@ -907,7 +994,7 @@ fn labels(input: Input) -> OCPResult<Input, ast::LabelExpression>
907
994
  fn map_pair(input: Input) -> OCPResult<Input, (String, ast::Expression)>
908
995
  {
909
996
  separated_pair(
910
- to_string(alt((ident, string_literal))),
997
+ alt((to_string(ident), string_literal)),
911
998
  token(Token::Colon),
912
999
  expression,
913
1000
  )
@@ -948,11 +1035,11 @@ fn array_literal(input: Input) -> OCPResult<Input, ast::Expression>
948
1035
  .parse(input)
949
1036
  }
950
1037
 
951
- fn node_pattern(input: Input) -> OCPResult<Input, ast::NodePattern>
1038
+ fn node_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, ast::NodePattern>
952
1039
  {
953
- delimited(
954
- token(Token::StartParenthesis),
955
- map(
1040
+ map_res_failure(
1041
+ delimited(
1042
+ token(Token::StartParenthesis),
956
1043
  (
957
1044
  // Optional variable
958
1045
  opt(ident),
@@ -960,19 +1047,28 @@ fn node_pattern(input: Input) -> OCPResult<Input, ast::NodePattern>
960
1047
  opt(labels),
961
1048
  // Optional properties map or parameter
962
1049
  opt(properties),
1050
+ // Optional inline predicate: WHERE <expr>
1051
+ opt(preceded(token(Token::Where), expression)),
963
1052
  ),
964
- |(ident, labels, properties)| ast::NodePattern {
1053
+ token(Token::EndParenthesis),
1054
+ ),
1055
+ |(ident, labels, properties, predicate)| {
1056
+ if predicate.is_some() && !T::ALLOW_PREDICATE
1057
+ {
1058
+ return Err(ErrorKind::CreatingWithPredicate);
1059
+ }
1060
+ Ok(ast::NodePattern {
965
1061
  variable: input
966
1062
  .1
967
1063
  .identifiers
968
1064
  .create_variable_from_name_optional(ident),
969
1065
  labels: labels.unwrap_or(ast::LabelExpression::None),
970
1066
  properties,
971
- },
972
- ),
973
- token(Token::EndParenthesis),
1067
+ predicate,
1068
+ })
1069
+ },
974
1070
  )
975
- .parse(input)
1071
+ .parse(input.clone())
976
1072
  }
977
1073
 
978
1074
  enum EdgeDirection
@@ -986,6 +1082,7 @@ trait EdgeMode
986
1082
  {
987
1083
  const ALLOW_UNDIRECTED: bool;
988
1084
  const ALLOW_LABEL_ALTERNATIVE: bool;
1085
+ const ALLOW_PREDICATE: bool;
989
1086
  }
990
1087
 
991
1088
  struct AllowUndirected;
@@ -993,6 +1090,7 @@ impl EdgeMode for AllowUndirected
993
1090
  {
994
1091
  const ALLOW_UNDIRECTED: bool = true;
995
1092
  const ALLOW_LABEL_ALTERNATIVE: bool = true;
1093
+ const ALLOW_PREDICATE: bool = true;
996
1094
  }
997
1095
 
998
1096
  struct DisallowUndirected;
@@ -1000,6 +1098,7 @@ impl EdgeMode for DisallowUndirected
1000
1098
  {
1001
1099
  const ALLOW_UNDIRECTED: bool = false;
1002
1100
  const ALLOW_LABEL_ALTERNATIVE: bool = false;
1101
+ const ALLOW_PREDICATE: bool = false;
1003
1102
  }
1004
1103
 
1005
1104
  struct EdgePattern
@@ -1008,7 +1107,8 @@ struct EdgePattern
1008
1107
  edge_variable: Option<ast::VariableIdentifier>,
1009
1108
  labels: ast::LabelExpression,
1010
1109
  properties: Option<ast::Expression>,
1011
- recursive_range: Option<std::ops::Range<usize>>,
1110
+ predicate: Option<ast::Expression>,
1111
+ quantifier: Option<std::ops::Range<usize>>,
1012
1112
  destination_node: ast::NodePattern,
1013
1113
  }
1014
1114
 
@@ -1039,6 +1139,78 @@ fn edge_range(input: Input) -> OCPResult<Input, std::ops::Range<usize>>
1039
1139
  .parse(input)
1040
1140
  }
1041
1141
 
1142
+ fn quantifier_suffix(input: Input) -> OCPResult<Input, std::ops::Range<usize>>
1143
+ {
1144
+ alt((
1145
+ delimited(
1146
+ token(Token::StartBrace),
1147
+ map(
1148
+ (opt(int), opt(preceded(token(Token::Comma), opt(int)))),
1149
+ |(min, rest)| match (min, rest)
1150
+ {
1151
+ // {n} - exact count
1152
+ (Some(n), None) => (n as usize)..(n as usize),
1153
+ // {min,max}
1154
+ (Some(min), Some(Some(max))) => (min as usize)..(max as usize),
1155
+ // {min,}
1156
+ (Some(min), Some(None)) => (min as usize)..usize::MAX,
1157
+ // {,max}
1158
+ (None, Some(Some(max))) => 0..(max as usize),
1159
+ // {,} or {} - both omitted; treat as equivalent to bare `*`.
1160
+ // Flagging rather than silently deciding: `{}`/`{,}` aren't
1161
+ // shown in the source docs at all, so this is an assumption,
1162
+ // not a confirmed spec reading.
1163
+ (None, Some(None)) | (None, None) => 0..usize::MAX,
1164
+ },
1165
+ ),
1166
+ token(Token::EndBrace),
1167
+ ),
1168
+ // Bare `*`: zero-or-more, spec-conformant (NOT the legacy `[*]`
1169
+ // default of 1.. — that default is preserved unchanged for the
1170
+ // legacy in-bracket syntax specifically; see WI4).
1171
+ map(token(Token::Star), |_| 0..usize::MAX),
1172
+ // Bare `+`: one-or-more.
1173
+ map(token(Token::Plus), |_| 1..usize::MAX),
1174
+ ))
1175
+ .parse(input)
1176
+ }
1177
+
1178
+ fn path_selector(input: Input) -> OCPResult<Input, ast::PathSelector>
1179
+ {
1180
+ alt((
1181
+ // ALL SHORTEST
1182
+ map(pair(token(Token::All), token(Token::Shortest)), |_| {
1183
+ ast::PathSelector::AllShortest
1184
+ }),
1185
+ // ALL (bare, no SHORTEST following)
1186
+ // NOTE: Must be tried after ALL SHORTEST, since ALL is consumed by both.
1187
+ // The pair combinator will try ALL SHORTEST first, and only if SHORTEST
1188
+ // doesn't follow will this branch be reached.
1189
+ map(token(Token::All), |_| ast::PathSelector::All),
1190
+ // SHORTEST <k> GROUPS
1191
+ // NOTE: Must be tried before bare SHORTEST <k>, since expression alone
1192
+ // would greedily consume <k> and leave GROUPS unconsumed, falling through
1193
+ // to a confusing trailing-token error.
1194
+ map(
1195
+ (token(Token::Shortest), expression, token(Token::Groups)),
1196
+ |(_, k, _)| ast::PathSelector::ShortestGroups(k),
1197
+ ),
1198
+ // SHORTEST GROUP (singular sugar, k defaults to 1)
1199
+ map(pair(token(Token::Shortest), token(Token::Group)), |_| {
1200
+ ast::PathSelector::ShortestGroups(ast::Expression::Value(ast::Value { value: 1.into() }))
1201
+ }),
1202
+ // SHORTEST <k>
1203
+ map(pair(token(Token::Shortest), expression), |(_, k)| {
1204
+ ast::PathSelector::Shortest(k)
1205
+ }),
1206
+ // ANY SHORTEST | ANY
1207
+ map(pair(token(Token::Any), opt(token(Token::Shortest))), |_| {
1208
+ ast::PathSelector::Any
1209
+ }),
1210
+ ))
1211
+ .parse(input)
1212
+ }
1213
+
1042
1214
  fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1043
1215
  {
1044
1216
  map_res_failure(
@@ -1058,6 +1230,8 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1058
1230
  opt(edge_range),
1059
1231
  // Optional properties map or parameter
1060
1232
  opt(properties),
1233
+ // Optional inline predicate: WHERE <expr>
1234
+ opt(preceded(token(Token::Where), expression)),
1061
1235
  token(Token::EndBracket),
1062
1236
  )),
1063
1237
  alt((
@@ -1065,22 +1239,33 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1065
1239
  map(token(Token::Minus), |_| EdgeDirection::Undirected),
1066
1240
  map(token(Token::LeftArrow), |_| EdgeDirection::Left),
1067
1241
  )),
1068
- node_pattern, // Right node
1242
+ opt(quantifier_suffix),
1243
+ node_pattern::<T>, // Right node
1069
1244
  ),
1070
- |(first_direction, edge_pattern, second_direction, destination_node)| {
1071
- let (edge_variable, recursive_range, labels, properties) = match edge_pattern
1245
+ |(first_direction, edge_pattern, second_direction, new_quantifier, destination_node)| {
1246
+ let (edge_variable, recursive_range, labels, properties, predicate) = match edge_pattern
1072
1247
  {
1073
- Some((_, var, labels, recursive_range, properties, _)) =>
1248
+ Some((_, var, labels, recursive_range, properties, predicate, _)) =>
1074
1249
  {
1075
- (var, recursive_range, labels, properties)
1250
+ (var, recursive_range, labels, properties, predicate)
1076
1251
  }
1077
- None => (None, None, None, None),
1252
+ None => (None, None, None, None, None),
1078
1253
  };
1079
1254
  let labels = labels.unwrap_or(ast::LabelExpression::None);
1080
1255
  if !T::ALLOW_LABEL_ALTERNATIVE && !labels.is_all_inclusive()
1081
1256
  {
1082
1257
  return Err(ErrorKind::NoSingleRelationshipType);
1083
1258
  }
1259
+ if predicate.is_some() && !T::ALLOW_PREDICATE
1260
+ {
1261
+ return Err(ErrorKind::CreatingWithPredicate);
1262
+ }
1263
+ let quantifier = match (recursive_range, new_quantifier)
1264
+ {
1265
+ (Some(_), Some(_)) => return Err(ErrorKind::ConflictingQuantifiers),
1266
+ (Some(r), None) | (None, Some(r)) => Some(r),
1267
+ (None, None) => None,
1268
+ };
1084
1269
  let edge_direction = match (first_direction, second_direction)
1085
1270
  {
1086
1271
  (EdgeDirection::Right, EdgeDirection::Right)
@@ -1111,186 +1296,844 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1111
1296
  edge_variable,
1112
1297
  labels,
1113
1298
  properties,
1299
+ predicate,
1114
1300
  destination_node,
1115
- recursive_range,
1301
+ quantifier,
1116
1302
  })
1117
1303
  },
1118
1304
  )
1119
1305
  .parse(input.clone())
1120
1306
  }
1121
1307
 
1308
+ struct GroupPattern
1309
+ {
1310
+ group: Vec<ast::Pattern>,
1311
+ group_where: Option<ast::Expression>,
1312
+ quantifier: std::ops::Range<usize>,
1313
+ destination_node: ast::NodePattern,
1314
+ }
1315
+
1316
+ fn group_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, GroupPattern>
1317
+ {
1318
+ map_res_failure(
1319
+ (
1320
+ delimited(
1321
+ token(Token::StartParenthesis),
1322
+ (
1323
+ node_or_edges_patterns::<T>,
1324
+ opt(preceded(token(Token::Where), expression)),
1325
+ ),
1326
+ token(Token::EndParenthesis),
1327
+ ),
1328
+ quantifier_suffix,
1329
+ node_pattern::<T>,
1330
+ ),
1331
+ |((group, group_where), quantifier, destination_node)| {
1332
+ // Check that the group has at least one edge
1333
+ if !group.iter().any(|p| matches!(p, ast::Pattern::Edge(_)))
1334
+ {
1335
+ return Err(ErrorKind::EmptyQuantifiedGroup);
1336
+ }
1337
+ Ok(GroupPattern {
1338
+ group,
1339
+ group_where,
1340
+ quantifier,
1341
+ destination_node,
1342
+ })
1343
+ },
1344
+ )
1345
+ .parse(input)
1346
+ }
1347
+
1348
+ enum ChainElement
1349
+ {
1350
+ Edge(EdgePattern),
1351
+ Group(GroupPattern),
1352
+ }
1353
+
1122
1354
  fn node_or_edges_patterns<T: EdgeMode>(input: Input) -> OCPResult<Input, Vec<ast::Pattern>>
1123
1355
  {
1124
- let (input, node) = node_pattern(input)?;
1356
+ let (input, node) = node_pattern::<T>(input)?;
1125
1357
 
1126
- let (input, edges) = many0(edge_pattern::<T>).parse(input)?;
1358
+ let (input, elements) = many0(alt((
1359
+ map(edge_pattern::<T>, ChainElement::Edge),
1360
+ map(group_pattern::<T>, ChainElement::Group),
1361
+ )))
1362
+ .parse(input)?;
1127
1363
 
1128
- if edges.is_empty()
1364
+ if elements.is_empty()
1129
1365
  {
1130
1366
  Ok((input, vec![ast::Pattern::Node(node)]))
1131
1367
  }
1132
1368
  else
1133
1369
  {
1134
1370
  let mut current_node = node;
1135
- let mut iterator = edges.into_iter().peekable();
1371
+ let mut iterator = elements.into_iter().peekable();
1136
1372
  let mut patterns = Vec::new();
1137
- while let Some(edge) = iterator.next()
1373
+ while let Some(element) = iterator.next()
1138
1374
  {
1139
- let EdgePattern {
1140
- edge_direction,
1141
- edge_variable,
1142
- labels,
1143
- properties,
1144
- mut destination_node,
1145
- recursive_range,
1146
- } = edge;
1147
- if destination_node.variable.is_none() && iterator.peek().is_some()
1375
+ match element
1148
1376
  {
1149
- destination_node.variable = Some(input.1.identifiers.create_anonymous_variable());
1377
+ ChainElement::Edge(edge) =>
1378
+ {
1379
+ let EdgePattern {
1380
+ edge_direction,
1381
+ edge_variable,
1382
+ labels,
1383
+ properties,
1384
+ predicate,
1385
+ mut destination_node,
1386
+ quantifier,
1387
+ } = edge;
1388
+ if destination_node.variable.is_none() && iterator.peek().is_some()
1389
+ {
1390
+ destination_node.variable = Some(input.1.identifiers.create_anonymous_variable());
1391
+ }
1392
+ let source = current_node;
1393
+ current_node = destination_node.clone();
1394
+ let (source, directivity, destination) = match edge_direction
1395
+ {
1396
+ EdgeDirection::Right => (
1397
+ source,
1398
+ graphcore::EdgeDirectivity::Directed,
1399
+ destination_node,
1400
+ ),
1401
+ EdgeDirection::Left => (
1402
+ destination_node,
1403
+ graphcore::EdgeDirectivity::Directed,
1404
+ source,
1405
+ ),
1406
+ EdgeDirection::Undirected => (
1407
+ source,
1408
+ graphcore::EdgeDirectivity::Undirected,
1409
+ destination_node,
1410
+ ),
1411
+ };
1412
+ // Clone source and destination before they're consumed by edge_pattern
1413
+ let qp_source = source.clone();
1414
+ let qp_destination = destination.clone();
1415
+ let edge_variable = if quantifier.is_some() && edge_variable.is_none()
1416
+ {
1417
+ // A quantified edge's own value needs to be trackable even when the
1418
+ // user didn't name it, so path_variable construction (which walks
1419
+ // group_variables looking for Value::Edge entries) has something to
1420
+ // find. Without this, an unnamed quantified edge is never bound to
1421
+ // any column, and `p` can never be assembled.
1422
+ Some(input.1.identifiers.create_anonymous_variable())
1423
+ }
1424
+ else
1425
+ {
1426
+ edge_variable
1427
+ };
1428
+ let edge_pattern = ast::EdgePattern {
1429
+ source,
1430
+ variable: edge_variable,
1431
+ directivity,
1432
+ labels,
1433
+ properties,
1434
+ destination: destination.clone(),
1435
+ predicate,
1436
+ };
1437
+ match quantifier
1438
+ {
1439
+ Some(range) =>
1440
+ {
1441
+ patterns.push(ast::Pattern::QuantifiedPath(ast::QuantifiedPathPattern {
1442
+ variable: None,
1443
+ source: qp_source,
1444
+ group: vec![ast::Pattern::Edge(edge_pattern)],
1445
+ group_where: None,
1446
+ path_where: None,
1447
+ quantifier: Some(range),
1448
+ destination: qp_destination,
1449
+ selector: ast::PathSelector::All,
1450
+ }))
1451
+ }
1452
+ None => patterns.push(ast::Pattern::Edge(edge_pattern)),
1453
+ }
1454
+ }
1455
+ ChainElement::Group(group) =>
1456
+ {
1457
+ let GroupPattern {
1458
+ group: mut inner_group,
1459
+ group_where,
1460
+ quantifier,
1461
+ mut destination_node,
1462
+ } = group;
1463
+
1464
+ // Anonymize unnamed edges in the group to make them trackable for path assembly
1465
+ // This applies the same fix as the bare-quantifier case: even if the user doesn't
1466
+ // name an edge, it needs to be bound to a column so path_variable can find it
1467
+ for pattern in inner_group.iter_mut()
1468
+ {
1469
+ if let ast::Pattern::Edge(edge) = pattern
1470
+ && edge.variable.is_none()
1471
+ {
1472
+ edge.variable = Some(input.1.identifiers.create_anonymous_variable());
1473
+ }
1474
+ }
1475
+
1476
+ // Ensure the group's terminal node (the destination of the last edge) has a variable
1477
+ // This is needed for compile_quantified_path to correctly identify which column
1478
+ // represents the group's exit node
1479
+ for pattern in inner_group.iter_mut().rev()
1480
+ {
1481
+ if let ast::Pattern::Edge(edge) = pattern
1482
+ {
1483
+ if edge.destination.variable.is_none()
1484
+ {
1485
+ edge.destination.variable = Some(input.1.identifiers.create_anonymous_variable());
1486
+ }
1487
+ break;
1488
+ }
1489
+ }
1490
+
1491
+ // Ensure the group's entry node (the source of the first edge) has a
1492
+ // variable, symmetric to the terminal-node anonymization above.
1493
+ for pattern in inner_group.iter_mut()
1494
+ {
1495
+ if let ast::Pattern::Edge(edge) = pattern
1496
+ {
1497
+ if edge.source.variable.is_none()
1498
+ {
1499
+ edge.source.variable = Some(input.1.identifiers.create_anonymous_variable());
1500
+ }
1501
+ break;
1502
+ }
1503
+ }
1504
+
1505
+ if destination_node.variable.is_none() && iterator.peek().is_some()
1506
+ {
1507
+ destination_node.variable = Some(input.1.identifiers.create_anonymous_variable());
1508
+ }
1509
+ let source = current_node;
1510
+ current_node = destination_node.clone();
1511
+ patterns.push(ast::Pattern::QuantifiedPath(ast::QuantifiedPathPattern {
1512
+ variable: None,
1513
+ source,
1514
+ group: inner_group,
1515
+ group_where,
1516
+ path_where: None,
1517
+ quantifier: Some(quantifier),
1518
+ destination: destination_node,
1519
+ selector: ast::PathSelector::All,
1520
+ }));
1521
+ }
1150
1522
  }
1151
- let source = current_node;
1152
- current_node = destination_node.clone();
1153
- let (source, directivity, destination) = match edge_direction
1154
- {
1155
- EdgeDirection::Right => (
1156
- source,
1157
- graphcore::EdgeDirectivity::Directed,
1158
- destination_node,
1159
- ),
1160
- EdgeDirection::Left => (
1161
- destination_node,
1162
- graphcore::EdgeDirectivity::Directed,
1163
- source,
1164
- ),
1165
- EdgeDirection::Undirected => (
1166
- source,
1167
- graphcore::EdgeDirectivity::Undirected,
1168
- destination_node,
1169
- ),
1170
- };
1171
- patterns.push(ast::Pattern::Edge(ast::EdgePattern {
1172
- source,
1173
- variable: edge_variable,
1174
- directivity,
1175
- labels,
1176
- properties,
1177
- destination,
1178
- recursive_range,
1179
- }));
1180
1523
  }
1181
1524
 
1182
1525
  Ok((input, patterns))
1183
1526
  }
1184
1527
  }
1185
1528
 
1186
- fn patterns<T: EdgeMode>(input: Input) -> OCPResult<Input, Vec<ast::Pattern>>
1529
+ /// Helper function to combine patterns with an optional path variable binding.
1530
+ /// Used by both `patterns<T>` (WI3) and `selector_scoped_pattern<T>` (WI4).
1531
+ ///
1532
+ /// Returns a single pattern (length-1 Vec) that represents the combined result,
1533
+ /// ready for selector application or direct use.
1534
+ fn combine_patterns_with_variable(
1535
+ context: &Context,
1536
+ variable_name: Option<&str>,
1537
+ patterns: Vec<ast::Pattern>,
1538
+ ) -> Result<Vec<ast::Pattern>, ErrorKind>
1187
1539
  {
1188
- let context = &input.1;
1189
- alt((
1190
- map_res(
1191
- (ident, token(Token::Equal), node_or_edges_patterns::<T>),
1192
- |(name, _, patterns)| {
1193
- // Accept path assignment for one or more consecutive edges by combining them into a single EdgePattern.
1194
- if patterns.is_empty()
1540
+ // If there's no variable binding, just pass through the patterns as-is
1541
+ if variable_name.is_none()
1542
+ {
1543
+ // No path variable binding; patterns are used as-is
1544
+ return Ok(patterns);
1545
+ }
1546
+
1547
+ let name = variable_name.unwrap();
1548
+
1549
+ // Accept path assignment for one or more consecutive edges by combining them into a single EdgePattern.
1550
+ if patterns.is_empty()
1551
+ {
1552
+ return Err(ErrorKind::PathAcceptSingleEdge);
1553
+ }
1554
+ // A path variable over a single quantified segment: `p = (a)-[*1..3]->(b)`
1555
+ // or its new-syntax/multi-element equivalents. Bind the path variable
1556
+ // directly onto the QuantifiedPathPattern; PathPattern is not involved.
1557
+ if let [ast::Pattern::QuantifiedPath(qp)] = patterns.as_slice()
1558
+ {
1559
+ let mut qp = qp.clone();
1560
+ qp.variable = Some(context.identifiers.create_variable_from_name(name));
1561
+ return Ok(vec![ast::Pattern::QuantifiedPath(qp)]);
1562
+ }
1563
+ // A path variable over one or more *un*quantified edges: today's
1564
+ // existing combining behavior, but now handling QuantifiedPath patterns.
1565
+ // Extract all edges and track if any have quantifiers.
1566
+ struct EdgeWithQuantifier
1567
+ {
1568
+ edge: ast::EdgePattern,
1569
+ quantifier: Option<std::ops::Range<usize>>,
1570
+ }
1571
+ let mut edges_with_quantifiers: Vec<EdgeWithQuantifier> = Vec::new();
1572
+ for p in patterns
1573
+ {
1574
+ match p
1575
+ {
1576
+ ast::Pattern::Edge(e) =>
1577
+ {
1578
+ edges_with_quantifiers.push(EdgeWithQuantifier {
1579
+ edge: e,
1580
+ quantifier: None,
1581
+ });
1582
+ }
1583
+ ast::Pattern::QuantifiedPath(qp) =>
1584
+ {
1585
+ if qp.group.len() != 1
1195
1586
  {
1196
1587
  return Err(ErrorKind::PathAcceptSingleEdge);
1197
1588
  }
1198
- // Ensure all inner patterns are edges
1199
- if !patterns.iter().all(|p| matches!(p, ast::Pattern::Edge(_)))
1589
+ let ast::Pattern::Edge(e) = &qp.group[0]
1590
+ else
1200
1591
  {
1201
1592
  return Err(ErrorKind::PathAcceptSingleEdge);
1593
+ };
1594
+ edges_with_quantifiers.push(EdgeWithQuantifier {
1595
+ edge: e.clone(),
1596
+ quantifier: qp.quantifier.clone(),
1597
+ });
1598
+ }
1599
+ _ => return Err(ErrorKind::PathAcceptSingleEdge),
1600
+ }
1601
+ }
1602
+ if edges_with_quantifiers.is_empty()
1603
+ {
1604
+ return Err(ErrorKind::PathAcceptSingleEdge);
1605
+ }
1606
+
1607
+ // Check if any edge has a quantifier
1608
+ let has_quantifier = edges_with_quantifiers
1609
+ .iter()
1610
+ .any(|ewq| ewq.quantifier.is_some());
1611
+
1612
+ // Combine edges into a single EdgePattern by taking the first source and last destination.
1613
+ // Preserve an inner edge variable if exactly one inner edge is named.
1614
+ let combined_variable = {
1615
+ let mut found: Option<ast::VariableIdentifier> = None;
1616
+ for ewq in edges_with_quantifiers.iter()
1617
+ {
1618
+ if let Some(var) = &ewq.edge.variable
1619
+ {
1620
+ if found.is_some()
1621
+ {
1622
+ // multiple inner variables; do not attempt to preserve a single one
1623
+ found = None;
1624
+ break;
1202
1625
  }
1203
- // Consume the patterns and extract their EdgePattern values
1204
- let mut edges: Vec<ast::EdgePattern> = patterns
1205
- .into_iter()
1206
- .map(|p| match p
1626
+ else
1627
+ {
1628
+ found = Some(var.clone());
1629
+ }
1630
+ }
1631
+ }
1632
+ found
1633
+ };
1634
+ let original_count = edges_with_quantifiers.len();
1635
+ let first_ewq = edges_with_quantifiers.remove(0);
1636
+ let last_ewq = edges_with_quantifiers
1637
+ .pop()
1638
+ .unwrap_or_else(|| EdgeWithQuantifier {
1639
+ edge: first_ewq.edge.clone(),
1640
+ quantifier: first_ewq.quantifier.clone(),
1641
+ });
1642
+
1643
+ // For a single-edge path, preserve that edge's labels, properties, and predicate.
1644
+ // For multi-hop paths, all edges become fixed edges in the path representation.
1645
+ // Use original_count (before removes) to correctly distinguish single-edge (N=1) from two-edge (N=2) paths.
1646
+ let (combined_labels, combined_properties, combined_predicate) = if original_count == 1
1647
+ {
1648
+ // Single edge: preserve its constraints.
1649
+ (
1650
+ first_ewq.edge.labels.clone(),
1651
+ first_ewq.edge.properties.clone(),
1652
+ first_ewq.edge.predicate.clone(),
1653
+ )
1654
+ }
1655
+ else
1656
+ {
1657
+ // Multi-hop: all edges become fixed edges in the path representation
1658
+ (ast::LabelExpression::None, None, None)
1659
+ };
1660
+ // Clone source and destination before they're moved into combined_edge
1661
+ let qp_source = first_ewq.edge.source.clone();
1662
+ let qp_destination = last_ewq.edge.destination.clone();
1663
+ let combined_edge = ast::EdgePattern {
1664
+ variable: combined_variable,
1665
+ source: first_ewq.edge.source,
1666
+ destination: last_ewq.edge.destination,
1667
+ directivity: first_ewq.edge.directivity,
1668
+ labels: combined_labels,
1669
+ properties: combined_properties,
1670
+ predicate: combined_predicate,
1671
+ };
1672
+
1673
+ // If any edge had a quantifier, wrap the combined result in QuantifiedPath.
1674
+ // If all edges were unquantified, wrap in PathPattern (original behavior).
1675
+ if has_quantifier
1676
+ {
1677
+ // For mixed chains with quantifiers, wrap in QuantifiedPath.
1678
+ // Use 0..usize::MAX as the quantifier range for now (represents [*] in legacy syntax).
1679
+ // This preserves the semantics of "at least one path must exist" while allowing
1680
+ // variable length paths to be represented.
1681
+ let quantifier_range = if let Some(var_range) = &last_ewq.quantifier
1682
+ {
1683
+ // If the last edge has a quantifier, use it (with adjustment for preceding fixed edges)
1684
+ if original_count == 1
1685
+ {
1686
+ last_ewq.quantifier.clone()
1687
+ }
1688
+ else
1689
+ {
1690
+ // For multi-hop paths, each fixed edge = 1 hop, then add the variable range
1691
+ Some(
1692
+ (var_range.start + (original_count - 1))
1693
+ ..var_range.end.saturating_add(original_count - 1),
1694
+ )
1695
+ }
1696
+ }
1697
+ else
1698
+ {
1699
+ // No quantifier found (shouldn't happen if has_quantifier is true, but defensive)
1700
+ Some(0..usize::MAX)
1701
+ };
1702
+
1703
+ Ok(vec![ast::Pattern::QuantifiedPath(
1704
+ ast::QuantifiedPathPattern {
1705
+ variable: Some(context.identifiers.create_variable_from_name(name)),
1706
+ source: qp_source,
1707
+ group: vec![ast::Pattern::Edge(combined_edge)],
1708
+ group_where: None,
1709
+ path_where: None,
1710
+ quantifier: quantifier_range,
1711
+ destination: qp_destination,
1712
+ selector: ast::PathSelector::All,
1713
+ },
1714
+ )])
1715
+ }
1716
+ else
1717
+ {
1718
+ Ok(vec![
1719
+ ast::PathPattern {
1720
+ variable: context.identifiers.create_variable_from_name(name),
1721
+ edge: combined_edge,
1722
+ }
1723
+ .into(),
1724
+ ])
1725
+ }
1726
+ }
1727
+
1728
+ fn selector_scoped_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, Vec<ast::Pattern>>
1729
+ {
1730
+ let context = &input.1;
1731
+ map_res(
1732
+ delimited(
1733
+ token(Token::StartParenthesis),
1734
+ (
1735
+ opt((ident, token(Token::Equal))),
1736
+ node_or_edges_patterns::<T>,
1737
+ opt(preceded(token(Token::Where), expression)),
1738
+ ),
1739
+ token(Token::EndParenthesis),
1740
+ ),
1741
+ |(variable_name_and_equal, inner_patterns, group_where)| {
1742
+ let variable_name = variable_name_and_equal.map(|(name, _)| name);
1743
+
1744
+ if inner_patterns.is_empty()
1745
+ {
1746
+ return Err(ErrorKind::PathAcceptSingleEdge);
1747
+ }
1748
+
1749
+ // Extract source and destination from the patterns for the QuantifiedPathPattern
1750
+ let (source, destination) = if inner_patterns.len() == 1
1751
+ {
1752
+ match &inner_patterns[0]
1753
+ {
1754
+ ast::Pattern::Node(_) => return Err(ErrorKind::PathAcceptSingleEdge),
1755
+ ast::Pattern::Edge(e) => (e.source.clone(), e.destination.clone()),
1756
+ ast::Pattern::Path(p) => (p.edge.source.clone(), p.edge.destination.clone()),
1757
+ ast::Pattern::QuantifiedPath(qp) => (qp.source.clone(), qp.destination.clone()),
1758
+ }
1759
+ }
1760
+ else
1761
+ {
1762
+ match (
1763
+ &inner_patterns[0],
1764
+ &inner_patterns[inner_patterns.len() - 1],
1765
+ )
1766
+ {
1767
+ (
1768
+ ast::Pattern::Edge(e_first)
1769
+ | ast::Pattern::Path(ast::PathPattern { edge: e_first, .. }),
1770
+ ast::Pattern::Edge(e_last) | ast::Pattern::Path(ast::PathPattern { edge: e_last, .. }),
1771
+ ) => (e_first.source.clone(), e_last.destination.clone()),
1772
+ _ => return Err(ErrorKind::PathAcceptSingleEdge),
1773
+ }
1774
+ };
1775
+
1776
+ // Use combine_patterns_with_variable helper to handle combining logic
1777
+ let combined = combine_patterns_with_variable(context, variable_name, inner_patterns)?;
1778
+
1779
+ if combined.is_empty()
1780
+ {
1781
+ return Err(ErrorKind::PathAcceptSingleEdge);
1782
+ }
1783
+
1784
+ // For selector_scoped_pattern, we always wrap the result in a QuantifiedPathPattern
1785
+ // with quantifier: None and group_where set
1786
+ // This works for any pattern type returned by combine_patterns_with_variable
1787
+ match combined.len()
1788
+ {
1789
+ 1 =>
1790
+ {
1791
+ match combined.into_iter().next().unwrap()
1207
1792
  {
1208
- ast::Pattern::Edge(e) => e,
1209
- _ => unreachable!(),
1210
- })
1211
- .collect();
1212
- // Combine edges into a single EdgePattern by taking the first source and last destination.
1213
- // Preserve an inner edge variable if exactly one inner edge is named.
1214
- let combined_variable = {
1215
- let mut found: Option<ast::VariableIdentifier> = None;
1216
- for e in edges.iter()
1793
+ ast::Pattern::QuantifiedPath(mut qp) =>
1794
+ {
1795
+ // Already a QuantifiedPathPattern from combine_patterns_with_variable.
1796
+ // If it already has a quantifier (inner pattern is quantified like --+),
1797
+ // preserve it and just add group_where. If quantifier is None (pre-filter
1798
+ // wrapper case), leave it as None. Do NOT unconditionally override it.
1799
+ qp.group_where = group_where;
1800
+ Ok(vec![ast::Pattern::QuantifiedPath(qp)])
1801
+ }
1802
+ ast::Pattern::Path(path_pattern) =>
1803
+ {
1804
+ // PathPattern from combine_patterns_with_variable (when variable bound to unquantified path)
1805
+ // Wrap in QuantifiedPathPattern with no quantifier
1806
+ let qp = ast::QuantifiedPathPattern {
1807
+ variable: Some(path_pattern.variable),
1808
+ source,
1809
+ group: vec![ast::Pattern::Edge(path_pattern.edge)],
1810
+ group_where,
1811
+ path_where: None,
1812
+ quantifier: None,
1813
+ destination,
1814
+ selector: ast::PathSelector::All,
1815
+ };
1816
+ Ok(vec![ast::Pattern::QuantifiedPath(qp)])
1817
+ }
1818
+ ast::Pattern::Edge(edge) =>
1819
+ {
1820
+ // Single Edge pattern (no variable binding)
1821
+ // Wrap in QuantifiedPathPattern with no quantifier
1822
+ let qp = ast::QuantifiedPathPattern {
1823
+ variable: None,
1824
+ source,
1825
+ group: vec![ast::Pattern::Edge(edge)],
1826
+ group_where,
1827
+ path_where: None,
1828
+ quantifier: None,
1829
+ destination,
1830
+ selector: ast::PathSelector::All,
1831
+ };
1832
+ Ok(vec![ast::Pattern::QuantifiedPath(qp)])
1833
+ }
1834
+ ast::Pattern::Node(_) =>
1835
+ {
1836
+ // Node pattern is not allowed
1837
+ Err(ErrorKind::PathAcceptSingleEdge)
1838
+ }
1839
+ }
1840
+ }
1841
+ _ =>
1842
+ {
1843
+ // Multiple patterns - wrap them all in a QuantifiedPathPattern
1844
+ let qp = ast::QuantifiedPathPattern {
1845
+ variable: None,
1846
+ source,
1847
+ group: combined,
1848
+ group_where,
1849
+ path_where: None,
1850
+ quantifier: None,
1851
+ destination,
1852
+ selector: ast::PathSelector::All,
1853
+ };
1854
+ Ok(vec![ast::Pattern::QuantifiedPath(qp)])
1855
+ }
1856
+ }
1857
+ },
1858
+ )
1859
+ .parse(input)
1860
+ }
1861
+
1862
+ fn patterns<T: EdgeMode>(input: Input) -> OCPResult<Input, Vec<ast::Pattern>>
1863
+ {
1864
+ let context = &input.1;
1865
+ map_res(
1866
+ (
1867
+ opt((ident, token(Token::Equal))),
1868
+ opt(path_selector),
1869
+ alt((selector_scoped_pattern::<T>, node_or_edges_patterns::<T>)),
1870
+ ),
1871
+ |(variable_name_and_equal, selector, patterns)| {
1872
+ let variable_name = variable_name_and_equal.map(|(name, _)| name);
1873
+
1874
+ // Step 2: Run the existing combining logic completely unchanged.
1875
+ // This is the closure body from the original `alt` branch, operating on
1876
+ // patterns and variable_name exactly as before.
1877
+
1878
+ // If there's no variable binding, just pass through with selector applied (if any)
1879
+ if variable_name.is_none()
1880
+ {
1881
+ // No path variable binding; patterns are used as-is with optional selector
1882
+ let mut result = patterns;
1883
+ if let Some(selector) = selector
1884
+ {
1885
+ // Step 3: Apply selector as final wrap only if present
1886
+ if result.len() != 1
1887
+ {
1888
+ return Err(ErrorKind::PathAcceptSingleEdge);
1889
+ }
1890
+ match &mut result[0]
1217
1891
  {
1218
- if let Some(var) = &e.variable
1892
+ ast::Pattern::QuantifiedPath(qp) =>
1219
1893
  {
1220
- if found.is_some()
1221
- {
1222
- // multiple inner variables; do not attempt to preserve a single one
1223
- found = None;
1224
- break;
1225
- }
1226
- else
1227
- {
1228
- found = Some(var.clone());
1229
- }
1894
+ qp.selector = selector;
1895
+ }
1896
+ ast::Pattern::Edge(_) | ast::Pattern::Path(_) =>
1897
+ {
1898
+ // Scoping-wrapper case (design §1d): wrap the existing pattern
1899
+ // into a QuantifiedPathPattern with no quantifier, carrying only the selector.
1900
+ let wrapped = ast::Pattern::QuantifiedPath(ast::QuantifiedPathPattern {
1901
+ variable: None,
1902
+ source: match &result[0]
1903
+ {
1904
+ ast::Pattern::Edge(e) => e.source.clone(),
1905
+ ast::Pattern::Path(p) => p.edge.source.clone(),
1906
+ _ => unreachable!(),
1907
+ },
1908
+ group: vec![result[0].clone()],
1909
+ group_where: None,
1910
+ path_where: None,
1911
+ quantifier: None,
1912
+ destination: match &result[0]
1913
+ {
1914
+ ast::Pattern::Edge(e) => e.destination.clone(),
1915
+ ast::Pattern::Path(p) => p.edge.destination.clone(),
1916
+ _ => unreachable!(),
1917
+ },
1918
+ selector,
1919
+ });
1920
+ result = vec![wrapped];
1921
+ }
1922
+ ast::Pattern::Node(_) =>
1923
+ {
1924
+ return Err(ErrorKind::PathAcceptSingleEdge);
1230
1925
  }
1231
1926
  }
1232
- found
1233
- };
1234
- let original_count = edges.len();
1235
- let first_edge = edges.remove(0);
1236
- let last_edge = edges.pop().unwrap_or_else(|| first_edge.clone());
1237
- // For a single-edge path, preserve that edge's labels, properties, and recursive_range.
1238
- // For multi-hop paths, compute the combined hop range (each fixed edge = 1 hop,
1239
- // var-length edge contributes its own range). Use original_count (before removes)
1240
- // to correctly distinguish single-edge (N=1) from two-edge (N=2) paths.
1241
- let (combined_labels, combined_properties, combined_recursive_range) = if original_count
1242
- == 1
1927
+ }
1928
+ return Ok(result);
1929
+ }
1930
+
1931
+ let name = variable_name.unwrap();
1932
+
1933
+ // Accept path assignment for one or more consecutive edges by combining them into a single EdgePattern.
1934
+ if patterns.is_empty()
1935
+ {
1936
+ return Err(ErrorKind::PathAcceptSingleEdge);
1937
+ }
1938
+ // A path variable over a single quantified segment: `p = (a)-[*1..3]->(b)`
1939
+ // or its new-syntax/multi-element equivalents. Bind the path variable
1940
+ // directly onto the QuantifiedPathPattern; PathPattern is not involved.
1941
+ if let [ast::Pattern::QuantifiedPath(qp)] = patterns.as_slice()
1942
+ {
1943
+ let mut qp = qp.clone();
1944
+ qp.variable = Some(context.identifiers.create_variable_from_name(name));
1945
+ // Step 3: Apply selector if present
1946
+ if let Some(selector) = selector
1243
1947
  {
1244
- // Single edge: preserve its constraints.
1245
- (
1246
- first_edge.labels.clone(),
1247
- first_edge.properties.clone(),
1248
- first_edge.recursive_range.clone(),
1249
- )
1948
+ qp.selector = selector;
1250
1949
  }
1251
- else
1950
+ return Ok(vec![ast::Pattern::QuantifiedPath(qp)]);
1951
+ }
1952
+ // A path variable over one or more *un*quantified edges: today's
1953
+ // existing combining behavior, but now handling QuantifiedPath patterns.
1954
+ // Extract all edges and track if any have quantifiers.
1955
+ struct EdgeWithQuantifier
1956
+ {
1957
+ edge: ast::EdgePattern,
1958
+ quantifier: Option<std::ops::Range<usize>>,
1959
+ }
1960
+ let mut edges_with_quantifiers: Vec<EdgeWithQuantifier> = Vec::new();
1961
+ for p in patterns
1962
+ {
1963
+ match p
1252
1964
  {
1253
- // Multi-hop: accumulate min and max hops across all constituent edges.
1254
- // Fixed edges contribute 1 hop each; var-length edges contribute their range.
1255
- let hop_range = |e: &ast::EdgePattern| -> (usize, usize) {
1256
- match &e.recursive_range
1965
+ ast::Pattern::Edge(e) =>
1966
+ {
1967
+ edges_with_quantifiers.push(EdgeWithQuantifier {
1968
+ edge: e,
1969
+ quantifier: None,
1970
+ });
1971
+ }
1972
+ ast::Pattern::QuantifiedPath(qp) =>
1973
+ {
1974
+ if qp.group.len() != 1
1257
1975
  {
1258
- Some(r) => (r.start, r.end),
1259
- None => (1, 1),
1976
+ return Err(ErrorKind::PathAcceptSingleEdge);
1260
1977
  }
1261
- };
1262
- let (mut min_total, mut max_total) = hop_range(&first_edge);
1263
- for e in &edges
1978
+ let ast::Pattern::Edge(e) = &qp.group[0]
1979
+ else
1980
+ {
1981
+ return Err(ErrorKind::PathAcceptSingleEdge);
1982
+ };
1983
+ edges_with_quantifiers.push(EdgeWithQuantifier {
1984
+ edge: e.clone(),
1985
+ quantifier: qp.quantifier.clone(),
1986
+ });
1987
+ }
1988
+ _ => return Err(ErrorKind::PathAcceptSingleEdge),
1989
+ }
1990
+ }
1991
+ if edges_with_quantifiers.is_empty()
1992
+ {
1993
+ return Err(ErrorKind::PathAcceptSingleEdge);
1994
+ }
1995
+
1996
+ // Check if any edge has a quantifier
1997
+ let has_quantifier = edges_with_quantifiers
1998
+ .iter()
1999
+ .any(|ewq| ewq.quantifier.is_some());
2000
+
2001
+ // Combine edges into a single EdgePattern by taking the first source and last destination.
2002
+ // Preserve an inner edge variable if exactly one inner edge is named.
2003
+ let combined_variable = {
2004
+ let mut found: Option<ast::VariableIdentifier> = None;
2005
+ for ewq in edges_with_quantifiers.iter()
2006
+ {
2007
+ if let Some(var) = &ewq.edge.variable
2008
+ {
2009
+ if found.is_some()
2010
+ {
2011
+ // multiple inner variables; do not attempt to preserve a single one
2012
+ found = None;
2013
+ break;
2014
+ }
2015
+ else
2016
+ {
2017
+ found = Some(var.clone());
2018
+ }
2019
+ }
2020
+ }
2021
+ found
2022
+ };
2023
+ let original_count = edges_with_quantifiers.len();
2024
+ let first_ewq = edges_with_quantifiers.remove(0);
2025
+ let last_ewq = edges_with_quantifiers
2026
+ .pop()
2027
+ .unwrap_or_else(|| EdgeWithQuantifier {
2028
+ edge: first_ewq.edge.clone(),
2029
+ quantifier: first_ewq.quantifier.clone(),
2030
+ });
2031
+
2032
+ // For a single-edge path, preserve that edge's labels, properties, and predicate.
2033
+ // For multi-hop paths, all edges become fixed edges in the path representation.
2034
+ // Use original_count (before removes) to correctly distinguish single-edge (N=1) from two-edge (N=2) paths.
2035
+ let (combined_labels, combined_properties, combined_predicate) = if original_count == 1
2036
+ {
2037
+ // Single edge: preserve its constraints.
2038
+ (
2039
+ first_ewq.edge.labels.clone(),
2040
+ first_ewq.edge.properties.clone(),
2041
+ first_ewq.edge.predicate.clone(),
2042
+ )
2043
+ }
2044
+ else
2045
+ {
2046
+ // Multi-hop: all edges become fixed edges in the path representation
2047
+ (ast::LabelExpression::None, None, None)
2048
+ };
2049
+ // Clone source and destination before they're moved into combined_edge
2050
+ let qp_source = first_ewq.edge.source.clone();
2051
+ let qp_destination = last_ewq.edge.destination.clone();
2052
+ let combined_edge = ast::EdgePattern {
2053
+ variable: combined_variable,
2054
+ source: first_ewq.edge.source,
2055
+ destination: last_ewq.edge.destination,
2056
+ directivity: first_ewq.edge.directivity,
2057
+ labels: combined_labels,
2058
+ properties: combined_properties,
2059
+ predicate: combined_predicate,
2060
+ };
2061
+
2062
+ // If any edge had a quantifier, wrap the combined result in QuantifiedPath.
2063
+ // If all edges were unquantified, wrap in PathPattern (original behavior).
2064
+ if has_quantifier
2065
+ {
2066
+ // For mixed chains with quantifiers, wrap in QuantifiedPath.
2067
+ // Use 0..usize::MAX as the quantifier range for now (represents [*] in legacy syntax).
2068
+ // This preserves the semantics of "at least one path must exist" while allowing
2069
+ // variable length paths to be represented.
2070
+ let quantifier_range = if let Some(var_range) = &last_ewq.quantifier
2071
+ {
2072
+ // If the last edge has a quantifier, use it (with adjustment for preceding fixed edges)
2073
+ if original_count == 1
2074
+ {
2075
+ last_ewq.quantifier.clone()
2076
+ }
2077
+ else
1264
2078
  {
1265
- let (mn, mx) = hop_range(e);
1266
- min_total += mn;
1267
- max_total = max_total.saturating_add(mx);
2079
+ // For multi-hop paths, each fixed edge = 1 hop, then add the variable range
2080
+ Some(
2081
+ (var_range.start + (original_count - 1))
2082
+ ..var_range.end.saturating_add(original_count - 1),
2083
+ )
1268
2084
  }
1269
- let (mn, mx) = hop_range(&last_edge);
1270
- min_total += mn;
1271
- max_total = max_total.saturating_add(mx);
1272
- (ast::LabelExpression::None, None, Some(min_total..max_total))
2085
+ }
2086
+ else
2087
+ {
2088
+ // No quantifier found (shouldn't happen if has_quantifier is true, but defensive)
2089
+ Some(0..usize::MAX)
1273
2090
  };
1274
- let combined_edge = ast::EdgePattern {
1275
- variable: combined_variable,
1276
- source: first_edge.source,
1277
- destination: last_edge.destination,
1278
- directivity: first_edge.directivity,
1279
- labels: combined_labels,
1280
- properties: combined_properties,
1281
- recursive_range: combined_recursive_range,
2091
+
2092
+ let mut qp = ast::QuantifiedPathPattern {
2093
+ variable: Some(context.identifiers.create_variable_from_name(name)),
2094
+ source: qp_source,
2095
+ group: vec![ast::Pattern::Edge(combined_edge)],
2096
+ group_where: None,
2097
+ path_where: None,
2098
+ quantifier: quantifier_range,
2099
+ destination: qp_destination,
2100
+ selector: ast::PathSelector::All,
1282
2101
  };
1283
- Ok(vec![
2102
+ // Step 3: Apply selector if present
2103
+ if let Some(selector) = selector
2104
+ {
2105
+ qp.selector = selector;
2106
+ }
2107
+ Ok(vec![ast::Pattern::QuantifiedPath(qp)])
2108
+ }
2109
+ else
2110
+ {
2111
+ let mut result = vec![
1284
2112
  ast::PathPattern {
1285
2113
  variable: context.identifiers.create_variable_from_name(name),
1286
2114
  edge: combined_edge,
1287
2115
  }
1288
2116
  .into(),
1289
- ])
1290
- },
1291
- ),
1292
- node_or_edges_patterns::<T>,
1293
- ))
2117
+ ];
2118
+ // Step 3: Apply selector if present - wrap unquantified path in QuantifiedPath
2119
+ if let Some(selector) = selector
2120
+ {
2121
+ let wrapped = ast::Pattern::QuantifiedPath(ast::QuantifiedPathPattern {
2122
+ variable: Some(context.identifiers.create_variable_from_name(name)),
2123
+ source: qp_source,
2124
+ group: result,
2125
+ group_where: None,
2126
+ path_where: None,
2127
+ quantifier: None,
2128
+ destination: qp_destination,
2129
+ selector,
2130
+ });
2131
+ result = vec![wrapped];
2132
+ }
2133
+ Ok(result)
2134
+ }
2135
+ },
2136
+ )
1294
2137
  .parse(input)
1295
2138
  }
1296
2139
 
@@ -1364,6 +2207,7 @@ fn match_statement(input: Input) -> OCPResult<Input, ast::Statement>
1364
2207
  ast::Pattern::Node(n) => n.variable.as_ref().map(|v| v.name().clone()),
1365
2208
  ast::Pattern::Edge(e) => e.variable.as_ref().map(|v| v.name().clone()),
1366
2209
  ast::Pattern::Path(p) => Some(p.variable.name().clone()),
2210
+ ast::Pattern::QuantifiedPath(qp) => qp.variable.as_ref().map(|v| v.name().clone()),
1367
2211
  })
1368
2212
  .collect();
1369
2213
 
@@ -1656,15 +2500,16 @@ fn return_statement(input: Input) -> OCPResult<Input, ast::Statement>
1656
2500
  map(
1657
2501
  (
1658
2502
  token(Token::Return),
2503
+ opt(token(Token::Distinct)),
1659
2504
  with_return_expression,
1660
2505
  modifiers,
1661
2506
  opt(preceded(token(Token::Where), expression)),
1662
2507
  ),
1663
- |(_, (all, expressions), modifiers, where_expression)| {
2508
+ |(_, distinct, (all, expressions), modifiers, where_expression)| {
1664
2509
  ast::Return {
1665
2510
  all,
1666
2511
  expressions,
1667
- modifiers,
2512
+ modifiers: modifiers.set_distinct(distinct.is_some()),
1668
2513
  where_expression,
1669
2514
  }
1670
2515
  .into()
@@ -1678,15 +2523,16 @@ fn with_statement(input: Input) -> OCPResult<Input, ast::Statement>
1678
2523
  map(
1679
2524
  (
1680
2525
  token(Token::With),
2526
+ opt(token(Token::Distinct)),
1681
2527
  with_return_expression,
1682
2528
  modifiers,
1683
2529
  opt(preceded(token(Token::Where), expression)),
1684
2530
  ),
1685
- |(_, (all, expressions), modifiers, where_expression)| {
2531
+ |(_, distinct, (all, expressions), modifiers, where_expression)| {
1686
2532
  ast::With {
1687
2533
  all,
1688
2534
  expressions,
1689
- modifiers,
2535
+ modifiers: modifiers.set_distinct(distinct.is_some()),
1690
2536
  where_expression,
1691
2537
  }
1692
2538
  .into()
@@ -1747,7 +2593,7 @@ fn parse_set_actions(input: Input) -> OCPResult<Input, Vec<ast::OneUpdate>>
1747
2593
  )),
1748
2594
  many0(preceded(
1749
2595
  token(Token::Dot),
1750
- to_string(alt((ident, string_literal))),
2596
+ alt((to_string(ident), string_literal)),
1751
2597
  )),
1752
2598
  token(Token::Equal),
1753
2599
  expression,
@@ -1772,7 +2618,7 @@ fn parse_set_actions(input: Input) -> OCPResult<Input, Vec<ast::OneUpdate>>
1772
2618
  )),
1773
2619
  many0(preceded(
1774
2620
  token(Token::Dot),
1775
- to_string(alt((ident, string_literal))),
2621
+ alt((to_string(ident), string_literal)),
1776
2622
  )),
1777
2623
  token(Token::PlusEqual),
1778
2624
  expression,
@@ -1824,7 +2670,7 @@ fn remove_statement(input: Input) -> OCPResult<Input, ast::Statement>
1824
2670
  ident,
1825
2671
  many1(preceded(
1826
2672
  token(Token::Dot),
1827
- to_string(alt((ident, string_literal))),
2673
+ alt((to_string(ident), string_literal)),
1828
2674
  )),
1829
2675
  ),
1830
2676
  |(ident, path)| {
@@ -1884,13 +2730,7 @@ fn call_statement(input: Input) -> OCPResult<Input, ast::Statement>
1884
2730
  fn use_statement(input: Input) -> OCPResult<Input, ast::Statement>
1885
2731
  {
1886
2732
  map(
1887
- (
1888
- token(Token::Use),
1889
- alt((
1890
- map(string_literal, |s| s.to_string()),
1891
- map(ident, |s| s.to_string()),
1892
- )),
1893
- ),
2733
+ (token(Token::Use), alt((string_literal, to_string(ident)))),
1894
2734
  |(_, name)| ast::UseGraph { name }.into(),
1895
2735
  )
1896
2736
  .parse(input)
@@ -1903,10 +2743,7 @@ fn create_graph_statement(input: Input) -> OCPResult<Input, ast::Statement>
1903
2743
  token(Token::Create),
1904
2744
  token(Token::Graph),
1905
2745
  opt((token(Token::If), token(Token::Not), token(Token::Exists))),
1906
- alt((
1907
- map(string_literal, |s| s.to_string()),
1908
- map(ident, |s| s.to_string()),
1909
- )),
2746
+ alt((string_literal, to_string(ident))),
1910
2747
  ),
1911
2748
  |(_, _, if_not_exists, name)| {
1912
2749
  ast::CreateGraph {
@@ -1926,10 +2763,7 @@ fn drop_graph_statement(input: Input) -> OCPResult<Input, ast::Statement>
1926
2763
  token(Token::Drop),
1927
2764
  token(Token::Graph),
1928
2765
  opt((token(Token::If), token(Token::Exists))),
1929
- alt((
1930
- map(string_literal, |s| s.to_string()),
1931
- map(ident, |s| s.to_string()),
1932
- )),
2766
+ alt((string_literal, to_string(ident))),
1933
2767
  ),
1934
2768
  |(_, _, if_exists, name)| {
1935
2769
  ast::DropGraph {
@@ -2003,3 +2837,299 @@ pub fn queries(input: Input) -> OCPResult<Input, ast::Queries>
2003
2837
  {
2004
2838
  separated_list1(token(Token::Semi), query).parse(input)
2005
2839
  }
2840
+
2841
+ #[cfg(test)]
2842
+ mod quantifier_suffix_tests
2843
+ {
2844
+ use super::*;
2845
+ use crate::oc::lexer::tokenize;
2846
+
2847
+ fn parse_quantifier(input: &str) -> Option<std::ops::Range<usize>>
2848
+ {
2849
+ let spanned_tokens = tokenize(input).ok()?;
2850
+ let context = Context::new(input);
2851
+ let input = Input(&spanned_tokens, &context);
2852
+ quantifier_suffix(input).ok().map(|(_, range)| range)
2853
+ }
2854
+
2855
+ #[test]
2856
+ fn test_quantifier_braced_min_max()
2857
+ {
2858
+ // {min,max} form
2859
+ let result = parse_quantifier("{1,5}");
2860
+ assert_eq!(result, Some(1..5));
2861
+ }
2862
+
2863
+ #[test]
2864
+ fn test_quantifier_braced_exact()
2865
+ {
2866
+ // {n} form - exact count
2867
+ let result = parse_quantifier("{3}");
2868
+ assert_eq!(result, Some(3..3));
2869
+ }
2870
+
2871
+ #[test]
2872
+ fn test_quantifier_braced_min_open()
2873
+ {
2874
+ // {min,} form
2875
+ let result = parse_quantifier("{2,}");
2876
+ assert_eq!(result, Some(2..usize::MAX));
2877
+ }
2878
+
2879
+ #[test]
2880
+ fn test_quantifier_braced_max_open()
2881
+ {
2882
+ // {,max} form
2883
+ let result = parse_quantifier("{,4}");
2884
+ assert_eq!(result, Some(0..4));
2885
+ }
2886
+
2887
+ #[test]
2888
+ fn test_quantifier_bare_star()
2889
+ {
2890
+ // Bare `*` form: zero-or-more
2891
+ let result = parse_quantifier("*");
2892
+ assert_eq!(result, Some(0..usize::MAX));
2893
+ }
2894
+
2895
+ #[test]
2896
+ fn test_quantifier_bare_plus()
2897
+ {
2898
+ // Bare `+` form: one-or-more
2899
+ let result = parse_quantifier("+");
2900
+ assert_eq!(result, Some(1..usize::MAX));
2901
+ }
2902
+ }
2903
+
2904
+ #[cfg(test)]
2905
+ mod path_selector_tests
2906
+ {
2907
+ use super::*;
2908
+ use crate::oc::lexer::tokenize;
2909
+
2910
+ fn parse_path_selector(input: &str) -> Option<ast::PathSelector>
2911
+ {
2912
+ let spanned_tokens = tokenize(input).ok()?;
2913
+ let context = Context::new(input);
2914
+ let input = Input(&spanned_tokens, &context);
2915
+ path_selector(input).ok().map(|(_, selector)| selector)
2916
+ }
2917
+
2918
+ fn assert_selector_eq(actual: ast::PathSelector, expected: ast::PathSelector, msg: &str)
2919
+ {
2920
+ assert_eq!(
2921
+ format!("{:?}", actual),
2922
+ format!("{:?}", expected),
2923
+ "{}",
2924
+ msg
2925
+ );
2926
+ }
2927
+
2928
+ #[test]
2929
+ fn test_path_selector_shortest_k()
2930
+ {
2931
+ // SHORTEST 1
2932
+ let result = parse_path_selector("SHORTEST 1");
2933
+ assert!(result.is_some(), "Expected SHORTEST 1 to parse");
2934
+ if let Some(selector) = result
2935
+ {
2936
+ assert!(
2937
+ matches!(selector, ast::PathSelector::Shortest(_)),
2938
+ "Expected Shortest variant"
2939
+ );
2940
+ }
2941
+ }
2942
+
2943
+ #[test]
2944
+ fn test_path_selector_shortest_parameter()
2945
+ {
2946
+ // SHORTEST $k
2947
+ let result = parse_path_selector("SHORTEST $k");
2948
+ assert!(result.is_some(), "Expected SHORTEST $k to parse");
2949
+ if let Some(selector) = result
2950
+ {
2951
+ assert!(
2952
+ matches!(selector, ast::PathSelector::Shortest(_)),
2953
+ "Expected Shortest variant"
2954
+ );
2955
+ }
2956
+ }
2957
+
2958
+ #[test]
2959
+ fn test_path_selector_shortest_k_groups()
2960
+ {
2961
+ // SHORTEST 2 GROUPS
2962
+ let result = parse_path_selector("SHORTEST 2 GROUPS");
2963
+ assert!(result.is_some(), "Expected SHORTEST 2 GROUPS to parse");
2964
+ if let Some(selector) = result
2965
+ {
2966
+ assert!(
2967
+ matches!(selector, ast::PathSelector::ShortestGroups(_)),
2968
+ "Expected ShortestGroups variant"
2969
+ );
2970
+ }
2971
+ }
2972
+
2973
+ #[test]
2974
+ fn test_path_selector_shortest_group_sugar()
2975
+ {
2976
+ // SHORTEST GROUP (singular sugar, k defaults to 1)
2977
+ let result = parse_path_selector("SHORTEST GROUP");
2978
+ assert!(result.is_some(), "Expected SHORTEST GROUP to parse");
2979
+ if let Some(selector) = result
2980
+ {
2981
+ assert!(
2982
+ matches!(selector, ast::PathSelector::ShortestGroups(_)),
2983
+ "Expected ShortestGroups variant for SHORTEST GROUP"
2984
+ );
2985
+ }
2986
+ }
2987
+
2988
+ #[test]
2989
+ fn test_path_selector_shortest_group_equivalence()
2990
+ {
2991
+ // SHORTEST GROUP and SHORTEST 1 GROUPS produce same representation
2992
+ let result_group = parse_path_selector("SHORTEST GROUP");
2993
+ let result_groups = parse_path_selector("SHORTEST 1 GROUPS");
2994
+
2995
+ assert!(result_group.is_some(), "Expected SHORTEST GROUP to parse");
2996
+ assert!(
2997
+ result_groups.is_some(),
2998
+ "Expected SHORTEST 1 GROUPS to parse"
2999
+ );
3000
+
3001
+ if let (Some(group_selector), Some(groups_selector)) = (result_group, result_groups)
3002
+ {
3003
+ // Both should be ShortestGroups and should have the same string representation
3004
+ // (confirming alias normalization collapses them to one representation)
3005
+ assert_selector_eq(
3006
+ group_selector,
3007
+ groups_selector,
3008
+ "Expected SHORTEST GROUP to be equivalent to SHORTEST 1 GROUPS",
3009
+ );
3010
+ }
3011
+ }
3012
+
3013
+ #[test]
3014
+ fn test_path_selector_all_shortest()
3015
+ {
3016
+ // ALL SHORTEST
3017
+ let result = parse_path_selector("ALL SHORTEST");
3018
+ assert!(result.is_some(), "Expected ALL SHORTEST to parse");
3019
+ if let Some(selector) = result
3020
+ {
3021
+ assert_selector_eq(
3022
+ selector,
3023
+ ast::PathSelector::AllShortest,
3024
+ "Expected AllShortest variant",
3025
+ );
3026
+ }
3027
+ }
3028
+
3029
+ #[test]
3030
+ fn test_path_selector_any()
3031
+ {
3032
+ // ANY
3033
+ let result = parse_path_selector("ANY");
3034
+ assert!(result.is_some(), "Expected ANY to parse");
3035
+ if let Some(selector) = result
3036
+ {
3037
+ assert_selector_eq(
3038
+ selector,
3039
+ ast::PathSelector::Any,
3040
+ "Expected Any variant for bare ANY",
3041
+ );
3042
+ }
3043
+ }
3044
+
3045
+ #[test]
3046
+ fn test_path_selector_any_shortest()
3047
+ {
3048
+ // ANY SHORTEST
3049
+ let result = parse_path_selector("ANY SHORTEST");
3050
+ assert!(result.is_some(), "Expected ANY SHORTEST to parse");
3051
+ if let Some(selector) = result
3052
+ {
3053
+ assert_selector_eq(
3054
+ selector,
3055
+ ast::PathSelector::Any,
3056
+ "Expected Any variant for ANY SHORTEST (alias normalized to Any)",
3057
+ );
3058
+ }
3059
+ }
3060
+
3061
+ #[test]
3062
+ fn test_path_selector_all()
3063
+ {
3064
+ // ALL (bare, no SHORTEST following)
3065
+ let result = parse_path_selector("ALL");
3066
+ assert!(result.is_some(), "Expected ALL to parse");
3067
+ if let Some(selector) = result
3068
+ {
3069
+ assert_selector_eq(
3070
+ selector,
3071
+ ast::PathSelector::All,
3072
+ "Expected All variant for bare ALL",
3073
+ );
3074
+ }
3075
+ }
3076
+
3077
+ #[test]
3078
+ fn test_path_selector_ordering_all_shortest_before_all()
3079
+ {
3080
+ // ALL SHORTEST - verifies that ALL SHORTEST is tried
3081
+ // before bare ALL in the alt ordering
3082
+ let result = parse_path_selector("ALL SHORTEST");
3083
+ assert!(result.is_some(), "Expected ALL SHORTEST to parse correctly");
3084
+
3085
+ if let Some(selector) = result
3086
+ {
3087
+ // Confirm it parsed as AllShortest, not just All
3088
+ match selector
3089
+ {
3090
+ ast::PathSelector::AllShortest =>
3091
+ {
3092
+ // Success: alt correctly tried ALL SHORTEST before bare ALL
3093
+ }
3094
+ _ =>
3095
+ {
3096
+ panic!(
3097
+ "Expected AllShortest variant, got {:?}. This indicates alt ordering issue where ALL consumed and SHORTEST was left unparsed.",
3098
+ selector
3099
+ );
3100
+ }
3101
+ }
3102
+ }
3103
+ }
3104
+
3105
+ #[test]
3106
+ fn test_path_selector_ordering_shortest_k_groups_before_shortest_k()
3107
+ {
3108
+ // SHORTEST 2 GROUPS - verifies that SHORTEST <k> GROUPS is tried
3109
+ // before bare SHORTEST <k> in the alt ordering
3110
+ let result = parse_path_selector("SHORTEST 2 GROUPS");
3111
+ assert!(
3112
+ result.is_some(),
3113
+ "Expected SHORTEST 2 GROUPS to parse correctly"
3114
+ );
3115
+
3116
+ if let Some(selector) = result
3117
+ {
3118
+ // Confirm it parsed as ShortestGroups, not just Shortest
3119
+ match selector
3120
+ {
3121
+ ast::PathSelector::ShortestGroups(_) =>
3122
+ {
3123
+ // Success: alt correctly tried SHORTEST <k> GROUPS before SHORTEST <k>
3124
+ }
3125
+ _ =>
3126
+ {
3127
+ panic!(
3128
+ "Expected ShortestGroups variant, got {:?}. This indicates alt ordering issue where SHORTEST <k> consumed '2' and left 'GROUPS' unparsed.",
3129
+ selector
3130
+ );
3131
+ }
3132
+ }
3133
+ }
3134
+ }
3135
+ }