lora-python 0.2.0__tar.gz → 0.3.0__tar.gz

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 (97) hide show
  1. {lora_python-0.2.0 → lora_python-0.3.0}/Cargo.lock +36 -13
  2. {lora_python-0.2.0 → lora_python-0.3.0}/Cargo.toml +10 -8
  3. {lora_python-0.2.0 → lora_python-0.3.0}/PKG-INFO +1 -1
  4. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/analyzer.rs +3 -3
  5. lora_python-0.3.0/crates/lora-database/src/database.rs +301 -0
  6. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/src/lib.rs +5 -1
  7. lora_python-0.3.0/crates/lora-database/tests/backend_stub.rs +388 -0
  8. lora_python-0.3.0/crates/lora-database/tests/snapshot.rs +195 -0
  9. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/vectors.rs +1 -1
  10. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/src/eval.rs +85 -84
  11. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/src/executor.rs +139 -102
  12. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/src/lib.rs +49 -17
  13. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-store/Cargo.toml +4 -0
  14. lora_python-0.3.0/crates/lora-store/src/graph.rs +837 -0
  15. lora_python-0.3.0/crates/lora-store/src/lib.rs +18 -0
  16. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-store/src/memory.rs +588 -161
  17. lora_python-0.3.0/crates/lora-store/src/mutation.rs +115 -0
  18. lora_python-0.3.0/crates/lora-store/src/snapshot.rs +441 -0
  19. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-store/src/spatial.rs +1 -1
  20. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-store/src/temporal.rs +6 -6
  21. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-store/src/vector.rs +5 -3
  22. {lora_python-0.2.0 → lora_python-0.3.0}/pyproject.toml +1 -1
  23. {lora_python-0.2.0 → lora_python-0.3.0}/python/lora_python/__init__.py +2 -0
  24. {lora_python-0.2.0 → lora_python-0.3.0}/python/lora_python/_async.py +20 -1
  25. {lora_python-0.2.0 → lora_python-0.3.0}/python/lora_python/types.py +18 -0
  26. lora_python-0.2.0/crates/lora-database/src/database.rs +0 -105
  27. lora_python-0.2.0/crates/lora-store/src/graph.rs +0 -649
  28. lora_python-0.2.0/crates/lora-store/src/lib.rs +0 -11
  29. {lora_python-0.2.0 → lora_python-0.3.0}/LICENSE +0 -0
  30. {lora_python-0.2.0 → lora_python-0.3.0}/README.md +0 -0
  31. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/Cargo.toml +0 -0
  32. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/errors.rs +0 -0
  33. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/lib.rs +0 -0
  34. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/resolved.rs +0 -0
  35. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/scope.rs +0 -0
  36. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-analyzer/src/symbols.rs +0 -0
  37. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-ast/Cargo.toml +0 -0
  38. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-ast/src/ast.rs +0 -0
  39. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-ast/src/lib.rs +0 -0
  40. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/Cargo.toml +0 -0
  41. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/lib.rs +0 -0
  42. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/logical.rs +0 -0
  43. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/optimizer.rs +0 -0
  44. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/pattern.rs +0 -0
  45. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/physical.rs +0 -0
  46. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-compiler/src/planner.rs +0 -0
  47. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/Cargo.toml +0 -0
  48. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
  49. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
  50. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/fixtures.rs +0 -0
  51. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
  52. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
  53. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
  54. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
  55. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/advanced_queries.rs +0 -0
  56. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/aggregation.rs +0 -0
  57. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/create.rs +0 -0
  58. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/errors.rs +0 -0
  59. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/expressions.rs +0 -0
  60. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/functions_extended.rs +0 -0
  61. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/invariants.rs +0 -0
  62. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/match.rs +0 -0
  63. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/merge.rs +0 -0
  64. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/ordering.rs +0 -0
  65. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/parameters.rs +0 -0
  66. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/parser.rs +0 -0
  67. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/paths.rs +0 -0
  68. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/projection.rs +0 -0
  69. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/seeds.rs +0 -0
  70. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/spatial.rs +0 -0
  71. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/temporal.rs +0 -0
  72. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/test_helpers.rs +0 -0
  73. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/types_advanced.rs +0 -0
  74. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/union.rs +0 -0
  75. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/update.rs +0 -0
  76. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/where_clause.rs +0 -0
  77. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-database/tests/with.rs +0 -0
  78. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/Cargo.toml +0 -0
  79. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/src/errors.rs +0 -0
  80. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/src/lib.rs +0 -0
  81. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-executor/src/value.rs +0 -0
  82. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-parser/Cargo.toml +0 -0
  83. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-parser/src/cypher.pest +0 -0
  84. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-parser/src/error.rs +0 -0
  85. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-parser/src/lib.rs +0 -0
  86. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-parser/src/parser.rs +0 -0
  87. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/.gitignore +0 -0
  88. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/Cargo.toml +0 -0
  89. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/LICENSE +0 -0
  90. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/README.md +0 -0
  91. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/build.rs +0 -0
  92. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/examples/async_demo.py +0 -0
  93. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/examples/basic.py +0 -0
  94. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/tests/test_async.py +0 -0
  95. {lora_python-0.2.0 → lora_python-0.3.0}/crates/lora-python/tests/test_sync.py +0 -0
  96. {lora_python-0.2.0 → lora_python-0.3.0}/python/lora_python/_native.pyi +0 -0
  97. {lora_python-0.2.0 → lora_python-0.3.0}/python/lora_python/py.typed +0 -0
