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
|
@@ -110,3 +110,1228 @@ fn test_parse_multiple_vector_index_creation()
|
|
|
110
110
|
expected.extend(ast::create_vector_index("idx2"));
|
|
111
111
|
compare_ast(ast, expected)
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn test_parse_edge_pattern_with_predicate()
|
|
116
|
+
{
|
|
117
|
+
let ast = parse_query("MATCH (a)-[l:LINK WHERE l.distance < 10]->(b) RETURN l");
|
|
118
|
+
// Find the MATCH statement
|
|
119
|
+
let match_stmt = ast
|
|
120
|
+
.iter()
|
|
121
|
+
.find_map(|stmt| {
|
|
122
|
+
if let oc::ast::Statement::Match(m) = stmt
|
|
123
|
+
{
|
|
124
|
+
Some(m)
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
{
|
|
128
|
+
None
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
.expect("Expected to find a MATCH statement");
|
|
132
|
+
|
|
133
|
+
assert_eq!(match_stmt.patterns.len(), 1);
|
|
134
|
+
if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
|
|
135
|
+
{
|
|
136
|
+
assert!(
|
|
137
|
+
edge_pattern.predicate.is_some(),
|
|
138
|
+
"Expected predicate to be Some"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
{
|
|
143
|
+
panic!("Expected Pattern::Edge");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn test_parse_edge_pattern_with_properties_and_predicate()
|
|
149
|
+
{
|
|
150
|
+
let ast = parse_query("MATCH (a)-[l:LINK {weight: 1} WHERE l.distance < 10]->(b) RETURN l");
|
|
151
|
+
// Find the MATCH statement
|
|
152
|
+
let match_stmt = ast
|
|
153
|
+
.iter()
|
|
154
|
+
.find_map(|stmt| {
|
|
155
|
+
if let oc::ast::Statement::Match(m) = stmt
|
|
156
|
+
{
|
|
157
|
+
Some(m)
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
{
|
|
161
|
+
None
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.expect("Expected to find a MATCH statement");
|
|
165
|
+
|
|
166
|
+
assert_eq!(match_stmt.patterns.len(), 1);
|
|
167
|
+
if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
|
|
168
|
+
{
|
|
169
|
+
assert!(
|
|
170
|
+
edge_pattern.properties.is_some(),
|
|
171
|
+
"Expected properties to be Some"
|
|
172
|
+
);
|
|
173
|
+
assert!(
|
|
174
|
+
edge_pattern.predicate.is_some(),
|
|
175
|
+
"Expected predicate to be Some"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
{
|
|
180
|
+
panic!("Expected Pattern::Edge");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[test]
|
|
185
|
+
fn test_parse_edge_pattern_without_predicate()
|
|
186
|
+
{
|
|
187
|
+
let ast = parse_query("MATCH (a)-[l]->(b) RETURN l");
|
|
188
|
+
// Find the MATCH statement
|
|
189
|
+
let match_stmt = ast
|
|
190
|
+
.iter()
|
|
191
|
+
.find_map(|stmt| {
|
|
192
|
+
if let oc::ast::Statement::Match(m) = stmt
|
|
193
|
+
{
|
|
194
|
+
Some(m)
|
|
195
|
+
}
|
|
196
|
+
else
|
|
197
|
+
{
|
|
198
|
+
None
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
.expect("Expected to find a MATCH statement");
|
|
202
|
+
|
|
203
|
+
assert_eq!(match_stmt.patterns.len(), 1);
|
|
204
|
+
if let oc::ast::Pattern::Edge(edge_pattern) = &match_stmt.patterns[0]
|
|
205
|
+
{
|
|
206
|
+
assert!(
|
|
207
|
+
edge_pattern.predicate.is_none(),
|
|
208
|
+
"Expected predicate to be None"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
{
|
|
213
|
+
panic!("Expected Pattern::Edge");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn test_parse_path_pattern_with_predicate()
|
|
219
|
+
{
|
|
220
|
+
let ast = parse_query("MATCH p = (a)-[l:LINK WHERE l.distance < 10]->(b) RETURN p");
|
|
221
|
+
// Find the MATCH statement
|
|
222
|
+
let match_stmt = ast
|
|
223
|
+
.iter()
|
|
224
|
+
.find_map(|stmt| {
|
|
225
|
+
if let oc::ast::Statement::Match(m) = stmt
|
|
226
|
+
{
|
|
227
|
+
Some(m)
|
|
228
|
+
}
|
|
229
|
+
else
|
|
230
|
+
{
|
|
231
|
+
None
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.expect("Expected to find a MATCH statement");
|
|
235
|
+
|
|
236
|
+
assert_eq!(match_stmt.patterns.len(), 1);
|
|
237
|
+
if let oc::ast::Pattern::Path(path_pattern) = &match_stmt.patterns[0]
|
|
238
|
+
{
|
|
239
|
+
assert!(
|
|
240
|
+
path_pattern.edge.predicate.is_some(),
|
|
241
|
+
"Expected path edge predicate to be Some"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
{
|
|
246
|
+
panic!("Expected Pattern::Path");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn test_parse_create_with_edge_predicate_fails()
|
|
252
|
+
{
|
|
253
|
+
let result = parse("CREATE (a)-[l:LINK WHERE l.x = 1]->(b)");
|
|
254
|
+
assert!(
|
|
255
|
+
result.is_err(),
|
|
256
|
+
"Expected parsing CREATE with edge predicate to fail"
|
|
257
|
+
);
|
|
258
|
+
if let Err(ref err) = result
|
|
259
|
+
{
|
|
260
|
+
// Verify the error is CreatingWithPredicate
|
|
261
|
+
assert!(
|
|
262
|
+
err.to_string().contains("CreatingWithPredicate"),
|
|
263
|
+
"Expected CreatingWithPredicate error, got: {}",
|
|
264
|
+
err
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn test_parse_create_with_destination_node_predicate_fails()
|
|
271
|
+
{
|
|
272
|
+
let result = parse("CREATE (a)-[:LINK]->(b WHERE b.x = 1)");
|
|
273
|
+
assert!(
|
|
274
|
+
result.is_err(),
|
|
275
|
+
"Expected parsing CREATE with node predicate to fail"
|
|
276
|
+
);
|
|
277
|
+
if let Err(ref err) = result
|
|
278
|
+
{
|
|
279
|
+
// Verify the error is CreatingWithPredicate
|
|
280
|
+
assert!(
|
|
281
|
+
err.to_string().contains("CreatingWithPredicate"),
|
|
282
|
+
"Expected CreatingWithPredicate error, got: {}",
|
|
283
|
+
err
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn test_parse_conflicting_quantifiers_fails()
|
|
290
|
+
{
|
|
291
|
+
let result = parse("MATCH (a)-[:R*1..3]->{4,5}(b) RETURN a");
|
|
292
|
+
assert!(
|
|
293
|
+
result.is_err(),
|
|
294
|
+
"Expected parsing with both legacy and new quantifiers to fail"
|
|
295
|
+
);
|
|
296
|
+
if let Err(ref err) = result
|
|
297
|
+
{
|
|
298
|
+
// Verify the error is ConflictingQuantifiers
|
|
299
|
+
assert!(
|
|
300
|
+
err.to_string().contains("ConflictingQuantifiers"),
|
|
301
|
+
"Expected ConflictingQuantifiers error, got: {}",
|
|
302
|
+
err
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn test_parse_legacy_quantifier_only_succeeds()
|
|
309
|
+
{
|
|
310
|
+
let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
|
|
311
|
+
assert!(
|
|
312
|
+
result.is_ok(),
|
|
313
|
+
"Expected parsing with legacy quantifier only to succeed, got: {:?}",
|
|
314
|
+
result.err()
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[test]
|
|
319
|
+
fn test_parse_new_quantifier_only_succeeds()
|
|
320
|
+
{
|
|
321
|
+
let result = parse("MATCH (a)-[:R]->{4,5}(b) RETURN a");
|
|
322
|
+
assert!(
|
|
323
|
+
result.is_ok(),
|
|
324
|
+
"Expected parsing with new quantifier only to succeed, got: {:?}",
|
|
325
|
+
result.err()
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn test_parse_legacy_quantifier_emits_quantified_path()
|
|
331
|
+
{
|
|
332
|
+
let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
|
|
333
|
+
assert!(
|
|
334
|
+
result.is_ok(),
|
|
335
|
+
"Expected parsing legacy quantifier to succeed, got: {:?}",
|
|
336
|
+
result.err()
|
|
337
|
+
);
|
|
338
|
+
if let Ok(queries) = result
|
|
339
|
+
{
|
|
340
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
341
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
342
|
+
{
|
|
343
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
344
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
345
|
+
{
|
|
346
|
+
assert!(
|
|
347
|
+
!match_clause.patterns.is_empty(),
|
|
348
|
+
"Expected patterns in MATCH"
|
|
349
|
+
);
|
|
350
|
+
// Find the QuantifiedPath pattern
|
|
351
|
+
let has_quantified_path = match_clause.patterns.iter().any(|p| {
|
|
352
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
353
|
+
if qp.variable.is_none()
|
|
354
|
+
&& qp.quantifier == Some(1..3)
|
|
355
|
+
&& qp.group.len() == 1
|
|
356
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
357
|
+
});
|
|
358
|
+
assert!(
|
|
359
|
+
has_quantified_path,
|
|
360
|
+
"Expected QuantifiedPath pattern with quantifier Some(1..3) and group containing single Edge"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn test_parse_new_quantifier_emits_quantified_path()
|
|
369
|
+
{
|
|
370
|
+
let result = parse("MATCH (a)-[:R]->{1,3}(b) RETURN a");
|
|
371
|
+
assert!(
|
|
372
|
+
result.is_ok(),
|
|
373
|
+
"Expected parsing new quantifier to succeed, got: {:?}",
|
|
374
|
+
result.err()
|
|
375
|
+
);
|
|
376
|
+
if let Ok(queries) = result
|
|
377
|
+
{
|
|
378
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
379
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
380
|
+
{
|
|
381
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
382
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
383
|
+
{
|
|
384
|
+
assert!(
|
|
385
|
+
!match_clause.patterns.is_empty(),
|
|
386
|
+
"Expected patterns in MATCH"
|
|
387
|
+
);
|
|
388
|
+
// Find the QuantifiedPath pattern
|
|
389
|
+
let has_quantified_path = match_clause.patterns.iter().any(|p| {
|
|
390
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
391
|
+
if qp.variable.is_none()
|
|
392
|
+
&& qp.quantifier == Some(1..3)
|
|
393
|
+
&& qp.group.len() == 1
|
|
394
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
395
|
+
});
|
|
396
|
+
assert!(
|
|
397
|
+
has_quantified_path,
|
|
398
|
+
"Expected QuantifiedPath pattern with quantifier Some(1..3) and group containing single Edge"
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
fn test_parse_quantified_edge_chain_preserves_continuity()
|
|
407
|
+
{
|
|
408
|
+
let result = parse("MATCH (a)-[:R]->(b)-[:S*1..2]->(c) RETURN a");
|
|
409
|
+
assert!(
|
|
410
|
+
result.is_ok(),
|
|
411
|
+
"Expected parsing edge chain with quantified edge to succeed, got: {:?}",
|
|
412
|
+
result.err()
|
|
413
|
+
);
|
|
414
|
+
if let Ok(queries) = result
|
|
415
|
+
{
|
|
416
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
417
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
418
|
+
{
|
|
419
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
420
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
421
|
+
{
|
|
422
|
+
assert!(
|
|
423
|
+
!match_clause.patterns.is_empty(),
|
|
424
|
+
"Expected patterns in MATCH"
|
|
425
|
+
);
|
|
426
|
+
// Count patterns: should have Edge(R), QuantifiedPath(S)
|
|
427
|
+
let has_edge = match_clause
|
|
428
|
+
.patterns
|
|
429
|
+
.iter()
|
|
430
|
+
.any(|p| matches!(p, gqlparser::oc::ast::Pattern::Edge(_)));
|
|
431
|
+
let has_quantified_path = match_clause.patterns.iter().any(|p| {
|
|
432
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
433
|
+
if qp.variable.is_none()
|
|
434
|
+
&& qp.quantifier == Some(1..2)
|
|
435
|
+
&& qp.group.len() == 1
|
|
436
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
437
|
+
});
|
|
438
|
+
assert!(
|
|
439
|
+
has_edge && has_quantified_path,
|
|
440
|
+
"Expected chain to contain ordinary Edge and QuantifiedPath pattern"
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#[test]
|
|
448
|
+
fn test_parse_path_variable_quantified_legacy_quantifier()
|
|
449
|
+
{
|
|
450
|
+
let result = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p");
|
|
451
|
+
assert!(
|
|
452
|
+
result.is_ok(),
|
|
453
|
+
"Expected parsing path variable with legacy quantifier to succeed, got: {:?}",
|
|
454
|
+
result.err()
|
|
455
|
+
);
|
|
456
|
+
if let Ok(queries) = result
|
|
457
|
+
{
|
|
458
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
459
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
460
|
+
{
|
|
461
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
462
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
463
|
+
{
|
|
464
|
+
assert!(
|
|
465
|
+
!match_clause.patterns.is_empty(),
|
|
466
|
+
"Expected patterns in MATCH"
|
|
467
|
+
);
|
|
468
|
+
// Find the QuantifiedPath pattern with variable p
|
|
469
|
+
let has_quantified_path_with_variable = match_clause.patterns.iter().any(|p| {
|
|
470
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
471
|
+
if qp.variable.is_some()
|
|
472
|
+
&& qp.variable.as_ref().unwrap().name() == "p"
|
|
473
|
+
&& qp.quantifier == Some(1..3)
|
|
474
|
+
&& qp.group.len() == 1
|
|
475
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
476
|
+
});
|
|
477
|
+
assert!(
|
|
478
|
+
has_quantified_path_with_variable,
|
|
479
|
+
"Expected QuantifiedPath pattern with variable Some(p) and quantifier Some(1..3)"
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
#[test]
|
|
487
|
+
fn test_parse_path_variable_quantified_new_quantifier()
|
|
488
|
+
{
|
|
489
|
+
let result = parse("MATCH p = (a)-[:R]->{1,3}(b) RETURN p");
|
|
490
|
+
assert!(
|
|
491
|
+
result.is_ok(),
|
|
492
|
+
"Expected parsing path variable with new quantifier to succeed, got: {:?}",
|
|
493
|
+
result.err()
|
|
494
|
+
);
|
|
495
|
+
if let Ok(queries) = result
|
|
496
|
+
{
|
|
497
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
498
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
499
|
+
{
|
|
500
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
501
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
502
|
+
{
|
|
503
|
+
assert!(
|
|
504
|
+
!match_clause.patterns.is_empty(),
|
|
505
|
+
"Expected patterns in MATCH"
|
|
506
|
+
);
|
|
507
|
+
// Find the QuantifiedPath pattern with variable p
|
|
508
|
+
let has_quantified_path_with_variable = match_clause.patterns.iter().any(|p| {
|
|
509
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
510
|
+
if qp.variable.is_some()
|
|
511
|
+
&& qp.variable.as_ref().unwrap().name() == "p"
|
|
512
|
+
&& qp.quantifier == Some(1..3)
|
|
513
|
+
&& qp.group.len() == 1
|
|
514
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
515
|
+
});
|
|
516
|
+
assert!(
|
|
517
|
+
has_quantified_path_with_variable,
|
|
518
|
+
"Expected QuantifiedPath pattern with variable Some(p) and quantifier Some(1..3)"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[test]
|
|
526
|
+
fn test_parse_quantified_path_source_destination_populated()
|
|
527
|
+
{
|
|
528
|
+
let result = parse("MATCH (a)-[:R*1..3]->(b) RETURN a");
|
|
529
|
+
assert!(
|
|
530
|
+
result.is_ok(),
|
|
531
|
+
"Expected parsing quantified path to succeed, got: {:?}",
|
|
532
|
+
result.err()
|
|
533
|
+
);
|
|
534
|
+
if let Ok(queries) = result
|
|
535
|
+
{
|
|
536
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
537
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
538
|
+
{
|
|
539
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
540
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
541
|
+
{
|
|
542
|
+
assert!(
|
|
543
|
+
!match_clause.patterns.is_empty(),
|
|
544
|
+
"Expected patterns in MATCH"
|
|
545
|
+
);
|
|
546
|
+
// Find the QuantifiedPath pattern and verify source/destination are populated
|
|
547
|
+
let found_qp_with_endpoints = match_clause.patterns.iter().any(|p| {
|
|
548
|
+
if let gqlparser::oc::ast::Pattern::QuantifiedPath(qp) = p
|
|
549
|
+
{
|
|
550
|
+
// Verify that source and destination are populated
|
|
551
|
+
let qp_source_var = &qp.source.variable;
|
|
552
|
+
let qp_destination_var = &qp.destination.variable;
|
|
553
|
+
|
|
554
|
+
// Get the edge from the group to compare
|
|
555
|
+
if let Some(gqlparser::oc::ast::Pattern::Edge(edge)) = qp.group.first()
|
|
556
|
+
{
|
|
557
|
+
// The source of the quantified path should match the edge's source
|
|
558
|
+
// The destination of the quantified path should match the edge's destination
|
|
559
|
+
let source_matches = qp_source_var == &edge.source.variable;
|
|
560
|
+
let destination_matches = qp_destination_var == &edge.destination.variable;
|
|
561
|
+
return source_matches && destination_matches;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
false
|
|
565
|
+
});
|
|
566
|
+
assert!(
|
|
567
|
+
found_qp_with_endpoints,
|
|
568
|
+
"Expected QuantifiedPath with populated source/destination matching edge endpoints"
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn test_parse_path_variable_quantified_group_binding()
|
|
577
|
+
{
|
|
578
|
+
let result = parse("MATCH p = (a) ((n)-[r:KNOWS]->(m)){1,5} (b) RETURN p");
|
|
579
|
+
assert!(
|
|
580
|
+
result.is_ok(),
|
|
581
|
+
"Expected parsing path variable with quantified group to succeed, got: {:?}",
|
|
582
|
+
result.err()
|
|
583
|
+
);
|
|
584
|
+
if let Ok(queries) = result
|
|
585
|
+
{
|
|
586
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
587
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
588
|
+
{
|
|
589
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
590
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
591
|
+
{
|
|
592
|
+
assert!(
|
|
593
|
+
!match_clause.patterns.is_empty(),
|
|
594
|
+
"Expected patterns in MATCH"
|
|
595
|
+
);
|
|
596
|
+
let has_group_path_variable = match_clause.patterns.iter().any(|p| {
|
|
597
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
598
|
+
if qp.variable.as_ref().is_some_and(|v| v.name() == "p")
|
|
599
|
+
&& qp.group.len() == 1
|
|
600
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_))
|
|
601
|
+
&& qp.source.variable.as_ref().is_some_and(|v| v.name() == "a")
|
|
602
|
+
&& qp.destination.variable.as_ref().is_some_and(|v| v.name() == "b"))
|
|
603
|
+
});
|
|
604
|
+
assert!(
|
|
605
|
+
has_group_path_variable,
|
|
606
|
+
"Expected QuantifiedPath pattern with variable Some(p), one inner edge, and anchors a/b"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn test_parse_group_pattern_without_path_variable()
|
|
615
|
+
{
|
|
616
|
+
let result = parse("MATCH (a) ((n)-[r:KNOWS]->(m)){1,5} (b) RETURN a");
|
|
617
|
+
assert!(
|
|
618
|
+
result.is_ok(),
|
|
619
|
+
"Expected parsing quantified group to succeed, got: {:?}",
|
|
620
|
+
result.err()
|
|
621
|
+
);
|
|
622
|
+
if let Ok(queries) = result
|
|
623
|
+
{
|
|
624
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
625
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
626
|
+
{
|
|
627
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
628
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
629
|
+
{
|
|
630
|
+
assert!(
|
|
631
|
+
!match_clause.patterns.is_empty(),
|
|
632
|
+
"Expected patterns in MATCH"
|
|
633
|
+
);
|
|
634
|
+
let has_group = match_clause.patterns.iter().any(|p| {
|
|
635
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
636
|
+
if qp.variable.is_none()
|
|
637
|
+
&& qp.group.len() == 1
|
|
638
|
+
&& qp.source.variable.as_ref().is_some_and(|v| v.name() == "a")
|
|
639
|
+
&& qp.destination.variable.as_ref().is_some_and(|v| v.name() == "b")
|
|
640
|
+
&& matches!(&qp.group[0], gqlparser::oc::ast::Pattern::Edge(_)))
|
|
641
|
+
});
|
|
642
|
+
assert!(
|
|
643
|
+
has_group,
|
|
644
|
+
"Expected QuantifiedPath pattern for quantified group with anchors a/b and one inner edge"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#[test]
|
|
652
|
+
fn test_parse_group_pattern_with_where_clause()
|
|
653
|
+
{
|
|
654
|
+
let result = parse("MATCH (a) ((n)-[r:KNOWS]->(m) WHERE n.x > 1){1,5} (b) RETURN a");
|
|
655
|
+
assert!(
|
|
656
|
+
result.is_ok(),
|
|
657
|
+
"Expected parsing quantified group with WHERE to succeed, got: {:?}",
|
|
658
|
+
result.err()
|
|
659
|
+
);
|
|
660
|
+
if let Ok(queries) = result
|
|
661
|
+
{
|
|
662
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
663
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
664
|
+
{
|
|
665
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
666
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
667
|
+
{
|
|
668
|
+
let has_group_where = match_clause.patterns.iter().any(|p| {
|
|
669
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
670
|
+
if qp.group_where.is_some())
|
|
671
|
+
});
|
|
672
|
+
assert!(
|
|
673
|
+
has_group_where,
|
|
674
|
+
"Expected QuantifiedPath pattern with group_where Some(_)"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#[test]
|
|
682
|
+
fn test_parse_group_pattern_multiple_inner_edges()
|
|
683
|
+
{
|
|
684
|
+
let result = parse("MATCH (a) ((n)-[:R]->(m)-[:S]->(k)){1,3} (b) RETURN a");
|
|
685
|
+
assert!(
|
|
686
|
+
result.is_ok(),
|
|
687
|
+
"Expected parsing quantified group with multiple inner edges to succeed, got: {:?}",
|
|
688
|
+
result.err()
|
|
689
|
+
);
|
|
690
|
+
if let Ok(queries) = result
|
|
691
|
+
{
|
|
692
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
693
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
694
|
+
{
|
|
695
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
696
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
697
|
+
{
|
|
698
|
+
let has_two_edge_group = match_clause.patterns.iter().any(|p| {
|
|
699
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
700
|
+
if qp.group.len() == 2)
|
|
701
|
+
});
|
|
702
|
+
assert!(
|
|
703
|
+
has_two_edge_group,
|
|
704
|
+
"Expected QuantifiedPath pattern whose inner group contains two edges"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#[test]
|
|
712
|
+
fn test_parse_group_pattern_chain_continuation()
|
|
713
|
+
{
|
|
714
|
+
let result = parse("MATCH (a) ((n)-[:R]->(m)){1,3} (b)-[:S]->(c) RETURN a");
|
|
715
|
+
assert!(
|
|
716
|
+
result.is_ok(),
|
|
717
|
+
"Expected parsing quantified group followed by edge continuation to succeed, got: {:?}",
|
|
718
|
+
result.err()
|
|
719
|
+
);
|
|
720
|
+
if let Ok(queries) = result
|
|
721
|
+
{
|
|
722
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
723
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
724
|
+
{
|
|
725
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
726
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
727
|
+
{
|
|
728
|
+
assert_eq!(
|
|
729
|
+
match_clause.patterns.len(),
|
|
730
|
+
2,
|
|
731
|
+
"Expected exactly two top-level patterns: quantified group and following edge"
|
|
732
|
+
);
|
|
733
|
+
let quant = match_clause.patterns.first().and_then(|p| match p
|
|
734
|
+
{
|
|
735
|
+
gqlparser::oc::ast::Pattern::QuantifiedPath(qp) => Some(qp),
|
|
736
|
+
_ => None,
|
|
737
|
+
});
|
|
738
|
+
let edge = match_clause.patterns.get(1).and_then(|p| match p
|
|
739
|
+
{
|
|
740
|
+
gqlparser::oc::ast::Pattern::Edge(e) => Some(e),
|
|
741
|
+
_ => None,
|
|
742
|
+
});
|
|
743
|
+
assert!(
|
|
744
|
+
quant.is_some(),
|
|
745
|
+
"Expected first top-level pattern to be QuantifiedPath"
|
|
746
|
+
);
|
|
747
|
+
assert!(
|
|
748
|
+
edge.is_some(),
|
|
749
|
+
"Expected second top-level pattern to be Edge"
|
|
750
|
+
);
|
|
751
|
+
let quant = quant.unwrap();
|
|
752
|
+
let edge = edge.unwrap();
|
|
753
|
+
assert_eq!(
|
|
754
|
+
quant
|
|
755
|
+
.destination
|
|
756
|
+
.variable
|
|
757
|
+
.as_ref()
|
|
758
|
+
.map(|v| v.name().clone()),
|
|
759
|
+
Some("b".to_string()),
|
|
760
|
+
"Expected quantified group destination to be b"
|
|
761
|
+
);
|
|
762
|
+
assert_eq!(
|
|
763
|
+
edge.source.variable.as_ref().map(|v| v.name().clone()),
|
|
764
|
+
Some("b".to_string()),
|
|
765
|
+
"Expected following edge source to continue from group destination b"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
#[test]
|
|
773
|
+
fn test_parse_group_pattern_quantifier_variants()
|
|
774
|
+
{
|
|
775
|
+
for (suffix, expected) in [
|
|
776
|
+
("{1,5}", Some(1..5)),
|
|
777
|
+
("{3}", Some(3..3)),
|
|
778
|
+
("{2,}", Some(2..usize::MAX)),
|
|
779
|
+
("{,4}", Some(0..4)),
|
|
780
|
+
("+", Some(1..usize::MAX)),
|
|
781
|
+
("*", Some(0..usize::MAX)),
|
|
782
|
+
]
|
|
783
|
+
{
|
|
784
|
+
let query = format!("MATCH (a) ((n)-[:R]->(m)){suffix} (b) RETURN a");
|
|
785
|
+
let result = parse(&query);
|
|
786
|
+
assert!(
|
|
787
|
+
result.is_ok(),
|
|
788
|
+
"Expected parsing quantified group with quantifier {suffix} to succeed, got: {:?}",
|
|
789
|
+
result.err()
|
|
790
|
+
);
|
|
791
|
+
if let Ok(queries) = result
|
|
792
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
793
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
794
|
+
{
|
|
795
|
+
let found = match_clause.patterns.iter().any(|p| {
|
|
796
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
797
|
+
if qp.quantifier == expected)
|
|
798
|
+
});
|
|
799
|
+
assert!(
|
|
800
|
+
found,
|
|
801
|
+
"Expected QuantifiedPath pattern with quantifier {:?} for suffix {suffix}",
|
|
802
|
+
expected
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
#[test]
|
|
809
|
+
fn test_parse_path_variable_unquantified_edge_regression()
|
|
810
|
+
{
|
|
811
|
+
let result = parse("MATCH p = (a)-[:R]->(b) RETURN p");
|
|
812
|
+
assert!(
|
|
813
|
+
result.is_ok(),
|
|
814
|
+
"Expected parsing path variable with unquantified edge to succeed, got: {:?}",
|
|
815
|
+
result.err()
|
|
816
|
+
);
|
|
817
|
+
if let Ok(queries) = result
|
|
818
|
+
{
|
|
819
|
+
assert!(!queries.is_empty(), "Expected at least one query");
|
|
820
|
+
if let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
821
|
+
{
|
|
822
|
+
assert!(!statements.is_empty(), "Expected at least one statement");
|
|
823
|
+
if let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
824
|
+
{
|
|
825
|
+
assert!(
|
|
826
|
+
!match_clause.patterns.is_empty(),
|
|
827
|
+
"Expected patterns in MATCH"
|
|
828
|
+
);
|
|
829
|
+
// Find the Path pattern (not QuantifiedPath) with variable p
|
|
830
|
+
let has_path_pattern = match_clause.patterns.iter().any(|p| {
|
|
831
|
+
matches!(p, gqlparser::oc::ast::Pattern::Path(path_pattern)
|
|
832
|
+
if path_pattern.variable.name() == "p")
|
|
833
|
+
});
|
|
834
|
+
assert!(
|
|
835
|
+
has_path_pattern,
|
|
836
|
+
"Expected Path pattern with variable p (unquantified edges still use PathPattern)"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
#[test]
|
|
844
|
+
fn test_parse_path_variable_mixed_quantified_unquantified_chain()
|
|
845
|
+
{
|
|
846
|
+
let result = parse("MATCH p = (a)-[:R]->(b)-[:S*1..3]->(c) RETURN p");
|
|
847
|
+
assert!(
|
|
848
|
+
result.is_ok(),
|
|
849
|
+
"Expected parsing mixed quantified/unquantified chain to succeed, got: {:?}",
|
|
850
|
+
result.err()
|
|
851
|
+
);
|
|
852
|
+
// Mixed chains with quantified and unquantified edges are now accepted
|
|
853
|
+
// and combined into a single PathPattern (quantifiers on individual edges are
|
|
854
|
+
// preserved as QuantifiedPath nodes internally, but the path variable combines
|
|
855
|
+
// them). Proper semantic handling of mixed chains is deferred to Milestone 3.
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// =============================================================================
|
|
859
|
+
// Path Selector Tests (WI #655)
|
|
860
|
+
// =============================================================================
|
|
861
|
+
|
|
862
|
+
// Test 1: Every PathSelector variant applied to a quantified pattern
|
|
863
|
+
// (confirms the qp.selector = selector branch)
|
|
864
|
+
|
|
865
|
+
#[test]
|
|
866
|
+
fn test_parse_selector_all_with_quantified_pattern()
|
|
867
|
+
{
|
|
868
|
+
let result = parse("MATCH p = ALL (a)-[:R*1..3]->(b) RETURN p");
|
|
869
|
+
assert!(
|
|
870
|
+
result.is_ok(),
|
|
871
|
+
"Expected ALL selector with quantified pattern to succeed, got: {:?}",
|
|
872
|
+
result.err()
|
|
873
|
+
);
|
|
874
|
+
if let Ok(queries) = result
|
|
875
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
876
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
877
|
+
{
|
|
878
|
+
let has_all_selector = match_clause.patterns.iter().any(|p| {
|
|
879
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
880
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::All))
|
|
881
|
+
});
|
|
882
|
+
assert!(
|
|
883
|
+
has_all_selector,
|
|
884
|
+
"Expected QuantifiedPath with PathSelector::All"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
#[test]
|
|
890
|
+
fn test_parse_selector_any_with_quantified_pattern()
|
|
891
|
+
{
|
|
892
|
+
let result = parse("MATCH p = ANY (a)-[:R*1..3]->(b) RETURN p");
|
|
893
|
+
assert!(
|
|
894
|
+
result.is_ok(),
|
|
895
|
+
"Expected ANY selector with quantified pattern to succeed, got: {:?}",
|
|
896
|
+
result.err()
|
|
897
|
+
);
|
|
898
|
+
if let Ok(queries) = result
|
|
899
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
900
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
901
|
+
{
|
|
902
|
+
let has_any_selector = match_clause.patterns.iter().any(|p| {
|
|
903
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
904
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Any))
|
|
905
|
+
});
|
|
906
|
+
assert!(
|
|
907
|
+
has_any_selector,
|
|
908
|
+
"Expected QuantifiedPath with PathSelector::Any"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
#[test]
|
|
914
|
+
fn test_parse_selector_all_shortest_with_quantified_pattern()
|
|
915
|
+
{
|
|
916
|
+
let result = parse("MATCH p = ALL SHORTEST (a)-[:R*1..3]->(b) RETURN p");
|
|
917
|
+
assert!(
|
|
918
|
+
result.is_ok(),
|
|
919
|
+
"Expected ALL SHORTEST selector with quantified pattern to succeed, got: {:?}",
|
|
920
|
+
result.err()
|
|
921
|
+
);
|
|
922
|
+
if let Ok(queries) = result
|
|
923
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
924
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
925
|
+
{
|
|
926
|
+
let has_all_shortest_selector = match_clause.patterns.iter().any(|p| {
|
|
927
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
928
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::AllShortest))
|
|
929
|
+
});
|
|
930
|
+
assert!(
|
|
931
|
+
has_all_shortest_selector,
|
|
932
|
+
"Expected QuantifiedPath with PathSelector::AllShortest"
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
#[test]
|
|
938
|
+
fn test_parse_selector_shortest_k_with_quantified_pattern()
|
|
939
|
+
{
|
|
940
|
+
let result = parse("MATCH p = SHORTEST 2 (a)-[:R*1..3]->(b) RETURN p");
|
|
941
|
+
assert!(
|
|
942
|
+
result.is_ok(),
|
|
943
|
+
"Expected SHORTEST 2 selector with quantified pattern to succeed, got: {:?}",
|
|
944
|
+
result.err()
|
|
945
|
+
);
|
|
946
|
+
if let Ok(queries) = result
|
|
947
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
948
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
949
|
+
{
|
|
950
|
+
let has_shortest_selector = match_clause.patterns.iter().any(|p| {
|
|
951
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
952
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Shortest(_)))
|
|
953
|
+
});
|
|
954
|
+
assert!(
|
|
955
|
+
has_shortest_selector,
|
|
956
|
+
"Expected QuantifiedPath with PathSelector::Shortest(_)"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
#[test]
|
|
962
|
+
fn test_parse_selector_shortest_groups_with_quantified_pattern()
|
|
963
|
+
{
|
|
964
|
+
let result = parse("MATCH p = SHORTEST 3 GROUPS (a)-[:R*1..3]->(b) RETURN p");
|
|
965
|
+
assert!(
|
|
966
|
+
result.is_ok(),
|
|
967
|
+
"Expected SHORTEST 3 GROUPS selector with quantified pattern to succeed, got: {:?}",
|
|
968
|
+
result.err()
|
|
969
|
+
);
|
|
970
|
+
if let Ok(queries) = result
|
|
971
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
972
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
973
|
+
{
|
|
974
|
+
let has_shortest_groups_selector = match_clause.patterns.iter().any(|p| {
|
|
975
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
976
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::ShortestGroups(_)))
|
|
977
|
+
});
|
|
978
|
+
assert!(
|
|
979
|
+
has_shortest_groups_selector,
|
|
980
|
+
"Expected QuantifiedPath with PathSelector::ShortestGroups(_)"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Test 2: Every PathSelector variant applied to a plain, non-quantified edge
|
|
986
|
+
// (confirms the scoping-wrapper branch, quantifier: None)
|
|
987
|
+
|
|
988
|
+
#[test]
|
|
989
|
+
fn test_parse_selector_all_with_plain_edge()
|
|
990
|
+
{
|
|
991
|
+
let result = parse("MATCH ALL (a)-[:R]->(b) RETURN a");
|
|
992
|
+
assert!(
|
|
993
|
+
result.is_ok(),
|
|
994
|
+
"Expected ALL selector with plain edge to succeed, got: {:?}",
|
|
995
|
+
result.err()
|
|
996
|
+
);
|
|
997
|
+
if let Ok(queries) = result
|
|
998
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
999
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
1000
|
+
{
|
|
1001
|
+
let has_all_selector = match_clause.patterns.iter().any(|p| {
|
|
1002
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
1003
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::All)
|
|
1004
|
+
&& qp.quantifier.is_none())
|
|
1005
|
+
});
|
|
1006
|
+
assert!(
|
|
1007
|
+
has_all_selector,
|
|
1008
|
+
"Expected QuantifiedPath with PathSelector::All and quantifier: None"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
#[test]
|
|
1014
|
+
fn test_parse_selector_any_with_plain_edge()
|
|
1015
|
+
{
|
|
1016
|
+
let result = parse("MATCH ANY (a)-[:R]->(b) RETURN a");
|
|
1017
|
+
assert!(
|
|
1018
|
+
result.is_ok(),
|
|
1019
|
+
"Expected ANY selector with plain edge to succeed, got: {:?}",
|
|
1020
|
+
result.err()
|
|
1021
|
+
);
|
|
1022
|
+
if let Ok(queries) = result
|
|
1023
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
1024
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
1025
|
+
{
|
|
1026
|
+
let has_any_selector = match_clause.patterns.iter().any(|p| {
|
|
1027
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
1028
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Any)
|
|
1029
|
+
&& qp.quantifier.is_none())
|
|
1030
|
+
});
|
|
1031
|
+
assert!(
|
|
1032
|
+
has_any_selector,
|
|
1033
|
+
"Expected QuantifiedPath with PathSelector::Any and quantifier: None"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
#[test]
|
|
1039
|
+
fn test_parse_selector_all_shortest_with_plain_edge()
|
|
1040
|
+
{
|
|
1041
|
+
let result = parse("MATCH ALL SHORTEST (a)-[:R]->(b) RETURN a");
|
|
1042
|
+
assert!(
|
|
1043
|
+
result.is_ok(),
|
|
1044
|
+
"Expected ALL SHORTEST selector with plain edge to succeed, got: {:?}",
|
|
1045
|
+
result.err()
|
|
1046
|
+
);
|
|
1047
|
+
if let Ok(queries) = result
|
|
1048
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
1049
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
1050
|
+
{
|
|
1051
|
+
let has_all_shortest_selector = match_clause.patterns.iter().any(|p| {
|
|
1052
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
1053
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::AllShortest)
|
|
1054
|
+
&& qp.quantifier.is_none())
|
|
1055
|
+
});
|
|
1056
|
+
assert!(
|
|
1057
|
+
has_all_shortest_selector,
|
|
1058
|
+
"Expected QuantifiedPath with PathSelector::AllShortest and quantifier: None"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
#[test]
|
|
1064
|
+
fn test_parse_selector_shortest_k_with_plain_edge()
|
|
1065
|
+
{
|
|
1066
|
+
let result = parse("MATCH SHORTEST 1 (a)-[:R]->(b) RETURN a");
|
|
1067
|
+
assert!(
|
|
1068
|
+
result.is_ok(),
|
|
1069
|
+
"Expected SHORTEST 1 selector with plain edge to succeed, got: {:?}",
|
|
1070
|
+
result.err()
|
|
1071
|
+
);
|
|
1072
|
+
if let Ok(queries) = result
|
|
1073
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
1074
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
1075
|
+
{
|
|
1076
|
+
let has_shortest_selector = match_clause.patterns.iter().any(|p| {
|
|
1077
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
1078
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::Shortest(_))
|
|
1079
|
+
&& qp.quantifier.is_none())
|
|
1080
|
+
});
|
|
1081
|
+
assert!(
|
|
1082
|
+
has_shortest_selector,
|
|
1083
|
+
"Expected QuantifiedPath with PathSelector::Shortest(_) and quantifier: None"
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
#[test]
|
|
1089
|
+
fn test_parse_selector_shortest_groups_with_plain_edge()
|
|
1090
|
+
{
|
|
1091
|
+
let result = parse("MATCH SHORTEST 5 GROUPS (a)-[:R]->(b) RETURN a");
|
|
1092
|
+
assert!(
|
|
1093
|
+
result.is_ok(),
|
|
1094
|
+
"Expected SHORTEST 5 GROUPS selector with plain edge to succeed, got: {:?}",
|
|
1095
|
+
result.err()
|
|
1096
|
+
);
|
|
1097
|
+
if let Ok(queries) = result
|
|
1098
|
+
&& let gqlparser::oc::ast::Query::Statements(statements) = &queries[0]
|
|
1099
|
+
&& let gqlparser::oc::ast::Statement::Match(match_clause) = &statements[0]
|
|
1100
|
+
{
|
|
1101
|
+
let has_shortest_groups_selector = match_clause.patterns.iter().any(|p| {
|
|
1102
|
+
matches!(p, gqlparser::oc::ast::Pattern::QuantifiedPath(qp)
|
|
1103
|
+
if matches!(qp.selector, gqlparser::oc::ast::PathSelector::ShortestGroups(_))
|
|
1104
|
+
&& qp.quantifier.is_none())
|
|
1105
|
+
});
|
|
1106
|
+
assert!(
|
|
1107
|
+
has_shortest_groups_selector,
|
|
1108
|
+
"Expected QuantifiedPath with PathSelector::ShortestGroups(_) and quantifier: None"
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Test 3: Error cases - selector with no pattern following or with bare node
|
|
1114
|
+
|
|
1115
|
+
#[test]
|
|
1116
|
+
fn test_parse_selector_no_pattern_should_error()
|
|
1117
|
+
{
|
|
1118
|
+
let result = parse("MATCH SHORTEST 1 RETURN a");
|
|
1119
|
+
assert!(
|
|
1120
|
+
result.is_err(),
|
|
1121
|
+
"Expected SHORTEST 1 with no pattern to error, got: {:?}",
|
|
1122
|
+
result
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
#[test]
|
|
1127
|
+
fn test_parse_selector_with_bare_node_should_error()
|
|
1128
|
+
{
|
|
1129
|
+
let result = parse("MATCH SHORTEST 1 (a) RETURN a");
|
|
1130
|
+
assert!(
|
|
1131
|
+
result.is_err(),
|
|
1132
|
+
"Expected SHORTEST 1 with bare node to error, got: {:?}",
|
|
1133
|
+
result
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Test 4: Regression - verify selector-free tests still pass
|
|
1138
|
+
// (Each test individually verifies that existing functionality is not broken)
|
|
1139
|
+
|
|
1140
|
+
#[test]
|
|
1141
|
+
fn test_selector_regression_simple_match()
|
|
1142
|
+
{
|
|
1143
|
+
let result = parse("MATCH (a) RETURN a");
|
|
1144
|
+
assert!(
|
|
1145
|
+
result.is_ok(),
|
|
1146
|
+
"Regression: Expected simple MATCH to still parse, got: {:?}",
|
|
1147
|
+
result.err()
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
#[test]
|
|
1152
|
+
fn test_selector_regression_edge_match()
|
|
1153
|
+
{
|
|
1154
|
+
let result = parse("MATCH (a)-[:R]->(b) RETURN a");
|
|
1155
|
+
assert!(
|
|
1156
|
+
result.is_ok(),
|
|
1157
|
+
"Regression: Expected edge MATCH to still parse, got: {:?}",
|
|
1158
|
+
result.err()
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
#[test]
|
|
1163
|
+
fn test_selector_regression_quantified_pattern()
|
|
1164
|
+
{
|
|
1165
|
+
let result = parse("MATCH p = (a)-[:R*1..3]->(b) RETURN p");
|
|
1166
|
+
assert!(
|
|
1167
|
+
result.is_ok(),
|
|
1168
|
+
"Regression: Expected quantified pattern to still parse, got: {:?}",
|
|
1169
|
+
result.err()
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
#[test]
|
|
1174
|
+
fn test_selector_regression_path_variable()
|
|
1175
|
+
{
|
|
1176
|
+
let result = parse("MATCH p = (a)-[:R]->(b) RETURN p");
|
|
1177
|
+
assert!(
|
|
1178
|
+
result.is_ok(),
|
|
1179
|
+
"Regression: Expected path variable to still parse, got: {:?}",
|
|
1180
|
+
result.err()
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// WI689 Tests: Empty quantified groups and terminal node tracking
|
|
1185
|
+
|
|
1186
|
+
#[test]
|
|
1187
|
+
fn test_empty_quantified_group_should_error()
|
|
1188
|
+
{
|
|
1189
|
+
let result = parse("MATCH (a) ((n)){1,5} (b) RETURN a");
|
|
1190
|
+
assert!(
|
|
1191
|
+
result.is_err(),
|
|
1192
|
+
"Expected empty quantified group to error, got: {:?}",
|
|
1193
|
+
result
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
#[test]
|
|
1198
|
+
fn test_quantified_group_with_unnamed_terminal_node()
|
|
1199
|
+
{
|
|
1200
|
+
let result = parse("MATCH (a) ((n)-[r]->()) {1,5} (b) RETURN a");
|
|
1201
|
+
assert!(
|
|
1202
|
+
result.is_ok(),
|
|
1203
|
+
"Expected quantified group with unnamed terminal node to parse, got: {:?}",
|
|
1204
|
+
result.err()
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
// Verify that the edge's destination now has a variable assigned
|
|
1208
|
+
let queries = result.unwrap();
|
|
1209
|
+
let oc::ast::Query::Statements(ast) = queries.into_iter().next().unwrap()
|
|
1210
|
+
else
|
|
1211
|
+
{
|
|
1212
|
+
panic!("Expected Statements query");
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// The AST should contain a QuantifiedPath pattern
|
|
1216
|
+
let found_qp = ast.iter().any(|stmt| {
|
|
1217
|
+
if let oc::ast::Statement::Match(match_stmt) = stmt
|
|
1218
|
+
{
|
|
1219
|
+
match_stmt.patterns.iter().any(|p| {
|
|
1220
|
+
if let oc::ast::Pattern::QuantifiedPath(qp) = p
|
|
1221
|
+
{
|
|
1222
|
+
// The last edge in the group should have a destination variable
|
|
1223
|
+
qp.group
|
|
1224
|
+
.iter()
|
|
1225
|
+
.rev()
|
|
1226
|
+
.find_map(|p| {
|
|
1227
|
+
if let oc::ast::Pattern::Edge(e) = p
|
|
1228
|
+
{
|
|
1229
|
+
e.destination.variable.is_some().then_some(())
|
|
1230
|
+
}
|
|
1231
|
+
else
|
|
1232
|
+
{
|
|
1233
|
+
None
|
|
1234
|
+
}
|
|
1235
|
+
})
|
|
1236
|
+
.is_some()
|
|
1237
|
+
}
|
|
1238
|
+
else
|
|
1239
|
+
{
|
|
1240
|
+
false
|
|
1241
|
+
}
|
|
1242
|
+
})
|
|
1243
|
+
}
|
|
1244
|
+
else
|
|
1245
|
+
{
|
|
1246
|
+
false
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
assert!(
|
|
1251
|
+
found_qp,
|
|
1252
|
+
"Expected to find a quantified path with a terminal variable"
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
#[test]
|
|
1257
|
+
fn test_quantified_group_with_named_terminal_node_preserved()
|
|
1258
|
+
{
|
|
1259
|
+
let result = parse("MATCH (a) ((n)-[r]->(m)){1,5} (b) RETURN a");
|
|
1260
|
+
assert!(
|
|
1261
|
+
result.is_ok(),
|
|
1262
|
+
"Expected quantified group with named terminal node to parse, got: {:?}",
|
|
1263
|
+
result.err()
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
// Verify that the named variable is preserved
|
|
1267
|
+
let queries = result.unwrap();
|
|
1268
|
+
let oc::ast::Query::Statements(ast) = queries.into_iter().next().unwrap()
|
|
1269
|
+
else
|
|
1270
|
+
{
|
|
1271
|
+
panic!("Expected Statements query");
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// The AST should contain a QuantifiedPath pattern with the correct variable name
|
|
1275
|
+
let found_qp = ast.iter().any(|stmt| {
|
|
1276
|
+
if let oc::ast::Statement::Match(match_stmt) = stmt
|
|
1277
|
+
{
|
|
1278
|
+
match_stmt.patterns.iter().any(|p| {
|
|
1279
|
+
if let oc::ast::Pattern::QuantifiedPath(qp) = p
|
|
1280
|
+
{
|
|
1281
|
+
// The last edge in the group should have a destination variable named "m"
|
|
1282
|
+
qp.group
|
|
1283
|
+
.iter()
|
|
1284
|
+
.rev()
|
|
1285
|
+
.find_map(|p| {
|
|
1286
|
+
if let oc::ast::Pattern::Edge(e) = p
|
|
1287
|
+
{
|
|
1288
|
+
e.destination
|
|
1289
|
+
.variable
|
|
1290
|
+
.as_ref()
|
|
1291
|
+
.and_then(|v| if v.name() == "m" { Some(()) } else { None })
|
|
1292
|
+
}
|
|
1293
|
+
else
|
|
1294
|
+
{
|
|
1295
|
+
None
|
|
1296
|
+
}
|
|
1297
|
+
})
|
|
1298
|
+
.is_some()
|
|
1299
|
+
}
|
|
1300
|
+
else
|
|
1301
|
+
{
|
|
1302
|
+
false
|
|
1303
|
+
}
|
|
1304
|
+
})
|
|
1305
|
+
}
|
|
1306
|
+
else
|
|
1307
|
+
{
|
|
1308
|
+
false
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
assert!(
|
|
1313
|
+
found_qp,
|
|
1314
|
+
"Expected to find a quantified path with terminal variable named 'm'"
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
#[test]
|
|
1319
|
+
fn test_parse_group_single_edge_range()
|
|
1320
|
+
{
|
|
1321
|
+
let ast = parse_query("MATCH (a) ((n)-[:R]->(m)){1,3} (b) RETURN a, b");
|
|
1322
|
+
compare_ast(ast, ast::group_single_edge_range())
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
#[test]
|
|
1326
|
+
fn test_parse_group_two_edges_range()
|
|
1327
|
+
{
|
|
1328
|
+
let ast = parse_query("MATCH (a) ((n)-[:R1]->(x)-[:R2]->(m)){1,3} (b) RETURN a, b");
|
|
1329
|
+
compare_ast(ast, ast::group_two_edges_range())
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
#[test]
|
|
1333
|
+
fn test_parse_shortest_single_edge_full_return()
|
|
1334
|
+
{
|
|
1335
|
+
let ast = parse_query("MATCH p = SHORTEST 1 (a)-[:R]-+(b) RETURN a, b, length(p)");
|
|
1336
|
+
compare_ast(ast, ast::shortest_single_edge_full_return())
|
|
1337
|
+
}
|