gqlite 1.8.1 → 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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/gqlitedb/Cargo.toml +0 -2
  4. data/ext/gqlitedb/src/compiler/variables_manager.rs +109 -10
  5. data/ext/gqlitedb/src/compiler.rs +481 -47
  6. data/ext/gqlitedb/src/connection.rs +12 -0
  7. data/ext/gqlitedb/src/consts.rs +1 -1
  8. data/ext/gqlitedb/src/error.rs +24 -0
  9. data/ext/gqlitedb/src/interpreter/evaluators.rs +91 -325
  10. data/ext/gqlitedb/src/interpreter/instructions.rs +72 -0
  11. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  12. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  13. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  14. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  15. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  16. data/ext/gqlitedb/src/planner.rs +218 -6
  17. data/ext/gqlitedb/src/query_result.rs +19 -4
  18. data/ext/gqlitedb/src/tests/compiler.rs +2453 -1
  19. data/ext/gqlitedb/src/tests/connection/redb.rs +8 -0
  20. data/ext/gqlitedb/src/tests/connection.rs +37 -0
  21. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  22. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  23. data/ext/gqlitedb/src/tests/planner.rs +432 -2
  24. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  25. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  26. data/ext/gqlitedb/src/tests/templates/ast.rs +284 -1
  27. data/ext/gqlitedb/src/tests/templates/programs.rs +306 -0
  28. data/ext/gqliterb/Cargo.toml +0 -1
  29. data/ext/gqlparser/src/oc/ast.rs +52 -3
  30. data/ext/gqlparser/src/oc/error.rs +18 -0
  31. data/ext/gqlparser/src/oc/lexer.rs +146 -0
  32. data/ext/gqlparser/src/oc/parser/tests.rs +365 -20
  33. data/ext/gqlparser/src/oc/parser.rs +1196 -143
  34. data/ext/graphcore/src/value.rs +12 -1
  35. metadata +5 -2
  36. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -1035,11 +1035,11 @@ fn array_literal(input: Input) -> OCPResult<Input, ast::Expression>
1035
1035
  .parse(input)
1036
1036
  }
1037
1037
 
1038
- fn node_pattern(input: Input) -> OCPResult<Input, ast::NodePattern>
1038
+ fn node_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, ast::NodePattern>
1039
1039
  {
1040
- delimited(
1041
- token(Token::StartParenthesis),
1042
- map(
1040
+ map_res_failure(
1041
+ delimited(
1042
+ token(Token::StartParenthesis),
1043
1043
  (
1044
1044
  // Optional variable
1045
1045
  opt(ident),
@@ -1047,19 +1047,28 @@ fn node_pattern(input: Input) -> OCPResult<Input, ast::NodePattern>
1047
1047
  opt(labels),
1048
1048
  // Optional properties map or parameter
1049
1049
  opt(properties),
1050
+ // Optional inline predicate: WHERE <expr>
1051
+ opt(preceded(token(Token::Where), expression)),
1050
1052
  ),
1051
- |(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 {
1052
1061
  variable: input
1053
1062
  .1
1054
1063
  .identifiers
1055
1064
  .create_variable_from_name_optional(ident),
1056
1065
  labels: labels.unwrap_or(ast::LabelExpression::None),
1057
1066
  properties,
1058
- },
1059
- ),
1060
- token(Token::EndParenthesis),
1067
+ predicate,
1068
+ })
1069
+ },
1061
1070
  )
1062
- .parse(input)
1071
+ .parse(input.clone())
1063
1072
  }
1064
1073
 
1065
1074
  enum EdgeDirection
@@ -1073,6 +1082,7 @@ trait EdgeMode
1073
1082
  {
1074
1083
  const ALLOW_UNDIRECTED: bool;
1075
1084
  const ALLOW_LABEL_ALTERNATIVE: bool;
1085
+ const ALLOW_PREDICATE: bool;
1076
1086
  }
1077
1087
 
1078
1088
  struct AllowUndirected;