@@ -116,6 +116,15 @@ dependencies = [
116
116
  "tracing",
117
117
  ]
118
118
 
119
+ [[package]]
120
+ name = "bincode"
121
+ version = "1.3.3"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
124
+ dependencies = [
125
+ "serde",
126
+ ]
127
+
119
128
  [[package]]
120
129
  name = "bindgen"
121
130
  version = "0.72.1"
@@ -283,6 +292,15 @@ dependencies = [
283
292
  "libc",
284
293
  ]
285
294
 
295
+ [[package]]
296
+ name = "crc32fast"
297
+ version = "1.5.0"
298
+ source = "registry+https://github.com/rust-lang/crates.io-index"
299
+ checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
300
+ dependencies = [
301
+ "cfg-if",
302
+ ]
303
+
286
304
  [[package]]
287
305
  name = "criterion"
288
306
  version = "0.8.2"
@@ -611,7 +629,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
611
629
 
612
630
  [[package]]
613
631
  name = "lora-analyzer"
614
- version = "0.2.0"
632
+ version = "0.3.0"
615
633
  dependencies = [
616
634
  "lora-ast",
617
635
  "lora-parser",
@@ -621,14 +639,14 @@ dependencies = [
621
639
 
622
640
  [[package]]
623
641
  name = "lora-ast"
624
- version = "0.2.0"
642
+ version = "0.3.0"
625
643
  dependencies = [
626
644
  "smallvec 2.0.0-alpha.12",
627
645
  ]
628
646
 
629
647
  [[package]]
630
648
  name = "lora-compiler"
631
- version = "0.2.0"
649
+ version = "0.3.0"
632
650
  dependencies = [
633
651
  "lora-analyzer",
634
652
  "lora-ast",
@@ -636,7 +654,7 @@ dependencies = [
636
654
 
637
655
  [[package]]
638
656
  name = "lora-database"
639
- version = "0.2.0"
657
+ version = "0.3.0"
640
658
  dependencies = [
641
659
  "anyhow",
642
660
  "criterion",
@@ -652,7 +670,7 @@ dependencies = [
652
670
 
653
671
  [[package]]
654
672
  name = "lora-executor"
655
- version = "0.2.0"
673
+ version = "0.3.0"
656
674
  dependencies = [
657
675
  "lora-analyzer",
658
676
  "lora-ast",
@@ -666,7 +684,7 @@ dependencies = [
666
684
 
667
685
  [[package]]
668
686
  name = "lora-ffi"
669
- version = "0.2.0"
687
+ version = "0.3.0"
670
688
  dependencies = [
671
689
  "lora-database",
672
690
  "lora-store",
@@ -676,7 +694,7 @@ dependencies = [
676
694
 
677
695
  [[package]]
678
696
  name = "lora-node"
679
- version = "0.2.0"
697
+ version = "0.3.0"
680
698
  dependencies = [
681
699
  "anyhow",
682
700
  "lora-database",
@@ -690,7 +708,7 @@ dependencies = [
690
708
 
691
709
  [[package]]
692
710
  name = "lora-parser"
693
- version = "0.2.0"
711
+ version = "0.3.0"
694
712
  dependencies = [
695
713
  "lora-ast",
696
714
  "pest",
@@ -701,7 +719,7 @@ dependencies = [
701
719
 
702
720
  [[package]]
703
721
  name = "lora-python"
704
- version = "0.2.0"
722
+ version = "0.3.0"
705
723
  dependencies = [
706
724
  "anyhow",
707
725
  "lora-database",
@@ -713,10 +731,11 @@ dependencies = [
713
731
 
714
732
  [[package]]
715
733
  name = "lora-server"
716
- version = "0.2.0"
734
+ version = "0.3.0"
717
735
  dependencies = [
718
736
  "anyhow",
719
737
  "axum",
738
+ "http-body-util",
720
739
  "lora-database",
721
740
  "serde",
722
741
  "serde_json",
@@ -726,15 +745,19 @@ dependencies = [
726
745
 
727
746
  [[package]]
728
747
  name = "lora-store"
729
- version = "0.2.0"
748
+ version = "0.3.0"
730
749
  dependencies = [
750
+ "bincode",
751
+ "crc32fast",
731
752
  "js-sys",
732
753
  "lora-ast",
754
+ "serde",
755
+ "thiserror",
733
756
  ]
734
757
 
735
758
  [[package]]
736
759
  name = "lora-wasm"
737
- version = "0.2.0"
760
+ version = "0.3.0"
738
761
  dependencies = [
739
762
  "anyhow",
740
763
  "console_error_panic_hook",
@@ -749,7 +772,7 @@ dependencies = [
749
772
 
750
773
  [[package]]
751
774
  name = "lora_ruby"
752
- version = "0.2.0"
775
+ version = "0.3.0"
753
776
  dependencies = [
754
777
  "anyhow",
755
778
  "lora-database",
@@ -4,7 +4,7 @@ resolver = "2"
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2021"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  license = "BUSL-1.1"
9
9
  authors = ["LoraDB, Inc."]
10
10
  repository = "https://github.com/lora-db/lora"
@@ -15,13 +15,13 @@ rust-version = "1.87"
15
15
  # Internal crates — versions are kept in lockstep with [workspace.package].version
16
16
  # by `scripts/sync-versions.mjs`. Both `path` and `version` are set so that
17
17
  # `cargo publish` works (crates.io cannot resolve path-only deps).
18
- lora-ast = { path = "crates/lora-ast", version = "=0.2.0" }
19
- lora-parser = { path = "crates/lora-parser", version = "=0.2.0" }
20
- lora-analyzer = { path = "crates/lora-analyzer", version = "=0.2.0" }
21
- lora-compiler = { path = "crates/lora-compiler", version = "=0.2.0" }
22
- lora-store = { path = "crates/lora-store", version = "=0.2.0" }
23
- lora-executor = { path = "crates/lora-executor", version = "=0.2.0" }
24
- lora-database = { path = "crates/lora-database", version = "=0.2.0" }
18
+ lora-ast = { path = "crates/lora-ast", version = "=0.3.0" }
19
+ lora-parser = { path = "crates/lora-parser", version = "=0.3.0" }
20
+ lora-analyzer = { path = "crates/lora-analyzer", version = "=0.3.0" }
21
+ lora-compiler = { path = "crates/lora-compiler", version = "=0.3.0" }
22
+ lora-store = { path = "crates/lora-store", version = "=0.3.0" }
23
+ lora-executor = { path = "crates/lora-executor", version = "=0.3.0" }
24
+ lora-database = { path = "crates/lora-database", version = "=0.3.0" }
25
25
 
26
26
  # External crates.
27
27
  anyhow = "1"
@@ -37,3 +37,5 @@ serde_json = "1.0.149"
37
37
  regex = "1"
38
38
  tower = "0.5.3"
39
39
  criterion = { version = "0.8.2", features = ["html_reports"] }
40
+ bincode = "1.3"
41
+ crc32fast = "1.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lora-python
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -5,10 +5,10 @@ use lora_ast::{
5
5
  ReadingClause, RelationshipPattern, Remove, RemoveItem, Return, Set, SetItem, SinglePartQuery,
6
6
  SingleQuery, Statement, Unwind, UpdatingClause, With,
7
7
  };
8
- use lora_store::GraphStorage;
8
+ use lora_store::GraphCatalog;
9
9
  use std::collections::{BTreeMap, BTreeSet};
10
10
 
11
- pub struct Analyzer<'a, S: GraphStorage + ?Sized> {
11
+ pub struct Analyzer<'a, S: GraphCatalog + ?Sized> {
12
12
  storage: &'a S,
13
13
  scopes: ScopeStack,
14
14
  symbols: SymbolTable,
@@ -22,7 +22,7 @@ enum PatternContext {
22
22
  Write,
23
23
  }
24
24
 
25
- impl<'a, S: GraphStorage + ?Sized> Analyzer<'a, S> {
25
+ impl<'a, S: GraphCatalog + ?Sized> Analyzer<'a, S> {
26
26
  pub fn new(storage: &'a S) -> Self {
27
27
  Self {
28
28
  storage,
@@ -0,0 +1,301 @@
1
+ use std::collections::BTreeMap;
2
+ use std::fs::{File, OpenOptions};
3
+ use std::io::{BufReader, BufWriter};
4
+ use std::path::{Path, PathBuf};
5
+ use std::sync::{Arc, Mutex, MutexGuard};
6
+
7
+ use anyhow::Result;
8
+ use lora_analyzer::Analyzer;
9
+ use lora_ast::Document;
10
+ use lora_compiler::{CompiledQuery, Compiler};
11
+ use lora_executor::{
12
+ ExecuteOptions, LoraValue, MutableExecutionContext, MutableExecutor, QueryResult,
13
+ };
14
+ use lora_parser::parse_query;
15
+ use lora_store::{GraphStorage, GraphStorageMut, InMemoryGraph, SnapshotMeta, Snapshotable};
16
+
17
+ /// Minimal abstraction any transport can depend on to run Lora queries.
18
+ pub trait QueryRunner: Send + Sync + 'static {
19
+ fn execute(&self, query: &str, options: Option<ExecuteOptions>) -> Result<QueryResult>;
20
+ }
21
+
22
+ /// Owns the graph store and orchestrates parse → analyze → compile → execute.
23
+ pub struct Database<S> {
24
+ store: Arc<Mutex<S>>,
25
+ }
26
+
27
+ impl Database<InMemoryGraph> {
28
+ /// Convenience constructor: a fresh, empty in-memory graph database.
29
+ pub fn in_memory() -> Self {
30
+ Self::from_graph(InMemoryGraph::new())
31
+ }
32
+ }
33
+
34
+ impl<S> Database<S>
35
+ where
36
+ S: GraphStorage + GraphStorageMut,
37
+ {
38
+ /// Build a database from a pre-wrapped, shared store.
39
+ pub fn new(store: Arc<Mutex<S>>) -> Self {
40
+ Self { store }
41
+ }
42
+
43
+ /// Build a database by taking ownership of a bare graph store.
44
+ pub fn from_graph(graph: S) -> Self {
45
+ Self::new(Arc::new(Mutex::new(graph)))
46
+ }
47
+
48
+ /// Handle to the underlying shared store — useful for callers that need
49
+ /// to snapshot or share the graph across multiple databases.
50
+ pub fn store(&self) -> &Arc<Mutex<S>> {
51
+ &self.store
52
+ }
53
+
54
+ /// Parse a query string into an AST without executing it.
55
+ pub fn parse(&self, query: &str) -> Result<Document> {
56
+ Ok(parse_query(query)?)
57
+ }
58
+
59
+ fn lock_store(&self) -> MutexGuard<'_, S> {
60
+ self.store
61
+ .lock()
62
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
63
+ }
64
+
65
+ fn compile_query(&self, query: &str) -> Result<(MutexGuard<'_, S>, CompiledQuery)> {
66
+ let document = self.parse(query)?;
67
+ let store = self.lock_store();
68
+
69
+ let resolved = {
70
+ let mut analyzer = Analyzer::new(&*store);
71
+ analyzer.analyze(&document)?
72
+ };
73
+
74
+ let compiled = Compiler::compile(&resolved);
75
+ Ok((store, compiled))
76
+ }
77
+
78
+ /// Execute a query and return its result.
79
+ pub fn execute(&self, query: &str, options: Option<ExecuteOptions>) -> Result<QueryResult> {
80
+ self.execute_with_params(query, options, BTreeMap::new())
81
+ }
82
+
83
+ /// Execute a query with bound parameters.
84
+ pub fn execute_with_params(
85
+ &self,
86
+ query: &str,
87
+ options: Option<ExecuteOptions>,
88
+ params: BTreeMap<String, LoraValue>,
89
+ ) -> Result<QueryResult> {
90
+ let (mut store, compiled) = self.compile_query(query)?;
91
+
92
+ let mut executor = MutableExecutor::new(MutableExecutionContext {
93
+ storage: &mut *store,
94
+ params,
95
+ });
96
+
97
+ Ok(executor.execute_compiled(&compiled, options)?)
98
+ }
99
+
100
+ // ---------- Storage-agnostic utility helpers ----------
101
+ //
102
+ // Bindings previously reached into `Arc<Mutex<InMemoryGraph>>` to answer
103
+ // stat / admin calls; these helpers let them depend on `Database<S>`
104
+ // instead, so swapping in a new backend only requires changing one type
105
+ // parameter.
106
+
107
+ /// Drop every node and relationship.
108
+ pub fn clear(&self) {
109
+ let mut guard = self.lock_store();
110
+ guard.clear();
111
+ }
112
+
113
+ /// Number of nodes currently in the graph.
114
+ pub fn node_count(&self) -> usize {
115
+ let guard = self.lock_store();
116
+ guard.node_count()
117
+ }
118
+
119
+ /// Number of relationships currently in the graph.
120
+ pub fn relationship_count(&self) -> usize {
121
+ let guard = self.lock_store();
122
+ guard.relationship_count()
123
+ }
124
+
125
+ /// Run a closure with a shared borrow of the underlying store. Used by
126
+ /// bindings to answer ad-hoc queries without locking the mutex themselves.
127
+ pub fn with_store<R>(&self, f: impl FnOnce(&S) -> R) -> R {
128
+ let guard = self.lock_store();
129
+ f(&*guard)
130
+ }
131
+
132
+ /// Run a closure with an exclusive borrow of the underlying store. Reserved
133
+ /// for admin paths (restore, bulk load); regular mutation goes through
134
+ /// `execute_with_params`.
135
+ pub fn with_store_mut<R>(&self, f: impl FnOnce(&mut S) -> R) -> R {
136
+ let mut guard = self.lock_store();
137
+ f(&mut *guard)
138
+ }
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Snapshot helpers
143
+ //
144
+ // A second impl block so the `Snapshotable` bound only constrains backends
145
+ // that actually need it. `Database<InMemoryGraph>` picks these up
146
+ // automatically; hypothetical backends that don't implement `Snapshotable`
147
+ // still get the core query API above.
148
+ // ---------------------------------------------------------------------------
149
+
150
+ impl<S> Database<S>
151
+ where
152
+ S: GraphStorage + GraphStorageMut + Snapshotable,
153
+ {
154
+ /// Serialize the current graph state to the given path. Writes are
155
+ /// atomic: the payload goes to `<path>.tmp`, is `fsync`'d, and then
156
+ /// renamed over the target; a torn write can never leave a half-written
157
+ /// file at `path`. If any step before the rename fails, the stale
158
+ /// `<path>.tmp` is removed so a crashed save never leaks scratch files.
159
+ ///
160
+ /// Holds the store mutex for the duration of the save so concurrent
161
+ /// queries see a consistent point-in-time snapshot.
162
+ pub fn save_snapshot_to(&self, path: impl AsRef<Path>) -> Result<SnapshotMeta> {
163
+ let path = path.as_ref();
164
+ let tmp = snapshot_tmp_path(path);
165
+
166
+ // Acquire the lock once so the snapshot is point-in-time consistent.
167
+ let guard = self.lock_store();
168
+
169
+ let file = OpenOptions::new()
170
+ .write(true)
171
+ .create(true)
172
+ .truncate(true)
173
+ .open(&tmp)?;
174
+ // Arm cleanup immediately after `open` succeeds: every early return
175
+ // below must either surface an error *and* unlink the tmp, or commit
176
+ // the guard once the rename takes effect.
177
+ let tmp_guard = TempFileGuard::new(tmp.clone());
178
+ let mut writer = BufWriter::new(file);
179
+
180
+ let meta = guard.save_snapshot(&mut writer)?;
181
+
182
+ // Flush the BufWriter before fsync; otherwise we fsync an empty
183
+ // underlying file.
184
+ use std::io::Write;
185
+ writer.flush()?;
186
+ let file = writer.into_inner().map_err(|e| e.into_error())?;
187
+ file.sync_all()?;
188
+ drop(file);
189
+
190
+ std::fs::rename(&tmp, path)?;
191
+ // The tmp path no longer has a file behind it — disarm the guard so
192
+ // it doesn't try to remove the just-renamed target by name race.
193
+ tmp_guard.commit();
194
+
195
+ // Best-effort parent-dir fsync so the rename itself is durable on
196
+ // power loss. Non-fatal if the parent can't be opened.
197
+ if let Some(parent) = path.parent() {
198
+ if let Ok(dir) = File::open(parent) {
199
+ let _ = dir.sync_all();
200
+ }
201
+ }
202
+
203
+ Ok(meta)
204
+ }
205
+
206
+ /// Replace the current graph state with a snapshot loaded from `path`.
207
+ /// Holds the store mutex for the duration of the load; concurrent
208
+ /// queries block until restore completes.
209
+ pub fn load_snapshot_from(&self, path: impl AsRef<Path>) -> Result<SnapshotMeta> {
210
+ let file = File::open(path.as_ref())?;
211
+ let reader = BufReader::new(file);
212
+
213
+ let mut guard = self.lock_store();
214
+ Ok(guard.load_snapshot(reader)?)
215
+ }
216
+ }
217
+
218
+ impl Database<InMemoryGraph> {
219
+ /// Convenience constructor: open (or create) an empty in-memory database
220
+ /// and immediately restore it from `path`. Errors if the file cannot be
221
+ /// opened or the snapshot is malformed.
222
+ pub fn in_memory_from_snapshot(path: impl AsRef<Path>) -> Result<Self> {
223
+ let db = Self::in_memory();
224
+ db.load_snapshot_from(path)?;
225
+ Ok(db)
226
+ }
227
+ }
228
+
229
+ fn snapshot_tmp_path(target: &Path) -> PathBuf {
230
+ let mut tmp = target.as_os_str().to_owned();
231
+ tmp.push(".tmp");
232
+ PathBuf::from(tmp)
233
+ }
234
+
235
+ /// RAII handle that deletes its path on drop unless [`commit`] is called.
236
+ ///
237
+ /// The snapshot save path creates `<target>.tmp` before the payload is
238
+ /// written; if any step between then and the final rename fails (or the
239
+ /// thread unwinds), the guard's `Drop` removes the scratch file so a crashed
240
+ /// save never leaves leftovers on disk.
241
+ ///
242
+ /// [`commit`]: Self::commit
243
+ struct TempFileGuard {
244
+ path: Option<PathBuf>,
245
+ }
246
+
247
+ impl TempFileGuard {
248
+ fn new(path: PathBuf) -> Self {
249
+ Self { path: Some(path) }
250
+ }
251
+
252
+ /// Disarm the guard. Call this once the tmp file's contents have been
253
+ /// handed off (e.g. renamed to their final destination) so the `Drop`
254
+ /// impl does not try to remove them.
255
+ fn commit(mut self) {
256
+ self.path.take();
257
+ }
258
+ }
259
+
260
+ impl Drop for TempFileGuard {
261
+ fn drop(&mut self) {
262
+ if let Some(path) = self.path.take() {
263
+ // Best-effort: cleanup failure is not worth surfacing — the
264
+ // worst case is a leaked scratch file that the next save
265
+ // overwrites via `OpenOptions::truncate(true)`.
266
+ let _ = std::fs::remove_file(path);
267
+ }
268
+ }
269
+ }
270
+
271
+ /// Storage-agnostic admin surface for HTTP / binding callers that want to
272
+ /// drive snapshot operations without naming the backend type parameter.
273
+ ///
274
+ /// `Database<S>` picks up a blanket impl when `S: Snapshotable + 'static`.
275
+ /// Transports (e.g. `lora-server`) type-erase on `Arc<dyn SnapshotAdmin>`.
276
+ pub trait SnapshotAdmin: Send + Sync + 'static {
277
+ fn save_snapshot(&self, path: &Path) -> Result<SnapshotMeta>;
278
+ fn load_snapshot(&self, path: &Path) -> Result<SnapshotMeta>;
279
+ }
280
+
281
+ impl<S> SnapshotAdmin for Database<S>
282
+ where
283
+ S: GraphStorage + GraphStorageMut + Snapshotable + Send + 'static,
284
+ {
285
+ fn save_snapshot(&self, path: &Path) -> Result<SnapshotMeta> {
286
+ self.save_snapshot_to(path)
287
+ }
288
+
289
+ fn load_snapshot(&self, path: &Path) -> Result<SnapshotMeta> {
290
+ self.load_snapshot_from(path)
291
+ }
292
+ }
293
+
294
+ impl<S> QueryRunner for Database<S>
295
+ where
296
+ S: GraphStorage + GraphStorageMut + Send + 'static,
297
+ {
298
+ fn execute(&self, query: &str, options: Option<ExecuteOptions>) -> Result<QueryResult> {
299
+ Database::execute(self, query, options)
300
+ }
301
+ }
@@ -16,7 +16,7 @@
16
16
 
17
17
  mod database;
18
18
 
19
- pub use database::{Database, QueryRunner};
19
+ pub use database::{Database, QueryRunner, SnapshotAdmin};
20
20
 
21
21
  // Re-export the core execution types so callers don't need a direct
22
22
  // dependency on `lora-executor`.
@@ -26,5 +26,9 @@ pub use lora_executor::{ExecuteOptions, LoraValue, QueryResult, ResultFormat};
26
26
  // depend on `lora-database` for the happy path.
27
27
  pub use lora_store::InMemoryGraph;
28
28
 
29
+ // Snapshot surface — re-exported so bindings/servers don't need a direct
30
+ // `lora-store` dependency just to name the meta / error types.
31
+ pub use lora_store::{SnapshotError, SnapshotMeta, Snapshotable};
32
+
29
33
  // Standalone parsing entry point (does not require building a `Database`).
30
34
  pub use lora_parser::parse_query;