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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/db-index/Cargo.toml +2 -2
  4. data/ext/gqlitedb/Cargo.toml +0 -2
  5. data/ext/gqlitedb/src/aggregators.rs +28 -0
  6. data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
  7. data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
  8. data/ext/gqlitedb/src/compiler.rs +506 -48
  9. data/ext/gqlitedb/src/connection.rs +14 -2
  10. data/ext/gqlitedb/src/consts.rs +1 -1
  11. data/ext/gqlitedb/src/error.rs +31 -0
  12. data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
  13. data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
  14. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  15. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  16. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  17. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  18. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  19. data/ext/gqlitedb/src/planner.rs +231 -6
  20. data/ext/gqlitedb/src/query_result.rs +19 -4
  21. data/ext/gqlitedb/src/store/redb.rs +1 -1
  22. data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
  23. data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
  24. data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
  25. data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
  26. data/ext/gqlitedb/src/tests/connection.rs +101 -0
  27. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  28. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  29. data/ext/gqlitedb/src/tests/planner.rs +433 -2
  30. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  31. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  32. data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
  33. data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
  34. data/ext/gqliterb/Cargo.toml +0 -1
  35. data/ext/gqlparser/src/oc/ast.rs +72 -3
  36. data/ext/gqlparser/src/oc/error.rs +18 -0
  37. data/ext/gqlparser/src/oc/lexer.rs +157 -2
  38. data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
  39. data/ext/gqlparser/src/oc/parser.rs +1312 -182
  40. data/ext/gqlparser/src/oc/string.rs +172 -0
  41. data/ext/gqlparser/src/oc.rs +1 -0
  42. data/ext/graphcore/src/value/value_map.rs +2 -2
  43. data/ext/graphcore/src/value.rs +13 -1
  44. metadata +6 -2
  45. 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
  },
@@ -89,6 +135,12 @@ pub enum LogicalPlan
89
135
  /// Limit expression.
90
136
  expression: ast::Expression,
91
137
  },
138
+ /// Make sure values only appear onces.
139
+ Distinct
140
+ {
141
+ /// Input operator.
142
+ source: Box<LogicalPlan>,
143
+ },
92
144
  /// Invoke a callable.
93
145
  Call
94
146
  {
@@ -123,7 +175,7 @@ pub enum LogicalPlan
123
175
  Merge
124
176
  {
125
177
  /// Patterns to match or create.
126
- patterns: Vec<ast::Pattern>,
178
+ patterns: Vec<PlannedPattern>,
127
179
  /// Updates applied when entities are created.
128
180
  on_create: Vec<ast::OneUpdate>,
129
181
  /// Updates applied when entities are matched.
@@ -183,6 +235,172 @@ fn wrap_filter(plan: LogicalPlan, where_expression: Option<ast::Expression>) ->
183
235
  }
184
236
  }
185
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
+
186
404
  fn wrap_modifiers(plan: LogicalPlan, modifiers: ast::Modifiers) -> LogicalPlan
187
405
  {
188
406
  let mut plan = plan;
@@ -211,6 +429,13 @@ fn wrap_modifiers(plan: LogicalPlan, modifiers: ast::Modifiers) -> LogicalPlan
211
429
  };
212
430
  }
213
431
 
432
+ if modifiers.distinct
433
+ {
434
+ plan = LogicalPlan::Distinct {
435
+ source: Box::new(plan),
436
+ }
437
+ }
438
+
214
439
  plan
215
440
  }
216
441
 
@@ -253,7 +478,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
253
478
  name: use_graph.name,
254
479
  }),
255
480
  ast::Statement::Create(create) => Ok(LogicalPlan::Create {
256
- patterns: create.patterns,
481
+ patterns: plan_patterns(create.patterns),
257
482
  }),
258
483
  ast::Statement::Match(match_statement) =>
259
484
  {
@@ -276,7 +501,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
276
501
  // Standard node scan when no SEARCH
277
502
  Ok(wrap_filter(
278
503
  LogicalPlan::NodeScan {
279
- patterns: match_statement.patterns,
504
+ patterns: plan_patterns(match_statement.patterns),
280
505
  optional: match_statement.optional,
281
506
  },
282
507
  match_statement.where_expression,
@@ -313,7 +538,7 @@ fn plan_impl(statement: ast::Statement) -> Result<LogicalPlan>
313
538
  updates: update_statement.updates,
314
539
  }),
315
540
  ast::Statement::Merge(merge_statement) => Ok(LogicalPlan::Merge {
316
- patterns: merge_statement.patterns,
541
+ patterns: plan_patterns(merge_statement.patterns),
317
542
  on_create: merge_statement.on_create,
318
543
  on_match: merge_statement.on_match,
319
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
  }
@@ -570,7 +570,7 @@ impl Store
570
570
  /// database forward exactly one step; the function recurses until `from` matches current.
571
571
  fn upgrade_database(&self, from: utils::Version, tx: &mut TransactionBox) -> Result<()>
572
572
  {
573
- if from == consts::GQLITE_VERSION
573
+ if from.major == consts::GQLITE_VERSION.major && from.minor == consts::GQLITE_VERSION.minor
574
574
  {
575
575
  return Ok(());
576
576
  }