gqlite 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/gqlitedb/Cargo.toml +0 -2
  4. data/ext/gqlitedb/src/compiler/variables_manager.rs +109 -10
  5. data/ext/gqlitedb/src/compiler.rs +481 -47
  6. data/ext/gqlitedb/src/connection.rs +12 -0
  7. data/ext/gqlitedb/src/consts.rs +1 -1
  8. data/ext/gqlitedb/src/error.rs +24 -0
  9. data/ext/gqlitedb/src/interpreter/evaluators.rs +91 -325
  10. data/ext/gqlitedb/src/interpreter/instructions.rs +72 -0
  11. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  12. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  13. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  14. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  15. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  16. data/ext/gqlitedb/src/planner.rs +218 -6
  17. data/ext/gqlitedb/src/query_result.rs +19 -4
  18. data/ext/gqlitedb/src/tests/compiler.rs +2453 -1
  19. data/ext/gqlitedb/src/tests/connection/redb.rs +8 -0
  20. data/ext/gqlitedb/src/tests/connection.rs +37 -0
  21. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  22. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  23. data/ext/gqlitedb/src/tests/planner.rs +432 -2
  24. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  25. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  26. data/ext/gqlitedb/src/tests/templates/ast.rs +284 -1
  27. data/ext/gqlitedb/src/tests/templates/programs.rs +306 -0
  28. data/ext/gqliterb/Cargo.toml +0 -1
  29. data/ext/gqlparser/src/oc/ast.rs +52 -3
  30. data/ext/gqlparser/src/oc/error.rs +18 -0
  31. data/ext/gqlparser/src/oc/lexer.rs +146 -0
  32. data/ext/gqlparser/src/oc/parser/tests.rs +365 -20
  33. data/ext/gqlparser/src/oc/parser.rs +1196 -143
  34. data/ext/graphcore/src/value.rs +12 -1
  35. metadata +5 -2
  36. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -6,6 +6,52 @@ use gqlparser::oc::ast;
6
6
  /// Ordered list of logical plans.
7
7
  pub type LogicalPlans = Vec<LogicalPlan>;
8
8
 
9
+ #[derive(Debug, Clone, PartialEq)]
10
+ pub enum StopCondition
11
+ {
12
+ Shortest(ast::Expression),
13
+ ShortestGroups(ast::Expression),
14
+ }
15
+
16
+ #[derive(Debug, Clone, PartialEq)]
17
+ pub enum PathStrategy
18
+ {
19
+ Dfs,
20
+ Bfs
21
+ {
22
+ stop_condition: StopCondition,
23
+ },
24
+ }
25
+
26
+ /// A pattern the planner has finished with. `Node`/`Edge`/`Path` pass
27
+ /// through unchanged — there's nothing to plan for them yet.
28
+ /// `QuantifiedPath` carries the planner's own decision alongside the
29
+ /// parsed pattern, so the compiler can never see one without the other.
30
+ #[derive(Debug)]
31
+ #[allow(clippy::large_enum_variant)]
32
+ pub enum PlannedPattern
33
+ {
34
+ Node(ast::NodePattern),
35
+ Edge(ast::EdgePattern),
36
+ Path(ast::PathPattern),
37
+ QuantifiedPath
38
+ {
39
+ pattern: ast::QuantifiedPathPattern,
40
+ strategy: PathStrategy,
41
+ },
42
+ }
43
+
44
+ impl PlannedPattern
45
+ {
46
+ /// Convenience for call sites (mainly test fixtures) that just want to
47
+ /// carry an ordinary pattern through planning without constructing a
48
+ /// `PlannedPattern` variant by hand.
49
+ pub fn unplanned(pattern: ast::Pattern) -> Self
50
+ {
51
+ plan_pattern(pattern)
52
+ }
53
+ }
54
+
9
55
  /// Logical representation of a statement.
10
56
  #[derive(Debug)]
11
57
  #[allow(clippy::large_enum_variant)]