@@ -1080,6 +1090,7 @@ impl EdgeMode for AllowUndirected
1080
1090
  {
1081
1091
  const ALLOW_UNDIRECTED: bool = true;
1082
1092
  const ALLOW_LABEL_ALTERNATIVE: bool = true;
1093
+ const ALLOW_PREDICATE: bool = true;
1083
1094
  }
1084
1095
 
1085
1096
  struct DisallowUndirected;
@@ -1087,6 +1098,7 @@ impl EdgeMode for DisallowUndirected
1087
1098
  {
1088
1099
  const ALLOW_UNDIRECTED: bool = false;
1089
1100
  const ALLOW_LABEL_ALTERNATIVE: bool = false;
1101
+ const ALLOW_PREDICATE: bool = false;
1090
1102
  }
1091
1103
 
1092
1104
  struct EdgePattern
@@ -1095,7 +1107,8 @@ struct EdgePattern
1095
1107
  edge_variable: Option<ast::VariableIdentifier>,
1096
1108
  labels: ast::LabelExpression,
1097
1109
  properties: Option<ast::Expression>,
1098
- recursive_range: Option<std::ops::Range<usize>>,
1110
+ predicate: Option<ast::Expression>,
1111
+ quantifier: Option<std::ops::Range<usize>>,
1099
1112
  destination_node: ast::NodePattern,
1100
1113
  }
1101
1114
 
@@ -1126,6 +1139,78 @@ fn edge_range(input: Input) -> OCPResult<Input, std::ops::Range<usize>>
1126
1139
  .parse(input)
1127
1140
  }
1128
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
+
1129
1214
  fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1130
1215
  {
1131
1216
  map_res_failure(
@@ -1145,6 +1230,8 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1145
1230
  opt(edge_range),
1146
1231
  // Optional properties map or parameter
1147
1232
  opt(properties),
1233
+ // Optional inline predicate: WHERE <expr>
1234
+ opt(preceded(token(Token::Where), expression)),
1148
1235
  token(Token::EndBracket),
1149
1236
  )),
1150
1237
  alt((
@@ -1152,22 +1239,33 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1152
1239
  map(token(Token::Minus), |_| EdgeDirection::Undirected),
1153
1240
  map(token(Token::LeftArrow), |_| EdgeDirection::Left),
1154
1241
  )),
1155
- node_pattern, // Right node
1242
+ opt(quantifier_suffix),
1243
+ node_pattern::<T>, // Right node
1156
1244
  ),
1157
- |(first_direction, edge_pattern, second_direction, destination_node)| {
1158
- 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
1159
1247
  {
1160
- Some((_, var, labels, recursive_range, properties, _)) =>
1248
+ Some((_, var, labels, recursive_range, properties, predicate, _)) =>
1161
1249
  {
1162
- (var, recursive_range, labels, properties)
1250
+ (var, recursive_range, labels, properties, predicate)
1163
1251
  }
1164
- None => (None, None, None, None),
1252
+ None => (None, None, None, None, None),
1165
1253
  };
1166
1254
  let labels = labels.unwrap_or(ast::LabelExpression::None);
1167
1255
  if !T::ALLOW_LABEL_ALTERNATIVE && !labels.is_all_inclusive()
1168
1256
  {
1169
1257
  return Err(ErrorKind::NoSingleRelationshipType);
1170
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
+ };
1171
1269
  let edge_direction = match (first_direction, second_direction)
1172
1270
  {
1173
1271
  (EdgeDirection::Right, EdgeDirection::Right)
@@ -1198,186 +1296,844 @@ fn edge_pattern<T: EdgeMode>(input: Input) -> OCPResult<Input, EdgePattern>
1198
1296
  edge_variable,
1199
1297
  labels,
1200
1298
  properties,
1299
+ predicate,
1201
1300
  destination_node,
1202
- recursive_range,
1301
+ quantifier,
1203
1302
  })
1204
1303
  },
1205
1304
  )
1206
1305
  .parse(input.clone())
1207
1306
  }
1208
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
+
1209
1354
  fn node_or_edges_patterns<T: EdgeMode>(input: Input) -> OCPResult<Input, Vec<ast::Pattern>>
