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