@@ -37,13 +83,13 @@ pub enum LogicalPlan
37
83
  Create
38
84
  {
39
85
  /// Patterns to create.
40
- patterns: Vec<ast::Pattern>,
86
+ patterns: Vec<PlannedPattern>,
41
87
  },
42
88
  /// Direct node scan produced from a MATCH clause.
43
89
  NodeScan
44
90
  {
45
91
  /// Patterns to match, including any label constraints present in the AST.
46
- patterns: Vec<ast::Pattern>,
92
+ patterns: Vec<PlannedPattern>,
47
93
  /// Whether the scan is optional.
48
94
  optional: bool,
49
95
  },
@@ -129,7 +175,7 @@ pub enum LogicalPlan
129
175
  Merge
130
176
  {
131
177
  /// Patterns to match or create.
132
- patterns: Vec<ast::Pattern>,
178
+ patterns: Vec<PlannedPattern>,
133
179
  /// Updates applied when entities are created.
134
180
  on_create: Vec<ast::OneUpdate>,
135
181
  /// Updates applied when entities are matched.
@@ -189,6 +235,172 @@ fn wrap_filter(plan: LogicalPlan, where_expression: Option<ast::Expression>) ->
189
235
  }
190
236
  }
191
237
 
238
+ fn one_literal() -> ast::Expression
239
+ {
240
+ ast::Value { value: 1i64.into() }.into()
241
+ }
242
+
243
+ pub(crate) fn choose_path_strategy(selector: &ast::PathSelector) -> PathStrategy
244
+ {
245
+ match selector
246
+ {
247
+ ast::PathSelector::All => PathStrategy::Dfs,
248
+ ast::PathSelector::Any => PathStrategy::Bfs {
249
+ stop_condition: StopCondition::Shortest(one_literal()),
250
+ },
251
+ ast::PathSelector::AllShortest => PathStrategy::Bfs {
252
+ stop_condition: StopCondition::ShortestGroups(one_literal()),
253
+ },
254
+ ast::PathSelector::Shortest(k) => PathStrategy::Bfs {
255
+ stop_condition: StopCondition::Shortest(k.clone()),
256
+ },
257
+ ast::PathSelector::ShortestGroups(k) => PathStrategy::Bfs {
258
+ stop_condition: StopCondition::ShortestGroups(k.clone()),
259
+ },
260
+ }
261
+ }
262
+
263
+ /// The one place `choose_path_strategy` is called from. Every pattern
264
+ /// that becomes part of a `LogicalPlan` goes through this.
265
+ pub(crate) fn plan_pattern(pattern: ast::Pattern) -> PlannedPattern
266
+ {
267
+ match pattern
268
+ {
269
+ ast::Pattern::Node(n) => PlannedPattern::Node(n),
270
+ ast::Pattern::Edge(e) => PlannedPattern::Edge(e),
271
+ ast::Pattern::Path(p) => PlannedPattern::Path(p),
272
+ ast::Pattern::QuantifiedPath(qp) if qp.quantifier.is_none() =>
273
+ {
274
+ // Selector-scoped pre-filter wrapper (design §1d): promote the
275
+ // inner content to a degenerate single-repetition quantified
276
+ // pattern if it isn't already quantified, then apply the outer
277
+ // selector's strategy to the result. The inner content's own
278
+ // `.selector` is always `All` in this construct (nothing in the
279
+ // grammar lets it be otherwise), so this is a plain override, not
280
+ // a merge.
281
+ let inner = qp
282
+ .group
283
+ .into_iter()
284
+ .next()
285
+ .expect("selector-scoped wrapper must have exactly one inner pattern");
286
+ let strategy = choose_path_strategy(&qp.selector);
287
+ match inner
288
+ {
289
+ ast::Pattern::QuantifiedPath(mut inner_qp) =>
290
+ {
291
+ // Already quantified (e.g. `--+`) — keep its own quantifier,
292
+ // just override the strategy. The wrapper's predicate becomes
293
+ // path_where (whole-path filter), not group_where (per-repetition).
294
+ inner_qp.variable = qp.variable.or(inner_qp.variable);
295
+ inner_qp.path_where = qp.group_where; // Wrapper predicate → path_where
296
+ PlannedPattern::QuantifiedPath {
297
+ pattern: inner_qp,
298
+ strategy,
299
+ }
300
+ }
301
+ other =>
302
+ {
303
+ // Not already quantified — promote to a degenerate
304
+ // exactly-one-repetition group. The wrapper's predicate becomes
305
+ // path_where (whole-path filter), not group_where (per-repetition).
306
+ let promoted = ast::QuantifiedPathPattern {
307
+ variable: qp.variable,
308
+ source: qp.source,
309
+ group: vec![other],
310
+ group_where: None, // Wrapper-promoted patterns never have group_where
311
+ path_where: qp.group_where, // Wrapper predicate → path_where
312
+ quantifier: Some(1..1),
313
+ destination: qp.destination,
314
+ selector: qp.selector,
315
+ };
316
+ PlannedPattern::QuantifiedPath {
317
+ pattern: promoted,
318
+ strategy,
319
+ }
320
+ }
321
+ }
322
+ }
323
+ ast::Pattern::QuantifiedPath(qp) if !matches!(qp.selector, ast::PathSelector::All) =>
324
+ {
325
+ // Selector-scoped pattern with selector override (e.g., SHORTEST 1).
326
+ // Two cases:
327
+ // 1. quantifier.is_none(): pre-filter wrapper with no inner quantifier,
328
+ // needs promotion to 1..1 with inner pattern in group
329
+ // 2. quantifier.is_some(): already-quantified inner (like --+), wrapped
330
+ // with a selector override. The quantifier is already set; just
331
+ // apply the selector's strategy. Also move group_where to path_where
332
+ // (whole-path filter) if present.
333
+ let strategy = choose_path_strategy(&qp.selector);
334
+
335
+ if qp.quantifier.is_some()
336
+ {
337
+ // Already has a quantifier (e.g., from --+ operator).
338
+ // Move group_where to path_where if present, then apply the strategy.
339
+ let mut pattern = qp;
340
+ if pattern.group_where.is_some()
341
+ {
342
+ pattern.path_where = pattern.group_where.take();
343
+ }
344
+ PlannedPattern::QuantifiedPath { pattern, strategy }
345
+ }
346
+ else
347
+ {
348
+ // Pre-filter wrapper with no inner quantifier.
349
+ // Extract and process the inner pattern.
350
+ let inner = qp
351
+ .group
352
+ .into_iter()
353
+ .next()
354
+ .expect("selector-scoped wrapper must have exactly one inner pattern");
355
+ match inner
356
+ {
357
+ ast::Pattern::QuantifiedPath(mut inner_qp) =>
358
+ {
359
+ // Already quantified — keep its own quantifier and override the strategy.
360
+ inner_qp.variable = qp.variable.or(inner_qp.variable);
361
+ inner_qp.path_where = qp.group_where; // Wrapper predicate → path_where
362
+ PlannedPattern::QuantifiedPath {
363
+ pattern: inner_qp,
364
+ strategy,
365
+ }
366
+ }
367
+ other =>
368
+ {
369
+ // Not already quantified — promote to a degenerate 1..1 group.
370
+ let promoted = ast::QuantifiedPathPattern {
371
+ variable: qp.variable,
372
+ source: qp.source,
373
+ group: vec![other],
374
+ group_where: None,
375
+ path_where: qp.group_where,
376
+ quantifier: Some(1..1),
377
+ destination: qp.destination,
378
+ selector: qp.selector,
379
+ };
380
+ PlannedPattern::QuantifiedPath {
381
+ pattern: promoted,
382
+ strategy,
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ ast::Pattern::QuantifiedPath(qp) =>
389
+ {
390
+ let strategy = choose_path_strategy(&qp.selector);
391
+ PlannedPattern::QuantifiedPath {
392
+ pattern: qp,
393
+ strategy,
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ pub(crate) fn plan_patterns(patterns: Vec<ast::Pattern>) -> Vec<PlannedPattern>
400
+ {
401
+ patterns.into_iter().map(plan_pattern).collect()
402
+ }
403
+
192
404
  fn wrap_modifiers(plan: LogicalPlan, modifiers: ast::Modifiers) -> LogicalPlan
193
405
  {
194
406
  let mut plan = plan;
@@ -266,7 +478,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
266
478
  name: use_graph.name,
267
479
  }),
268
480
  ast::Statement::Create(create) => Ok(LogicalPlan::Create {
269
- patterns: create.patterns,
481
+ patterns: plan_patterns(create.patterns),
270
482
  }),
271
483
  ast::Statement::Match(match_statement) =>
272
484
  {
@@ -289,7 +501,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
289
501
  // Standard node scan when no SEARCH
290
502
  Ok(wrap_filter(
291
503
  LogicalPlan::NodeScan {
292
- patterns: match_statement.patterns,
504
+ patterns: plan_patterns(match_statement.patterns),
293
505
  optional: match_statement.optional,
294
506
  },
295
507
  match_statement.where_expression,
@@ -326,7 +538,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
326
538
  updates: update_statement.updates,
327
539
  }),
328
540
  ast::Statement::Merge(merge_statement) => Ok(LogicalPlan::Merge {
329
- patterns: merge_statement.patterns,
541
+ patterns: plan_patterns(merge_statement.patterns),
330
542
  on_create: merge_statement.on_create,
331
543
  on_match: merge_statement.on_match,
332
544
  }),
@@ -1,7 +1,7 @@
1
1
  use graphcore::value_map;
2
2
  use itertools::Itertools as _;
3
3
 
4
- use crate::error::InternalError;
4
+ use crate::error::Error;
5
5
 
6
6
  /// Hold the result of executing a query.
7
7
  #[derive(Debug, PartialEq, Clone)]
@@ -31,10 +31,25 @@ impl QueryResult
31
31
  }
32
32
  }
33
33
  /// Try convert into table
34
- pub fn try_into_table(self) -> Result<graphcore::Table, InternalError>
34
+ pub fn try_into_table(self) -> Result<graphcore::Table, Error>
35
35
  {
36
36
  self.try_into()
37
37
  }
38
+
39
+ /// This is a convenience method for queries like `RETURN count(*) AS n`.
40
+ pub fn into_single_value<T>(self) -> Result<T, Error>
41
+ where
42
+ T: TryFrom<graphcore::Value>,
43
+ Error: From<<T as TryFrom<graphcore::Value>>::Error>,
44
+ {
45
+ let table = match self
46
+ {
47
+ QueryResult::Table(tbl) => tbl,
48
+ _ => return Err(crate::error::InternalError::InvalidQueryResultCast.into()),
49
+ };
50
+ let value = table.value(0, 0)?;
51
+ Ok(value.clone().try_into()?)
52
+ }
38
53
  }
39
54
 
40
55
  impl From<graphcore::Value> for QueryResult
@@ -55,13 +70,13 @@ impl From<graphcore::Table> for QueryResult
55
70
 
56
71
  impl TryInto<graphcore::Table> for QueryResult
57
72
  {
58
- type Error = InternalError;
73
+ type Error = Error;
59
74
  fn try_into(self) -> Result<graphcore::Table, Self::Error>
60
75
  {
61
76
  match self
62
77
  {
63
78
  QueryResult::Table(tbl) => Ok(tbl),
64
- _ => Err(InternalError::InvalidQueryResultCast),
79
+ _ => Err(crate::error::InternalError::InvalidQueryResultCast.into()),
65
80
  }
66
81
  }
67
82
  }