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
@@ -1,9 +1,10 @@
1
1
  use std::collections::{HashMap, HashSet};
2
2
 
3
- use rustc_hash::FxHashSet;
4
-
5
3
  use crate::{
6
- interpreter::instructions::{Block, VariablesSizes},
4
+ interpreter::{
5
+ instructions::{Block, VariablesSizes},
6
+ path,
7
+ },
7
8
  prelude::*,
8
9
  store::{SelectNodeQuery, TransactionBoxable},
9
10
  value_table::{MutableRowInterface, Row, RowInterface},
@@ -12,7 +13,7 @@ use interpreter::instructions;
12
13
 
13
14
  #[derive(Debug, Clone)]
14
15
  #[allow(clippy::enum_variant_names)]
15
- enum Value
16
+ pub(crate) enum Value
16
17
  {
17
18
  GraphValue(value::Value),
18
19
  NodeQuery(store::SelectNodeQuery),
@@ -157,18 +158,18 @@ macro_rules! contain_to_value {
157
158
  }
158
159
 
159
160
  #[derive(Default, Debug)]
160
- struct Stack
161
+ pub(crate) struct Stack
161
162
  {
162
163
  stack: Vec<Value>,
163
164
  }
164
165
 
165
166
  impl Stack
166
167
  {
167
- fn push(&mut self, value: impl Into<Value>)
168
+ pub(crate) fn push(&mut self, value: impl Into<Value>)
168
169
  {
169
170
  self.stack.push(value.into());
170
171
  }
171
- fn try_pop(&mut self) -> Result<Value>
172
+ pub(crate) fn try_pop(&mut self) -> Result<Value>
172
173
  {
173
174
  self.stack.pop().ok_or(InternalError::EmptyStack.into())
174
175
  }
@@ -191,7 +192,7 @@ impl Stack
191
192
  {
192
193
  self.stack
193
194
  }
194
- fn try_pop_as_boolean(&mut self) -> Result<bool>
195
+ pub(crate) fn try_pop_as_boolean(&mut self) -> Result<bool>
195
196
  {
196
197
  let v = self.try_pop()?;
197
198
  match v
@@ -205,7 +206,7 @@ impl Stack
205
206
  }
206
207
  }
207
208
 
208
- trait TryPopInto<T>
209
+ pub(crate) trait TryPopInto<T>
209
210
  {
210
211
  fn try_pop_into(&mut self) -> Result<T>;
211
212
  fn try_drain_into(&mut self, n: usize) -> Result<Vec<T>>;
@@ -354,7 +355,7 @@ fn execute_string_operator(
354
355
  Ok(())
355
356
  }
356
357
 
357
- fn eval_instructions(
358
+ pub(crate) fn eval_instructions(
358
359
  stack: &mut Stack,
359
360
  row: &impl value_table::RowInterface,
360
361
  instructions: &instructions::Instructions,
@@ -1228,6 +1229,39 @@ fn filter_rows(
1228
1229
  .collect()
1229
1230
  }
1230
1231
 
1232
+ /// Executes a chain of `BlockMatch` blocks sequentially against `current_rows`.
1233
+ /// Each block transforms the rows for the next block in the sequence.
1234
+ ///
1235
+ /// This is the reusable helper for executing a `Vec<BlockMatch>` chain, used by
1236
+ /// `Block::Match` handlers, group-repetition steps (WI4), and similar contexts.
1237
+ #[allow(clippy::too_many_arguments)]
1238
+ pub(crate) fn execute_block_matches<TStore: store::Store>(
1239
+ blocks: &[instructions::BlockMatch],
1240
+ store: &TStore,
1241
+ tx: &mut TStore::TransactionBox,
1242
+ graph_name: &str,
1243
+ stack: &mut Stack,
1244
+ parameters: &crate::value::ValueMap,
1245
+ mut current_rows: Vec<value_table::Row>,
1246
+ allow_recursive: bool,
1247
+ ) -> Result<Vec<value_table::Row>>
1248
+ {
1249
+ for block in blocks
1250
+ {
1251
+ current_rows = execute_block_match(
1252
+ block,
1253
+ store,
1254
+ tx,
1255
+ graph_name,
1256
+ stack,
1257
+ parameters,
1258
+ current_rows,
1259
+ allow_recursive,
1260
+ )?;
1261
+ }
1262
+ Ok(current_rows)
1263
+ }
1264
+
1231
1265
  /// Executes a single `BlockMatch` (node or edge) against every row in `current_rows`,
1232
1266
  /// appending matching rows into `new_rows`.
1233
1267
  ///
@@ -1290,309 +1324,34 @@ fn execute_block_match<TStore: store::Store>(
1290
1324
  }
1291
1325
  }
1292
1326
 
1293
- instructions::BlockMatch::MatchEdge {
1294
- instructions,
1295
- left_variable,
1296
- edge_variable,
1297
- right_variable,
1298
- path_variable,
1299
- filter,
1300
- directivity,
1301
- recursive_range,
1302
- } =>
1327
+ instructions::BlockMatch::MatchEdge { .. } =>
1303
1328
  {
1304
- eval_instructions(stack, &row, instructions, parameters)?;
1305
- let query = stack.try_pop_into()?;
1306
-
1307
- match recursive_range
1308
- {
1309
- None =>
1310
- {
1311
- let edges = store.select_edges(tx, graph_name, query, *directivity)?;
1312
-
1313
- for edge in edges.into_iter()
1314
- {
1315
- let mut new_row = row.clone();
1316
- let (left, right) = if edge.reversed
1317
- {
1318
- (edge.path.destination(), edge.path.source())
1319
- }
1320
- else
1321
- {
1322
- (edge.path.source(), edge.path.destination())
1323
- };
1324
-
1325
- if let Some(left_variable) = left_variable
1326
- {
1327
- new_row.set(*left_variable, left.to_owned().into())?;
1328
- }
1329
- if let Some(right_variable) = right_variable
1330
- {
1331
- new_row.set(*right_variable, right.to_owned().into())?;
1332
- }
1333
- if let Some(edge_variable) = edge_variable
1334
- {
1335
- new_row.set(*edge_variable, edge.path.to_edge().into())?;
1336
- }
1337
- if let Some(path_variable) = path_variable
1338
- {
1339
- new_row.set(*path_variable, edge.path.to_owned().into())?;
1340
- }
1341
-
1342
- let should_add_row = if filter.is_empty()
1343
- {
1344
- true
1345
- }
1346
- else
1347
- {
1348
- let mut stack = Stack::default();
1349
- stack.push(true);
1350
- stack.push(edge.path.to_edge());
1351
- eval_instructions(&mut stack, &new_row, filter, parameters)?;
1352
- stack.try_pop()?;
1353
- stack.try_pop_into()?
1354
- };
1355
-
1356
- if should_add_row
1357
- {
1358
- new_rows.push(new_row);
1359
- }
1360
- }
1361
- }
1362
-
1363
- Some(recursive_range) =>
1364
- {
1365
- if !allow_recursive
1366
- {
1367
- return Err(InternalError::UnsupportedRecursivePatternInMerge.into());
1368
- }
1369
-
1370
- // Recursive DFS
1371
- let min_hops = recursive_range.start;
1372
- let max_hops = recursive_range.end;
1373
- let capacity = max_hops.min(100);
1374
-
1375
- let bound_dest_key: Option<graph::Key> = if let Some(right_col) = right_variable
1376
- {
1377
- match row.get(*right_col)?
1378
- {
1379
- value::Value::Node(node) => Some(node.key()),
1380
- _ => None,
1381
- }
1382
- }
1383
- else
1384
- {
1385
- None
1386
- };
1387
-
1388
- let start_nodes: Vec<graph::Node> = {
1389
- let source_query = query.source_query();
1390
- store.select_nodes(tx, graph_name, source_query)?
1391
- };
1392
-
1393
- struct Frame
1394
- {
1395
- node: graph::Node,
1396
- edges: Vec<store::EdgeResult>,
1397
- edge_index: usize,
1398
- depth: usize,
1399
- }
1400
-
1401
- for start_node in start_nodes
1402
- {
1403
- let mut path_edges: Vec<graph::Edge> = Vec::with_capacity(capacity);
1404
- let mut used_edges: FxHashSet<graph::Key> =
1405
- FxHashSet::with_capacity_and_hasher(capacity, Default::default());
1406
- let mut dedup_set: FxHashSet<(Option<graph::Key>, Option<graph::Key>, usize)> =
1407
- FxHashSet::with_capacity_and_hasher(capacity, Default::default());
1408
-
1409
- let initial_edges = {
1410
- let q = store::SelectEdgeQuery::select_source_keys(
1411
- store::SelectNodeQuery::select_keys(vec![start_node.key()]),
1412
- );
1413
- store.select_edges(tx, graph_name, q, *directivity)?
1414
- };
1415
-
1416
- let mut dfs_stack: Vec<Frame> = vec![Frame {
1417
- node: start_node.clone(),
1418
- edges: initial_edges,
1419
- edge_index: 0,
1420
- depth: 0,
1421
- }];
1422
-
1423
- while !dfs_stack.is_empty()
1424
- {
1425
- let mut push_next: Option<Frame> = None;
1426
- let mut should_pop = false;
1427
-
1428
- let dfs_intermediate_nodes: Vec<graph::Node> = if dfs_stack.len() >= 2
1429
- {
1430
- dfs_stack[1..dfs_stack.len() - 1]
1431
- .iter()
1432
- .map(|f| f.node.clone())
1433
- .collect()
1434
- }
1435
- else
1436
- {
1437
- Vec::new()
1438
- };
1439
-
1440
- {
1441
- let frame = dfs_stack.last_mut().unwrap();
1442
-
1443
- // Emit row
1444
- if frame.edge_index == 0 && frame.depth >= min_hops
1445
- {
1446
- let current_node = &frame.node;
1447
-
1448
- let mut new_row = row.clone();
1449
-
1450
- if let Some(left_var) = left_variable
1451
- {
1452
- new_row.set(*left_var, start_node.clone().into())?;
1453
- }
1454
- if let Some(right_var) = right_variable
1455
- {
1456
- new_row.set(*right_var, current_node.clone().into())?;
1457
- }
1458
- if let Some(edge_var) = edge_variable
1459
- && let Some(last_edge) = path_edges.last()
1460
- {
1461
- new_row.set(*edge_var, last_edge.clone().into())?;
1462
- }
1463
- if let Some(path_var) = path_variable
1464
- {
1465
- let path = graph::Path::new(
1466
- start_node.clone(),
1467
- path_edges
1468
- .clone()
1469
- .into_iter()
1470
- .zip(
1471
- dfs_intermediate_nodes
1472
- .clone()
1473
- .into_iter()
1474
- .chain(std::iter::once(current_node.clone())),
1475
- )
1476
- .collect(),
1477
- );
1478
- new_row.set(*path_var, path.into())?;
1479
- }
1480
-
1481
- let passes_filter = if filter.is_empty()
1482
- {
1483
- true
1484
- }
1485
- else
1486
- {
1487
- let mut eval_stack = Stack::default();
1488
- eval_stack.push(true);
1489
- if let Some(last_edge) = path_edges.last()
1490
- {
1491
- eval_stack.push(last_edge.clone());
1492
- }
1493
- eval_instructions(&mut eval_stack, &new_row, filter, parameters)?;
1494
- eval_stack.try_pop()?;
1495
- eval_stack.try_pop_into()?
1496
- };
1497
-
1498
- let matches_dest = if let Some(dest_key) = bound_dest_key
1499
- {
1500
- current_node.key() == dest_key
1501
- }
1502
- else
1503
- {
1504
- true
1505
- };
1506
-
1507
- if passes_filter && matches_dest
1508
- {
1509
- let left_key_opt: Option<graph::Key> = if left_variable.is_some()
1510
- {
1511
- Some(start_node.key())
1512
- }
1513
- else
1514
- {
1515
- None
1516
- };
1517
- let right_key_opt: Option<graph::Key> = Some(current_node.key());
1518
- let path_len = path_edges.len();
1519
- let sig = (left_key_opt, right_key_opt, path_len);
1520
-
1521
- if !dedup_set.contains(&sig)
1522
- {
1523
- dedup_set.insert(sig);
1524
- new_rows.push(new_row);
1525
- }
1526
- }
1527
- }
1528
-
1529
- // Stop for max depth
1530
- if frame.depth == max_hops || frame.edge_index >= frame.edges.len()
1531
- {
1532
- should_pop = true;
1533
- }
1534
- else
1535
- {
1536
- // Next hop
1537
- let edge_result = &frame.edges[frame.edge_index];
1538
- frame.edge_index += 1;
1539
-
1540
- let edge = edge_result.path.to_edge();
1541
- let edge_key = edge.key();
1542
-
1543
- if used_edges.contains(&edge_key)
1544
- {
1545
- continue;
1546
- }
1547
-
1548
- let next_node = if edge_result.reversed
1549
- {
1550
- edge_result.path.source().clone()
1551
- }
1552
- else
1553
- {
1554
- edge_result.path.destination().clone()
1555
- };
1556
-
1557
- used_edges.insert(edge_key);
1558
- path_edges.push(edge.clone());
1559
-
1560
- let next_edges = {
1561
- let q = store::SelectEdgeQuery::select_source_keys(
1562
- store::SelectNodeQuery::select_keys(vec![next_node.key()]),
1563
- );
1564
- store.select_edges(tx, graph_name, q, *directivity)?
1565
- };
1566
-
1567
- push_next = Some(Frame {
1568
- node: next_node,
1569
- edges: next_edges,
1570
- edge_index: 0,
1571
- depth: frame.depth + 1,
1572
- });
1573
- }
1574
- }
1575
-
1576
- // Mutate stack
1577
- if should_pop
1578
- {
1579
- dfs_stack.pop();
1580
- if let Some(last_edge) = path_edges.pop()
1581
- {
1582
- used_edges.remove(&last_edge.key());
1583
- }
1584
- continue;
1585
- }
1329
+ super::path::execute_match_edge(
1330
+ block,
1331
+ store,
1332
+ tx,
1333
+ graph_name,
1334
+ stack,
1335
+ parameters,
1336
+ &row,
1337
+ allow_recursive,
1338
+ &mut new_rows,
1339
+ )?;
1340
+ }
1586
1341
 
1587
- if let Some(frame) = push_next
1588
- {
1589
- dfs_stack.push(frame);
1590
- continue;
1591
- }
1592
- }
1593
- }
1594
- }
1595
- }
1342
+ instructions::BlockMatch::MatchQuantifiedPath { .. } =>
1343
+ {
1344
+ path::execute_match_quantified_path(
1345
+ block,
1346
+ store,
1347
+ tx,
1348
+ graph_name,
1349
+ stack,
1350
+ parameters,
1351
+ &row,
1352
+ allow_recursive,
1353
+ &mut new_rows,
1354
+ )?;
1596
1355
  }
1597
1356
  }
1598
1357
  }
@@ -1770,20 +1529,17 @@ pub(crate) fn eval_program<TStore: store::Store>(
1770
1529
  let mut output_table = value_table::ValueTable::new(variables_size.persistent_variables);
1771
1530
  for row in input_table.into_row_iter()
1772
1531
  {
1773
- let mut current_rows: Vec<_> = vec![row.clone().extended(variables_size.total_size())?];
1774
- for block in blocks.iter()
1775
- {
1776
- current_rows = execute_block_match(
1777
- block,
1778
- store,
1779
- &mut tx,
1780
- &graph_name,
1781
- &mut stack,
1782
- parameters,
1783
- current_rows,
1784
- true, // recursive patterns allowed in MATCH
1785
- )?;
1786
- }
1532
+ let current_rows: Vec<_> = vec![row.clone().extended(variables_size.total_size())?];
1533
+ let mut current_rows = execute_block_matches(
1534
+ blocks,
1535
+ store,
1536
+ &mut tx,
1537
+ &graph_name,
1538
+ &mut stack,
1539
+ parameters,
1540
+ current_rows,
1541
+ true, // recursive patterns allowed in MATCH
1542
+ )?;
1787
1543
  if !filter.is_empty()
1788
1544
  {
1789
1545
  current_rows = filter_rows(current_rows.into_iter(), filter, parameters)?;
@@ -2440,6 +2196,16 @@ pub(crate) fn eval_program<TStore: store::Store>(
2440
2196
  );
2441
2197
  return Ok(crate::value::Value::Map(res).into());
2442
2198
  }
2199
+ else if name == "gqlite.db.list_graphs"
2200
+ {
2201
+ let mut graphs = store.graphs_list(&mut tx)?;
2202
+ graphs.sort();
2203
+ let list: Vec<crate::value::Value> = graphs
2204
+ .into_iter()
2205
+ .map(crate::value::Value::String)
2206
+ .collect();
2207
+ return Ok(crate::value::Value::Array(list).into());
2208
+ }
2443
2209
  else
2444
2210
  {
2445
2211
  return Err(InternalError::Unimplemented("call for any other function").into());
@@ -1,5 +1,22 @@
1
1
  use crate::{prelude::*, value_table::ColId};
2
2
 
3
+ #[derive(Debug)]
4
+ pub(crate) enum StopCondition
5
+ {
6
+ Shortest(Instructions),
7
+ ShortestGroups(Instructions),
8
+ }
9
+
10
+ #[derive(Debug)]
11
+ pub(crate) enum PathStrategy
12
+ {
13
+ Dfs,
14
+ Bfs
15
+ {
16
+ stop_condition: StopCondition,
17
+ },
18
+ }
19
+
3
20
  #[derive(Debug)]
4
21
  #[allow(clippy::large_enum_variant)]
5
22
  pub(crate) enum Instruction
@@ -103,6 +120,7 @@ pub(crate) struct CreateAction
103
120
  }
104
121
 
105
122
  #[derive(Debug)]
123
+ #[allow(clippy::enum_variant_names)]
106
124
  pub(crate) enum BlockMatch
107
125
  {
108
126
  MatchNode
@@ -122,6 +140,60 @@ pub(crate) enum BlockMatch
122
140
  directivity: graphcore::EdgeDirectivity,
123
141
  recursive_range: Option<std::ops::Range<usize>>,
124
142
  },
143
+ MatchQuantifiedPath
144
+ {
145
+ /// Compiled query for the group's fixed anchor before it (`qp.source`).
146
+ /// Pops one `SelectNodeQuery`, same as an ordinary `MatchNode`'s
147
+ /// `instructions` field — reuses `compile_match_node` directly (WI2).
148
+ source_instructions: Instructions,
149
+ source_variable: Option<ColId>,
150
+ source_filter: Instructions,
151
+ /// The destination node is validated only at emission time via
152
+ /// `destination_filter`, not queried from the store.
153
+ destination_variable: Option<ColId>,
154
+ destination_filter: Instructions,
155
+ /// The `p = ...` binding, if present.
156
+ path_variable: Option<ColId>,
157
+ /// Resolved `min..max` from `qp.quantifier` — always `Some` in
158
+ /// practice by the time this instruction exists (WI3 only ever
159
+ /// constructs one from a `QuantifiedPathPattern` with `group.len() > 1`,
160
+ /// and Milestone 2/4's `None`-quantifier scoping-wrapper use case,
161
+ /// design §1d, isn't reachable from real query text until Milestone 5).
162
+ quantifier: std::ops::Range<usize>,
163
+ /// The group's inner pattern sequence, compiled the same way an
164
+ /// ordinary top-level pattern chain is (WI2) — run once per repetition.
165
+ group_instructions: Vec<BlockMatch>,
166
+ /// Every variable declared inside the group (`n`, `r`, `m` for
167
+ /// `(a) ((n)-[r]->(m)){1,5} (b)`) — however many there are. Bound as a
168
+ /// list at execution (Epic 8); this field only declares *which*
169
+ /// columns need that treatment.
170
+ group_variables: Vec<ColId>,
171
+ /// Which entry in `group_variables` is the group's own terminal node
172
+ /// (the exit node of its last inner edge) - guaranteed present by the
173
+ /// parser-level fix. Used for chaining repetitions and for
174
+ /// checking `destination_filter`/binding `destination_variable`,
175
+ /// replacing the "first Node found" heuristic.
176
+ group_terminal_variable: ColId,
177
+ /// Which entry in `group_variables` is the group's own entry node
178
+ /// (the source node of its first inner edge) - guaranteed present by the
179
+ /// parser-level fix. Used to wire the previous repetition's terminal
180
+ /// to the next repetition's entry, so pattern search is constrained
181
+ /// from the current position rather than all nodes.
182
+ group_entry_variable: ColId,
183
+ /// Compiled group `WHERE`, if present (design §1, `qp.group_where`) —
184
+ /// evaluated once per repetition once Epic 8 wires up execution.
185
+ /// Genuine groups only; never `Some` on wrapper-promoted patterns.
186
+ group_where_filter: Option<Instructions>,
187
+ /// Whole-completed-path predicate, from the selector-scoped wrapper
188
+ /// (`qp.path_where`). Evaluated once a candidate reaches `destination`,
189
+ /// before it's admitted to shortest-path selection. Distinct from
190
+ /// `group_where_filter`'s per-repetition timing; never both `Some` on
191
+ /// the same instruction in practice (a wrapper-promoted pattern has no
192
+ /// genuine group `WHERE` of its own).
193
+ path_filter: Option<Instructions>,
194
+ /// The path strategy (Dfs or Bfs with stop condition) from the planner.
195
+ strategy: PathStrategy,
196
+ },
125
197
  }
126
198
 
127
199
  #[derive(Debug)]
@@ -1,4 +1,5 @@
1
1
  pub(crate) mod evaluators;
2
2
  pub(crate) mod instructions;
3
+ pub(crate) mod path;
3
4
 
4
5
  pub(crate) type Program = Vec<instructions::Block>;