1210
1355
  {
1211
- let (input, node) = node_pattern(input)?;
1356
+ let (input, node) = node_pattern::<T>(input)?;
1212
1357
 
1213
- 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)?;
1214
1363
 
1215
- if edges.is_empty()
1364
+ if elements.is_empty()
1216
1365
  {
1217
1366
  Ok((input, vec![ast::Pattern::Node(node)]))
1218
1367
  }
1219
1368
  else
1220
1369
  {
1221
1370
  let mut current_node = node;
1222
- let mut iterator = edges.into_iter().peekable();
1371
+ let mut iterator = elements.into_iter().peekable();
1223
1372
  let mut patterns = Vec::new();
1224
- while let Some(edge) = iterator.next()
1373
+ while let Some(element) = iterator.next()
1225
1374
  {
1226
- let EdgePattern {
1227
- edge_direction,
1228
- edge_variable,
1229
- labels,
1230
- properties,
1231
- mut destination_node,
1232
- recursive_range,
1233
- } = edge;
1234
- if destination_node.variable.is_none() && iterator.peek().is_some()
1375
+ match element
1235
1376
  {
1236
- 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
+ }
1237
1522
  }
1238
- let source = current_node;
1239
- current_node = destination_node.clone();
1240
- let (source, directivity, destination) = match edge_direction
1241
- {
1242
- EdgeDirection::Right => (
1243
- source,
1244
- graphcore::EdgeDirectivity::Directed,
1245
- destination_node,
1246
- ),
1247
- EdgeDirection::Left => (
1248
- destination_node,
1249
- graphcore::EdgeDirectivity::Directed,
1250
- source,
1251
- ),
1252
- EdgeDirection::Undirected => (
1253
- source,
1254
- graphcore::EdgeDirectivity::Undirected,
1255
- destination_node,
1256
- ),
1257
- };
1258
- patterns.push(ast::Pattern::Edge(ast::EdgePattern {
1259
- source,
1260
- variable: edge_variable,
1261
- directivity,
1262
- labels,
1263
- properties,
1264
- destination,
1265
- recursive_range,
1266
- }));
1267
1523
  }
1268
1524
 
1269
1525
  Ok((input, patterns))
1270
1526
  }
1271
1527
  }
1272
1528
 
