lora-python 0.3.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 (110) hide show
  1. {lora_python-0.3.0 → lora_python-0.4.0}/Cargo.lock +25 -13
  2. {lora_python-0.3.0 → lora_python-0.4.0}/Cargo.toml +10 -9
  3. {lora_python-0.3.0 → lora_python-0.4.0}/PKG-INFO +36 -2
  4. {lora_python-0.3.0 → lora_python-0.4.0}/README.md +35 -1
  5. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/Cargo.toml +5 -0
  6. lora_python-0.4.0/crates/lora-database/benches/wal_benchmarks.rs +205 -0
  7. lora_python-0.4.0/crates/lora-database/src/database.rs +770 -0
  8. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/src/lib.rs +6 -1
  9. lora_python-0.4.0/crates/lora-database/tests/wal.rs +465 -0
  10. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/README.md +35 -1
  11. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/examples/basic.py +2 -1
  12. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/src/lib.rs +65 -23
  13. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/tests/test_async.py +26 -0
  14. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/tests/test_sync.py +58 -0
  15. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/memory.rs +118 -0
  16. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/mutation.rs +24 -6
  17. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/snapshot.rs +17 -0
  18. lora_python-0.4.0/crates/lora-wal/Cargo.toml +19 -0
  19. lora_python-0.4.0/crates/lora-wal/src/config.rs +64 -0
  20. lora_python-0.4.0/crates/lora-wal/src/dir.rs +194 -0
  21. lora_python-0.4.0/crates/lora-wal/src/error.rs +51 -0
  22. lora_python-0.4.0/crates/lora-wal/src/lib.rs +50 -0
  23. lora_python-0.4.0/crates/lora-wal/src/lock.rs +104 -0
  24. lora_python-0.4.0/crates/lora-wal/src/lsn.rs +87 -0
  25. lora_python-0.4.0/crates/lora-wal/src/record.rs +443 -0
  26. lora_python-0.4.0/crates/lora-wal/src/recorder_adapter.rs +479 -0
  27. lora_python-0.4.0/crates/lora-wal/src/replay.rs +260 -0
  28. lora_python-0.4.0/crates/lora-wal/src/segment.rs +543 -0
  29. lora_python-0.4.0/crates/lora-wal/src/testing.rs +39 -0
  30. lora_python-0.4.0/crates/lora-wal/src/wal.rs +1002 -0
  31. {lora_python-0.3.0 → lora_python-0.4.0}/pyproject.toml +1 -1
  32. {lora_python-0.3.0 → lora_python-0.4.0}/python/lora_python/__init__.py +5 -1
  33. {lora_python-0.3.0 → lora_python-0.4.0}/python/lora_python/_async.py +15 -5
  34. {lora_python-0.3.0 → lora_python-0.4.0}/python/lora_python/_native.pyi +6 -3
  35. lora_python-0.3.0/crates/lora-database/src/database.rs +0 -301
  36. {lora_python-0.3.0 → lora_python-0.4.0}/LICENSE +0 -0
  37. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/Cargo.toml +0 -0
  38. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/analyzer.rs +0 -0
  39. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/errors.rs +0 -0
  40. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/lib.rs +0 -0
  41. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/resolved.rs +0 -0
  42. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/scope.rs +0 -0
  43. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-analyzer/src/symbols.rs +0 -0
  44. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-ast/Cargo.toml +0 -0
  45. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-ast/src/ast.rs +0 -0
  46. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-ast/src/lib.rs +0 -0
  47. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/Cargo.toml +0 -0
  48. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/lib.rs +0 -0
  49. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/logical.rs +0 -0
  50. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/optimizer.rs +0 -0
  51. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/pattern.rs +0 -0
  52. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/physical.rs +0 -0
  53. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-compiler/src/planner.rs +0 -0
  54. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
  55. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
  56. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/fixtures.rs +0 -0
  57. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
  58. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
  59. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
  60. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
  61. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/advanced_queries.rs +0 -0
  62. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/aggregation.rs +0 -0
  63. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/backend_stub.rs +0 -0
  64. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/create.rs +0 -0
  65. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/errors.rs +0 -0
  66. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/expressions.rs +0 -0
  67. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/functions_extended.rs +0 -0
  68. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/invariants.rs +0 -0
  69. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/match.rs +0 -0
  70. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/merge.rs +0 -0
  71. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/ordering.rs +0 -0
  72. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/parameters.rs +0 -0
  73. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/parser.rs +0 -0
  74. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/paths.rs +0 -0
  75. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/projection.rs +0 -0
  76. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/seeds.rs +0 -0
  77. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/snapshot.rs +0 -0
  78. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/spatial.rs +0 -0
  79. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/temporal.rs +0 -0
  80. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/test_helpers.rs +0 -0
  81. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/types_advanced.rs +0 -0
  82. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/union.rs +0 -0
  83. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/update.rs +0 -0
  84. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/vectors.rs +0 -0
  85. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/where_clause.rs +0 -0
  86. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-database/tests/with.rs +0 -0
  87. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/Cargo.toml +0 -0
  88. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/src/errors.rs +0 -0
  89. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/src/eval.rs +0 -0
  90. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/src/executor.rs +0 -0
  91. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/src/lib.rs +0 -0
  92. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-executor/src/value.rs +0 -0
  93. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-parser/Cargo.toml +0 -0
  94. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-parser/src/cypher.pest +0 -0
  95. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-parser/src/error.rs +0 -0
  96. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-parser/src/lib.rs +0 -0
  97. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-parser/src/parser.rs +0 -0
  98. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/.gitignore +0 -0
  99. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/Cargo.toml +0 -0
  100. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/LICENSE +0 -0
  101. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/build.rs +0 -0
  102. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-python/examples/async_demo.py +0 -0
  103. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/Cargo.toml +0 -0
  104. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/graph.rs +0 -0
  105. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/lib.rs +0 -0
  106. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/spatial.rs +0 -0
  107. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/temporal.rs +0 -0
  108. {lora_python-0.3.0 → lora_python-0.4.0}/crates/lora-store/src/vector.rs +0 -0
  109. {lora_python-0.3.0 → lora_python-0.4.0}/python/lora_python/py.typed +0 -0
  110. {lora_python-0.3.0 → lora_python-0.4.0}/python/lora_python/types.py +0 -0
