lora-python 0.2.0__tar.gz → 0.4.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 (112) hide show
  1. {lora_python-0.2.0 → lora_python-0.4.0}/Cargo.lock +48 -13
  2. {lora_python-0.2.0 → lora_python-0.4.0}/Cargo.toml +12 -9
  3. {lora_python-0.2.0 → lora_python-0.4.0}/PKG-INFO +36 -2
  4. {lora_python-0.2.0 → lora_python-0.4.0}/README.md +35 -1
  5. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/analyzer.rs +3 -3
  6. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/Cargo.toml +5 -0
  7. lora_python-0.4.0/crates/lora-database/benches/wal_benchmarks.rs +205 -0
  8. lora_python-0.4.0/crates/lora-database/src/database.rs +770 -0
  9. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/src/lib.rs +10 -1
  10. lora_python-0.4.0/crates/lora-database/tests/backend_stub.rs +388 -0
  11. lora_python-0.4.0/crates/lora-database/tests/snapshot.rs +195 -0
  12. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/vectors.rs +1 -1
  13. lora_python-0.4.0/crates/lora-database/tests/wal.rs +465 -0
  14. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/src/eval.rs +85 -84
  15. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/src/executor.rs +139 -102
  16. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/README.md +35 -1
  17. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/examples/basic.py +2 -1
  18. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/src/lib.rs +102 -28
  19. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/tests/test_async.py +26 -0
  20. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/tests/test_sync.py +58 -0
  21. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-store/Cargo.toml +4 -0
  22. lora_python-0.4.0/crates/lora-store/src/graph.rs +837 -0
  23. lora_python-0.4.0/crates/lora-store/src/lib.rs +18 -0
  24. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-store/src/memory.rs +706 -161
  25. lora_python-0.4.0/crates/lora-store/src/mutation.rs +133 -0
  26. lora_python-0.4.0/crates/lora-store/src/snapshot.rs +458 -0
  27. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-store/src/spatial.rs +1 -1
  28. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-store/src/temporal.rs +6 -6
  29. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-store/src/vector.rs +5 -3
  30. lora_python-0.4.0/crates/lora-wal/Cargo.toml +19 -0
  31. lora_python-0.4.0/crates/lora-wal/src/config.rs +64 -0
  32. lora_python-0.4.0/crates/lora-wal/src/dir.rs +194 -0
  33. lora_python-0.4.0/crates/lora-wal/src/error.rs +51 -0
  34. lora_python-0.4.0/crates/lora-wal/src/lib.rs +50 -0
  35. lora_python-0.4.0/crates/lora-wal/src/lock.rs +104 -0
  36. lora_python-0.4.0/crates/lora-wal/src/lsn.rs +87 -0
  37. lora_python-0.4.0/crates/lora-wal/src/record.rs +443 -0
  38. lora_python-0.4.0/crates/lora-wal/src/recorder_adapter.rs +479 -0
  39. lora_python-0.4.0/crates/lora-wal/src/replay.rs +260 -0
  40. lora_python-0.4.0/crates/lora-wal/src/segment.rs +543 -0
  41. lora_python-0.4.0/crates/lora-wal/src/testing.rs +39 -0
  42. lora_python-0.4.0/crates/lora-wal/src/wal.rs +1002 -0
  43. {lora_python-0.2.0 → lora_python-0.4.0}/pyproject.toml +1 -1
  44. {lora_python-0.2.0 → lora_python-0.4.0}/python/lora_python/__init__.py +7 -1
  45. {lora_python-0.2.0 → lora_python-0.4.0}/python/lora_python/_async.py +35 -6
  46. {lora_python-0.2.0 → lora_python-0.4.0}/python/lora_python/_native.pyi +6 -3
  47. {lora_python-0.2.0 → lora_python-0.4.0}/python/lora_python/types.py +18 -0
  48. lora_python-0.2.0/crates/lora-database/src/database.rs +0 -105
  49. lora_python-0.2.0/crates/lora-store/src/graph.rs +0 -649
  50. lora_python-0.2.0/crates/lora-store/src/lib.rs +0 -11
  51. {lora_python-0.2.0 → lora_python-0.4.0}/LICENSE +0 -0
  52. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/Cargo.toml +0 -0
  53. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/errors.rs +0 -0
  54. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/lib.rs +0 -0
  55. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/resolved.rs +0 -0
  56. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/scope.rs +0 -0
  57. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-analyzer/src/symbols.rs +0 -0
  58. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-ast/Cargo.toml +0 -0
  59. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-ast/src/ast.rs +0 -0
  60. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-ast/src/lib.rs +0 -0
  61. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/Cargo.toml +0 -0
  62. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/lib.rs +0 -0
  63. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/logical.rs +0 -0
  64. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/optimizer.rs +0 -0
  65. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/pattern.rs +0 -0
  66. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/physical.rs +0 -0
  67. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-compiler/src/planner.rs +0 -0
  68. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
  69. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
  70. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/fixtures.rs +0 -0
  71. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
  72. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
  73. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
  74. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
  75. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/advanced_queries.rs +0 -0
  76. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/aggregation.rs +0 -0
  77. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/create.rs +0 -0
  78. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/errors.rs +0 -0
  79. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/expressions.rs +0 -0
  80. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/functions_extended.rs +0 -0
  81. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/invariants.rs +0 -0
  82. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/match.rs +0 -0
  83. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/merge.rs +0 -0
  84. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/ordering.rs +0 -0
  85. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/parameters.rs +0 -0
  86. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/parser.rs +0 -0
  87. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/paths.rs +0 -0
  88. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/projection.rs +0 -0
  89. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/seeds.rs +0 -0
  90. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/spatial.rs +0 -0
  91. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/temporal.rs +0 -0
  92. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/test_helpers.rs +0 -0
  93. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/types_advanced.rs +0 -0
  94. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/union.rs +0 -0
  95. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/update.rs +0 -0
  96. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/where_clause.rs +0 -0
  97. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-database/tests/with.rs +0 -0
  98. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/Cargo.toml +0 -0
  99. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/src/errors.rs +0 -0
  100. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/src/lib.rs +0 -0
  101. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-executor/src/value.rs +0 -0
  102. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-parser/Cargo.toml +0 -0
  103. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-parser/src/cypher.pest +0 -0
  104. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-parser/src/error.rs +0 -0
  105. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-parser/src/lib.rs +0 -0
  106. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-parser/src/parser.rs +0 -0
  107. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/.gitignore +0 -0
  108. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/Cargo.toml +0 -0
  109. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/LICENSE +0 -0
  110. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/build.rs +0 -0
  111. {lora_python-0.2.0 → lora_python-0.4.0}/crates/lora-python/examples/async_demo.py +0 -0
  112. {lora_python-0.2.0 → lora_python-0.4.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.4.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.4.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.4.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.4.0"
640
658
  dependencies = [
641
659
  "anyhow",
642
660
  "criterion",
@@ -646,13 +664,14 @@ dependencies = [
646
664
  "lora-executor",
647
665
  "lora-parser",
648
666
  "lora-store",
667
+ "lora-wal",
649
668
  "serde_json",
650
669
  "tower",
651
670
  ]
652
671
 
653
672
  [[package]]
654
673
  name = "lora-executor"
655
- version = "0.2.0"
674
+ version = "0.4.0"
656
675
  dependencies = [
657
676
  "lora-analyzer",
658
677
  "lora-ast",
@@ -666,7 +685,7 @@ dependencies = [
666
685
 
667
686
  [[package]]
668
687
  name = "lora-ffi"
669
- version = "0.2.0"
688
+ version = "0.4.0"
670
689
  dependencies = [
671
690
  "lora-database",
672
691
  "lora-store",
@@ -676,7 +695,7 @@ dependencies = [
676
695
 
677
696
  [[package]]
678
697
  name = "lora-node"
679
- version = "0.2.0"
698
+ version = "0.4.0"
680
699
  dependencies = [
681
700
  "anyhow",
682
701
  "lora-database",
@@ -690,7 +709,7 @@ dependencies = [
690
709
 
691
710
  [[package]]
692
711
  name = "lora-parser"
693
- version = "0.2.0"
712
+ version = "0.4.0"
694
713
  dependencies = [
695
714
  "lora-ast",
696
715
  "pest",
@@ -701,7 +720,7 @@ dependencies = [
701
720
 
702
721
  [[package]]
703
722
  name = "lora-python"
704
- version = "0.2.0"
723
+ version = "0.4.0"
705
724
  dependencies = [
706
725
  "anyhow",
707
726
  "lora-database",
@@ -713,10 +732,11 @@ dependencies = [
713
732
 
714
733
  [[package]]
715
734
  name = "lora-server"
716
- version = "0.2.0"
735
+ version = "0.4.0"
717
736
  dependencies = [
718
737
  "anyhow",
719
738
  "axum",
739
+ "http-body-util",
720
740
  "lora-database",
721
741
  "serde",
722
742
  "serde_json",
@@ -726,15 +746,30 @@ dependencies = [
726
746
 
727
747
  [[package]]
728
748
  name = "lora-store"
729
- version = "0.2.0"
749
+ version = "0.4.0"
730
750
  dependencies = [
751
+ "bincode",
752
+ "crc32fast",
731
753
  "js-sys",
732
754
  "lora-ast",
755
+ "serde",
756
+ "thiserror",
757
+ ]
758
+
759
+ [[package]]
760
+ name = "lora-wal"
761
+ version = "0.4.0"
762
+ dependencies = [
763
+ "bincode",
764
+ "crc32fast",
765
+ "lora-store",
766
+ "serde",
767
+ "thiserror",
733
768
  ]
734
769
 
735
770
  [[package]]
736
771
  name = "lora-wasm"
737
- version = "0.2.0"
772
+ version = "0.4.0"
738
773
  dependencies = [
739
774
  "anyhow",
740
775
  "console_error_panic_hook",
@@ -749,7 +784,7 @@ dependencies = [
749
784
 
750
785
  [[package]]
751
786
  name = "lora_ruby"
752
- version = "0.2.0"
787
+ version = "0.4.0"
753
788
  dependencies = [
754
789
  "anyhow",
755
790
  "lora-database",
@@ -1,10 +1,10 @@
1
1
  [workspace]
2
- members = ["crates/lora-ast", "crates/lora-parser", "crates/lora-analyzer", "crates/lora-compiler", "crates/lora-store", "crates/lora-executor", "crates/lora-database", "crates/lora-python"]
2
+ members = ["crates/lora-ast", "crates/lora-parser", "crates/lora-analyzer", "crates/lora-compiler", "crates/lora-store", "crates/lora-wal", "crates/lora-executor", "crates/lora-database", "crates/lora-python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2021"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  license = "BUSL-1.1"
9
9
  authors = ["LoraDB, Inc."]
10
10
  repository = "https://github.com/lora-db/lora"
@@ -15,13 +15,14 @@ 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.4.0" }
19
+ lora-parser = { path = "crates/lora-parser", version = "=0.4.0" }
20
+ lora-analyzer = { path = "crates/lora-analyzer", version = "=0.4.0" }
21
+ lora-compiler = { path = "crates/lora-compiler", version = "=0.4.0" }
22
+ lora-store = { path = "crates/lora-store", version = "=0.4.0" }
23
+ lora-wal = { path = "crates/lora-wal", version = "=0.4.0" }
24
+ lora-executor = { path = "crates/lora-executor", version = "=0.4.0" }
25
+ lora-database = { path = "crates/lora-database", version = "=0.4.0" }
25
26
 
26
27
  # External crates.
27
28
  anyhow = "1"
@@ -37,3 +38,5 @@ serde_json = "1.0.149"
37
38
  regex = "1"
38
39
  tower = "0.5.3"
39
40
  criterion = { version = "0.8.2", features = ["html_reports"] }
41
+ bincode = "1.3"
42
+ 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.4.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -30,7 +30,7 @@ Project-URL: Repository, https://github.com/lora-db/lora
30
30
 
31
31
  # lora-python
32
32
 
33
- Python bindings for the [Lora](../../README.md) in-memory graph
33
+ Python bindings for the [Lora](../../README.md) graph
34
34
  engine. Ships both a synchronous PyO3 `Database` class and an
35
35
  asyncio-compatible `AsyncDatabase` wrapper that never blocks the event loop.
36
36
 
@@ -64,6 +64,18 @@ for row in res["rows"]:
64
64
  print(n["properties"]["name"])
65
65
  ```
66
66
 
67
+ Initialization rule:
68
+
69
+ ```python
70
+ from lora_python import Database
71
+
72
+ scratch = Database.create() # in-memory
73
+ persistent = Database.create("./app") # persistent: directory string
74
+ ```
75
+
76
+ If you want persistence, pass a directory string to `Database.create(...)`
77
+ or `Database(...)`.
78
+
67
79
  ## Async usage (non-blocking)
68
80
 
69
81
  ```python
@@ -79,6 +91,13 @@ async def main():
79
91
  asyncio.run(main())
80
92
  ```
81
93
 
94
+ Async initialization follows the same rule:
95
+
96
+ ```python
97
+ db = await AsyncDatabase.create() # in-memory
98
+ db = await AsyncDatabase.create("./app") # persistent: directory string
99
+ ```
100
+
82
101
  `AsyncDatabase.execute` dispatches the query onto the default asyncio
83
102
  thread pool via `asyncio.to_thread`. The PyO3 `Database.execute` releases
84
103
  the Python GIL for the duration of engine work, so other coroutines on the
@@ -125,6 +144,21 @@ Constructors and guards are exported from `lora_python.types`:
125
144
 
126
145
  All three are available as `lora_python.LoraError`, etc.
127
146
 
147
+ ## Persistence
148
+
149
+ `Database.create("./app")`, `Database("./app")`, and
150
+ `await AsyncDatabase.create("./app")` open or create a WAL-backed
151
+ persistent database rooted at that directory. Reopening the same path
152
+ replays committed writes before returning the handle.
153
+
154
+ Call `db.close()` / `await db.close()` before reopening the same WAL
155
+ directory inside one process.
156
+
157
+ This first Python persistence slice intentionally stays small: the
158
+ binding exposes WAL-backed initialization plus the existing
159
+ `save_snapshot` / `load_snapshot` APIs, but not checkpoint, truncate,
160
+ status, or sync-mode controls.
161
+
128
162
  ## Architecture
129
163
 
130
164
  ```
@@ -1,6 +1,6 @@
1
1
  # lora-python
2
2
 
3
- Python bindings for the [Lora](../../README.md) in-memory graph
3
+ Python bindings for the [Lora](../../README.md) graph
4
4
  engine. Ships both a synchronous PyO3 `Database` class and an
5
5
  asyncio-compatible `AsyncDatabase` wrapper that never blocks the event loop.
6
6
 
@@ -34,6 +34,18 @@ for row in res["rows"]:
34
34
  print(n["properties"]["name"])
35
35
  ```
36
36
 
37
+ Initialization rule:
38
+
39
+ ```python
40
+ from lora_python import Database
41
+
42
+ scratch = Database.create() # in-memory
43
+ persistent = Database.create("./app") # persistent: directory string
44
+ ```
45
+
46
+ If you want persistence, pass a directory string to `Database.create(...)`
47
+ or `Database(...)`.
48
+
37
49
  ## Async usage (non-blocking)
38
50
 
39
51
  ```python
@@ -49,6 +61,13 @@ async def main():
49
61
  asyncio.run(main())
50
62
  ```
51
63
 
64
+ Async initialization follows the same rule:
65
+
66
+ ```python
67
+ db = await AsyncDatabase.create() # in-memory
68
+ db = await AsyncDatabase.create("./app") # persistent: directory string
69
+ ```
70
+
52
71
  `AsyncDatabase.execute` dispatches the query onto the default asyncio
53
72
  thread pool via `asyncio.to_thread`. The PyO3 `Database.execute` releases
54
73
  the Python GIL for the duration of engine work, so other coroutines on the
@@ -95,6 +114,21 @@ Constructors and guards are exported from `lora_python.types`:
95
114
 
96
115
  All three are available as `lora_python.LoraError`, etc.
97
116
 
117
+ ## Persistence
118
+
119
+ `Database.create("./app")`, `Database("./app")`, and
120
+ `await AsyncDatabase.create("./app")` open or create a WAL-backed
121
+ persistent database rooted at that directory. Reopening the same path
122
+ replays committed writes before returning the handle.
123
+
124
+ Call `db.close()` / `await db.close()` before reopening the same WAL
125
+ directory inside one process.
126
+
127
+ This first Python persistence slice intentionally stays small: the
128
+ binding exposes WAL-backed initialization plus the existing
129
+ `save_snapshot` / `load_snapshot` APIs, but not checkpoint, truncate,
130
+ status, or sync-mode controls.
131
+
98
132
  ## Architecture
99
133
 
100
134
  ```
@@ -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,
@@ -23,6 +23,7 @@ lora-analyzer.workspace = true
23
23
  lora-compiler.workspace = true
24
24
  lora-executor.workspace = true
25
25
  lora-store.workspace = true
26
+ lora-wal.workspace = true
26
27
 
27
28
  serde_json.workspace = true
28
29
  tower.workspace = true
@@ -49,3 +50,7 @@ harness = false
49
50
  [[bench]]
50
51
  name = "perf_smoke_benchmarks"
51
52
  harness = false
53
+
54
+ [[bench]]
55
+ name = "wal_benchmarks"
56
+ harness = false
@@ -0,0 +1,205 @@
1
+ //! WAL-aware microbenchmarks.
2
+ //!
3
+ //! These exercise the four durability profiles end-to-end through
4
+ //! `Database::execute_with_params`:
5
+ //!
6
+ //! - **`no_wal`** — `Database::in_memory()` (the existing fast path;
7
+ //! serves as the baseline the others are compared against).
8
+ //! - **`per_commit`** — `WalConfig::Enabled` with `SyncMode::PerCommit`
9
+ //! (fsync before every commit returns).
10
+ //! - **`group`** — `WalConfig::Enabled` with `SyncMode::Group`
11
+ //! (write-only on commit, bg flusher fsyncs).
12
+ //! - **`none`** — `WalConfig::Enabled` with `SyncMode::None`
13
+ //! (no fsync at all, OS-buffered).
14
+ //!
15
+ //! The shape that matters is *commit latency* — every iteration runs a
16
+ //! single tiny `CREATE` statement so the engine work is negligible and
17
+ //! the WAL path dominates. On NVMe the gap between `no_wal` and
18
+ //! `per_commit` is roughly the cost of one `fsync` (50–200 µs); the
19
+ //! gap between `per_commit` and `group` / `none` measures how much
20
+ //! you save by deferring durability.
21
+ //!
22
+ //! There is also a small **`recovery`** bench that times opening a WAL
23
+ //! with N committed transactions and re-applying them. This is the
24
+ //! number that bounds startup time on a fresh process.
25
+ //!
26
+ //! Run with:
27
+ //! `cargo bench -p lora-database --bench wal_benchmarks`
28
+
29
+ use std::path::PathBuf;
30
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
+
32
+ use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
33
+ use lora_database::{Database, ExecuteOptions, ResultFormat, SyncMode, WalConfig};
34
+ use std::hint::black_box;
35
+
36
+ fn opts() -> Option<ExecuteOptions> {
37
+ Some(ExecuteOptions {
38
+ format: ResultFormat::Rows,
39
+ })
40
+ }
41
+
42
+ /// Per-iteration scratch directory. Criterion's `iter_batched_setup`
43
+ /// runs setup once per batch, so this is called once per Criterion
44
+ /// "sample" — fine even at sample_size=20.
45
+ struct ScratchDir {
46
+ path: PathBuf,
47
+ }
48
+
49
+ impl ScratchDir {
50
+ fn new(tag: &str) -> Self {
51
+ let mut path = std::env::temp_dir();
52
+ path.push(format!(
53
+ "lora-wal-bench-{}-{}-{}",
54
+ tag,
55
+ std::process::id(),
56
+ SystemTime::now()
57
+ .duration_since(UNIX_EPOCH)
58
+ .unwrap()
59
+ .as_nanos()
60
+ ));
61
+ std::fs::create_dir_all(&path).unwrap();
62
+ Self { path }
63
+ }
64
+ }
65
+
66
+ impl Drop for ScratchDir {
67
+ fn drop(&mut self) {
68
+ let _ = std::fs::remove_dir_all(&self.path);
69
+ }
70
+ }
71
+
72
+ fn enabled(dir: &std::path::Path, sync_mode: SyncMode) -> WalConfig {
73
+ WalConfig::Enabled {
74
+ dir: dir.to_path_buf(),
75
+ sync_mode,
76
+ segment_target_bytes: 8 * 1024 * 1024,
77
+ }
78
+ }
79
+
80
+ fn smoke_config() -> Criterion {
81
+ // Per-bench budget of ~3 s. fsync cost dominates per_commit so we
82
+ // cannot run this as cheaply as the in-memory smoke suite, but we
83
+ // also don't want a 30-second bench on every CI run.
84
+ Criterion::default()
85
+ .warm_up_time(Duration::from_millis(500))
86
+ .measurement_time(Duration::from_millis(2_500))
87
+ .sample_size(20)
88
+ .noise_threshold(0.10)
89
+ }
90
+
91
+ fn bench_commit_latency(c: &mut Criterion) {
92
+ let mut group = c.benchmark_group("wal/commit_latency");
93
+
94
+ // ---- baseline: no WAL ---------------------------------------------------
95
+ {
96
+ group.bench_function("no_wal", |b| {
97
+ b.iter_batched(
98
+ Database::in_memory,
99
+ |db| {
100
+ black_box(db.execute("CREATE (:N {v: 1})", opts()).unwrap());
101
+ },
102
+ BatchSize::SmallInput,
103
+ );
104
+ });
105
+ }
106
+
107
+ // ---- PerCommit (fsync per commit) --------------------------------------
108
+ {
109
+ group.bench_function("per_commit", |b| {
110
+ b.iter_batched(
111
+ || {
112
+ let dir = ScratchDir::new("per-commit");
113
+ let db =
114
+ Database::open_with_wal(enabled(&dir.path, SyncMode::PerCommit)).unwrap();
115
+ (dir, db)
116
+ },
117
+ |(_dir, db)| {
118
+ black_box(db.execute("CREATE (:N {v: 1})", opts()).unwrap());
119
+ },
120
+ BatchSize::SmallInput,
121
+ );
122
+ });
123
+ }
124
+
125
+ // ---- Group (write-only on commit, bg flusher fsyncs) -------------------
126
+ {
127
+ group.bench_function("group", |b| {
128
+ b.iter_batched(
129
+ || {
130
+ let dir = ScratchDir::new("group");
131
+ let db = Database::open_with_wal(enabled(
132
+ &dir.path,
133
+ SyncMode::Group { interval_ms: 50 },
134
+ ))
135
+ .unwrap();
136
+ (dir, db)
137
+ },
138
+ |(_dir, db)| {
139
+ black_box(db.execute("CREATE (:N {v: 1})", opts()).unwrap());
140
+ },
141
+ BatchSize::SmallInput,
142
+ );
143
+ });
144
+ }
145
+
146
+ // ---- None (no fsync at all) --------------------------------------------
147
+ {
148
+ group.bench_function("none", |b| {
149
+ b.iter_batched(
150
+ || {
151
+ let dir = ScratchDir::new("none");
152
+ let db = Database::open_with_wal(enabled(&dir.path, SyncMode::None)).unwrap();
153
+ (dir, db)
154
+ },
155
+ |(_dir, db)| {
156
+ black_box(db.execute("CREATE (:N {v: 1})", opts()).unwrap());
157
+ },
158
+ BatchSize::SmallInput,
159
+ );
160
+ });
161
+ }
162
+
163
+ group.finish();
164
+ }
165
+
166
+ fn bench_recovery(c: &mut Criterion) {
167
+ let mut group = c.benchmark_group("wal/recovery");
168
+
169
+ // Time how long Database::open_with_wal takes on a directory that
170
+ // has N committed transactions waiting to be replayed. This bounds
171
+ // startup time on a crash-recovery boot.
172
+ for n in [100usize, 1_000].iter().copied() {
173
+ // Build the WAL once outside the iter loop.
174
+ let dir = ScratchDir::new(&format!("recovery-{}", n));
175
+ {
176
+ let db = Database::open_with_wal(enabled(&dir.path, SyncMode::PerCommit)).unwrap();
177
+ for _ in 0..n {
178
+ db.execute("CREATE (:N {v: 1})", opts()).unwrap();
179
+ }
180
+ // Drop to release file handles; bg flusher (none here) joins.
181
+ drop(db);
182
+ }
183
+
184
+ group.bench_function(format!("replay_{}", n), |b| {
185
+ b.iter(|| {
186
+ let db = Database::open_with_wal(enabled(&dir.path, SyncMode::None)).unwrap();
187
+ black_box(db.node_count());
188
+ });
189
+ });
190
+
191
+ // Keep `dir` alive across the iter so the WAL files persist
192
+ // for every iteration. ScratchDir's Drop will clean up at
193
+ // bench end.
194
+ std::mem::forget(dir);
195
+ }
196
+
197
+ group.finish();
198
+ }
199
+
200
+ criterion_group! {
201
+ name = benches;
202
+ config = smoke_config();
203
+ targets = bench_commit_latency, bench_recovery,
204
+ }
205
+ criterion_main!(benches);