1273
- 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>
1274
1539
  {
1275
- let context = &input.1;
1276
- alt((
1277
- map_res(
1278
- (ident, token(Token::Equal), node_or_edges_patterns::<T>),
1279
- |(name, _, patterns)| {
1280
- // Accept path assignment for one or more consecutive edges by combining them into a single EdgePattern.
1281
- 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
1282
1586
  {
1283
1587
  return Err(ErrorKind::PathAcceptSingleEdge);
1284
1588
  }
1285
- // Ensure all inner patterns are edges
1286
- if !patterns.iter().all(|p| matches!(p, ast::Pattern::Edge(_)))
1589
+ let ast::Pattern::Edge(e) = &qp.group[0]
1590
+ else
1287
1591
  {
1288
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;
1289
1625
  }
1290
- // Consume the patterns and extract their EdgePattern values
1291
- let mut edges: Vec<ast::EdgePattern> = patterns
1292
- .into_iter()
1293
- .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()
1294
1792
  {
1295
- ast::Pattern::Edge(e) => e,
1296
- _ => unreachable!(),
1297
- })
1298
- .collect();
1299
- // Combine edges into a single EdgePattern by taking the first source and last destination.
1300
- // Preserve an inner edge variable if exactly one inner edge is named.
1301
- let combined_variable = {
1302
- let mut found: Option<ast::VariableIdentifier> = None;
1303
- 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
1304
1887
  {
1305
- if let Some(var) = &e.variable
1888
+ return Err(ErrorKind::PathAcceptSingleEdge);
1889
+ }
1890
+ match &mut result[0]
1891
+ {
1892
+ ast::Pattern::QuantifiedPath(qp) =>
1306
1893
  {
1307
- if found.is_some()
1308
- {
1309
- // multiple inner variables; do not attempt to preserve a single one
1310
- found = None;
1311
- break;
1312
- }
1313
- else
1314
- {
1315
- found = Some(var.clone());
1316
- }
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);
1317
1925
  }
1318
1926
  }
1319
- found
1320
- };
1321
- let original_count = edges.len();
1322
- let first_edge = edges.remove(0);
1323
- let last_edge = edges.pop().unwrap_or_else(|| first_edge.clone());
1324
- // For a single-edge path, preserve that edge's labels, properties, and recursive_range.
1325
- // For multi-hop paths, compute the combined hop range (each fixed edge = 1 hop,
1326
- // var-length edge contributes its own range). Use original_count (before removes)
1327
- // to correctly distinguish single-edge (N=1) from two-edge (N=2) paths.
1328
- let (combined_labels, combined_properties, combined_recursive_range) = if original_count
1329
- == 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
1330
1947
  {
1331
- // Single edge: preserve its constraints.
1332
- (
1333
- first_edge.labels.clone(),
1334
- first_edge.properties.clone(),
1335
- first_edge.recursive_range.clone(),
1336
- )
1948
+ qp.selector = selector;
1337
1949
  }
1338
- 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
1339
1964
  {
1340
- // Multi-hop: accumulate min and max hops across all constituent edges.
1341
- // Fixed edges contribute 1 hop each; var-length edges contribute their range.
1342
- let hop_range = |e: &ast::EdgePattern| -> (usize, usize) {
1343
- 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
1344
1975
  {
1345
- Some(r) => (r.start, r.end),
1346
- None => (1, 1),
1976
+ return Err(ErrorKind::PathAcceptSingleEdge);
1347
1977
  }
1348
- };
1349
- let (mut min_total, mut max_total) = hop_range(&first_edge);
1350
- 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
1351
2008
  {
1352
- let (mn, mx) = hop_range(e);
1353
- min_total += mn;
1354
- max_total = max_total.saturating_add(mx);
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
+ }
1355
2019
  }
1356
- let (mn, mx) = hop_range(&last_edge);
1357
- min_total += mn;
1358
- max_total = max_total.saturating_add(mx);
1359
- (ast::LabelExpression::None, None, Some(min_total..max_total))
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
2078
+ {
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
+ )
2084
+ }
2085
+ }
2086
+ else
2087
+ {
2088
+ // No quantifier found (shouldn't happen if has_quantifier is true, but defensive)
2089
+ Some(0..usize::MAX)
1360
2090
  };
1361
- let combined_edge = ast::EdgePattern {
1362
- variable: combined_variable,
1363
- source: first_edge.source,
1364
- destination: last_edge.destination,
1365
- directivity: first_edge.directivity,
1366
- labels: combined_labels,
1367
- properties: combined_properties,
1368
- 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,
1369
2101
  };
1370
- 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![
1371
2112
  ast::PathPattern {
1372
2113
  variable: context.identifiers.create_variable_from_name(name),
1373
2114
  edge: combined_edge,
1374
2115
  }
1375
2116
  .into(),
1376
- ])
1377
- },
1378
- ),
1379
- node_or_edges_patterns::<T>,
1380
- ))
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
+ )
1381
2137
  .parse(input)
1382
2138
  }
1383
2139
 
@@ -1451,6 +2207,7 @@ fn match_statement(input: Input) -> OCPResult<Input, ast::Statement>
1451
2207
  ast::Pattern::Node(n) => n.variable.as_ref().map(|v| v.name().clone()),
1452
2208
  ast::Pattern::Edge(e) => e.variable.as_ref().map(|v| v.name().clone()),
1453
2209
  ast::Pattern::Path(p) => Some(p.variable.name().clone()),
2210
+ ast::Pattern::QuantifiedPath(qp) => qp.variable.as_ref().map(|v| v.name().clone()),
1454
2211
  })
1455
2212
  .collect();
1456
2213
 
@@ -2080,3 +2837,299 @@ pub fn queries(input: Input) -> OCPResult<Input, ast::Queries>
2080
2837
  {
2081
2838
  separated_list1(token(Token::Semi), query).parse(input)
2082
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
+ }