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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +8 -8
- data/ext/gqlitedb/Cargo.toml +0 -2
- data/ext/gqlitedb/src/compiler/variables_manager.rs +109 -10
- data/ext/gqlitedb/src/compiler.rs +481 -47
- data/ext/gqlitedb/src/connection.rs +12 -0
- data/ext/gqlitedb/src/consts.rs +1 -1
- data/ext/gqlitedb/src/error.rs +24 -0
- data/ext/gqlitedb/src/interpreter/evaluators.rs +91 -325
- data/ext/gqlitedb/src/interpreter/instructions.rs +72 -0
- data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
- data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
- data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
- data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
- data/ext/gqlitedb/src/interpreter/path.rs +542 -0
- data/ext/gqlitedb/src/planner.rs +218 -6
- data/ext/gqlitedb/src/query_result.rs +19 -4
- data/ext/gqlitedb/src/tests/compiler.rs +2453 -1
- data/ext/gqlitedb/src/tests/connection/redb.rs +8 -0
- data/ext/gqlitedb/src/tests/connection.rs +37 -0
- data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
- data/ext/gqlitedb/src/tests/parser.rs +1225 -0
- data/ext/gqlitedb/src/tests/planner.rs +432 -2
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
- data/ext/gqlitedb/src/tests/templates/ast.rs +284 -1
- data/ext/gqlitedb/src/tests/templates/programs.rs +306 -0
- data/ext/gqliterb/Cargo.toml +0 -1
- data/ext/gqlparser/src/oc/ast.rs +52 -3
- data/ext/gqlparser/src/oc/error.rs +18 -0
- data/ext/gqlparser/src/oc/lexer.rs +146 -0
- data/ext/gqlparser/src/oc/parser/tests.rs +365 -20
- data/ext/gqlparser/src/oc/parser.rs +1196 -143
- data/ext/graphcore/src/value.rs +12 -1
- metadata +5 -2
- 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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
1371
|
+
let mut iterator = elements.into_iter().peekable();
|
|
1223
1372
|
let mut patterns = Vec::new();
|
|
1224
|
-
while let Some(
|
|
1373
|
+
while let Some(element) = iterator.next()
|
|
1225
1374
|
{
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
.
|
|
1293
|
-
|
|
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::
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
1888
|
+
return Err(ErrorKind::PathAcceptSingleEdge);
|
|
1889
|
+
}
|
|
1890
|
+
match &mut result[0]
|
|
1891
|
+
{
|
|
1892
|
+
ast::Pattern::QuantifiedPath(qp) =>
|
|
1306
1893
|
{
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1346
|
-
None => (1, 1),
|
|
1976
|
+
return Err(ErrorKind::PathAcceptSingleEdge);
|
|
1347
1977
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|