@@ -629,7 +629,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
629
629
 
630
630
  [[package]]
631
631
  name = "lora-analyzer"
632
- version = "0.3.0"
632
+ version = "0.4.0"
633
633
  dependencies = [
634
634
  "lora-ast",
635
635
  "lora-parser",
@@ -639,14 +639,14 @@ dependencies = [
639
639
 
640
640
  [[package]]
641
641
  name = "lora-ast"
642
- version = "0.3.0"
642
+ version = "0.4.0"
643
643
  dependencies = [
644
644
  "smallvec 2.0.0-alpha.12",
645
645
  ]
646
646
 
647
647
  [[package]]
648
648
  name = "lora-compiler"
649
- version = "0.3.0"
649
+ version = "0.4.0"
650
650
  dependencies = [
651
651
  "lora-analyzer",
652
652
  "lora-ast",
@@ -654,7 +654,7 @@ dependencies = [
654
654
 
655
655
  [[package]]
656
656
  name = "lora-database"
657
- version = "0.3.0"
657
+ version = "0.4.0"
658
658
  dependencies = [
659
659
  "anyhow",
660
660
  "criterion",
@@ -664,13 +664,14 @@ dependencies = [
664
664
  "lora-executor",
665
665
  "lora-parser",
666
666
  "lora-store",
667
+ "lora-wal",
667
668
  "serde_json",
668
669
  "tower",
669
670
  ]
670
671
 
671
672
  [[package]]
672
673
  name = "lora-executor"
673
- version = "0.3.0"
674
+ version = "0.4.0"
674
675
  dependencies = [
675
676
  "lora-analyzer",
676
677
  "lora-ast",
@@ -684,7 +685,7 @@ dependencies = [
684
685
 
685
686
  [[package]]
686
687
  name = "lora-ffi"
687
- version = "0.3.0"
688
+ version = "0.4.0"
688
689
  dependencies = [
689
690
  "lora-database",
690
691
  "lora-store",
@@ -694,7 +695,7 @@ dependencies = [
694
695
 
695
696
  [[package]]
696
697
  name = "lora-node"
697
- version = "0.3.0"
698
+ version = "0.4.0"
698
699
  dependencies = [
699
700
  "anyhow",
700
701
  "lora-database",
@@ -708,7 +709,7 @@ dependencies = [
708
709
 
709
710
  [[package]]
710
711
  name = "lora-parser"
711
- version = "0.3.0"
712
+ version = "0.4.0"
712
713
  dependencies = [
713
714
  "lora-ast",
714
715
  "pest",
@@ -719,7 +720,7 @@ dependencies = [
719
720
 
720
721
  [[package]]
721
722
  name = "lora-python"
722
- version = "0.3.0"
723
+ version = "0.4.0"
723
724
  dependencies = [
724
725
  "anyhow",
725
726
  "lora-database",
@@ -731,7 +732,7 @@ dependencies = [
731
732
 
732
733
  [[package]]
733
734
  name = "lora-server"
734
- version = "0.3.0"
735
+ version = "0.4.0"
735
736
  dependencies = [
736
737
  "anyhow",
737
738
  "axum",
@@ -745,7 +746,7 @@ dependencies = [
745
746
 
746
747
  [[package]]
747
748
  name = "lora-store"
748
- version = "0.3.0"
749
+ version = "0.4.0"
749
750
  dependencies = [
750
751
  "bincode",
751
752
  "crc32fast",
@@ -755,9 +756,20 @@ dependencies = [
755
756
  "thiserror",
756
757
  ]
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",
768
+ ]
769
+
758
770
  [[package]]
759
771
  name = "lora-wasm"
760
- version = "0.3.0"
772
+ version = "0.4.0"
761
773
  dependencies = [
762
774
  "anyhow",
763
775
  "console_error_panic_hook",
@@ -772,7 +784,7 @@ dependencies = [
772
784
 
773
785
  [[package]]
774
786
  name = "lora_ruby"
775
- version = "0.3.0"
787
+ version = "0.4.0"
776
788
  dependencies = [
777
789
  "anyhow",
778
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.3.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.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" }
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lora-python
3
- Version: 0.3.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
  ```
@@ -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);