lora-python 0.5.0__tar.gz → 0.5.1__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 (116) hide show
  1. {lora_python-0.5.0 → lora_python-0.5.1}/Cargo.lock +14 -14
  2. {lora_python-0.5.0 → lora_python-0.5.1}/Cargo.toml +9 -9
  3. {lora_python-0.5.0 → lora_python-0.5.1}/PKG-INFO +11 -11
  4. {lora_python-0.5.0 → lora_python-0.5.1}/README.md +10 -10
  5. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/wal_benchmarks.rs +1 -1
  6. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/archive.rs +9 -3
  7. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/database.rs +3 -3
  8. lora_python-0.5.1/crates/lora-database/src/named.rs +204 -0
  9. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/wal.rs +72 -8
  10. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/README.md +10 -10
  11. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/examples/basic.py +1 -1
  12. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/recorder_adapter.rs +1 -1
  13. {lora_python-0.5.0 → lora_python-0.5.1}/pyproject.toml +1 -1
  14. {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/__init__.py +1 -1
  15. {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/_async.py +1 -1
  16. lora_python-0.5.0/crates/lora-database/src/named.rs +0 -124
  17. {lora_python-0.5.0 → lora_python-0.5.1}/LICENSE +0 -0
  18. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/Cargo.toml +0 -0
  19. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/analyzer.rs +0 -0
  20. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/errors.rs +0 -0
  21. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/lib.rs +0 -0
  22. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/resolved.rs +0 -0
  23. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/scope.rs +0 -0
  24. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/symbols.rs +0 -0
  25. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/Cargo.toml +0 -0
  26. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/src/ast.rs +0 -0
  27. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/src/lib.rs +0 -0
  28. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/Cargo.toml +0 -0
  29. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/lib.rs +0 -0
  30. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/logical.rs +0 -0
  31. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/optimizer.rs +0 -0
  32. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/pattern.rs +0 -0
  33. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/physical.rs +0 -0
  34. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/planner.rs +0 -0
  35. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/Cargo.toml +0 -0
  36. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
  37. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
  38. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/fixtures.rs +0 -0
  39. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
  40. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
  41. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
  42. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
  43. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/lib.rs +0 -0
  44. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/stream.rs +0 -0
  45. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/transaction.rs +0 -0
  46. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/advanced_queries.rs +0 -0
  47. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/aggregation.rs +0 -0
  48. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/backend_stub.rs +0 -0
  49. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/create.rs +0 -0
  50. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/errors.rs +0 -0
  51. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/expressions.rs +0 -0
  52. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/functions_extended.rs +0 -0
  53. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/invariants.rs +0 -0
  54. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/match.rs +0 -0
  55. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/merge.rs +0 -0
  56. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/ordering.rs +0 -0
  57. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/parameters.rs +0 -0
  58. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/parser.rs +0 -0
  59. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/paths.rs +0 -0
  60. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/projection.rs +0 -0
  61. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/seeds.rs +0 -0
  62. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/snapshot.rs +0 -0
  63. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/spatial.rs +0 -0
  64. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/temporal.rs +0 -0
  65. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/test_helpers.rs +0 -0
  66. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/transactions.rs +0 -0
  67. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/types_advanced.rs +0 -0
  68. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/union.rs +0 -0
  69. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/update.rs +0 -0
  70. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/vectors.rs +0 -0
  71. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/where_clause.rs +0 -0
  72. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/with.rs +0 -0
  73. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/Cargo.toml +0 -0
  74. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/errors.rs +0 -0
  75. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/eval.rs +0 -0
  76. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/executor.rs +0 -0
  77. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/lib.rs +0 -0
  78. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/pull.rs +0 -0
  79. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/value.rs +0 -0
  80. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/Cargo.toml +0 -0
  81. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/cypher.pest +0 -0
  82. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/error.rs +0 -0
  83. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/lib.rs +0 -0
  84. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/parser.rs +0 -0
  85. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/.gitignore +0 -0
  86. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/Cargo.toml +0 -0
  87. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/LICENSE +0 -0
  88. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/build.rs +0 -0
  89. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/examples/async_demo.py +0 -0
  90. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/src/lib.rs +0 -0
  91. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/tests/test_async.py +0 -0
  92. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/tests/test_sync.py +0 -0
  93. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/Cargo.toml +0 -0
  94. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/graph.rs +0 -0
  95. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/lib.rs +0 -0
  96. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/memory.rs +0 -0
  97. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/mutation.rs +0 -0
  98. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/snapshot.rs +0 -0
  99. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/spatial.rs +0 -0
  100. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/temporal.rs +0 -0
  101. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/vector.rs +0 -0
  102. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/Cargo.toml +0 -0
  103. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/config.rs +0 -0
  104. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/dir.rs +0 -0
  105. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/error.rs +0 -0
  106. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lib.rs +0 -0
  107. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lock.rs +0 -0
  108. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lsn.rs +0 -0
  109. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/record.rs +0 -0
  110. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/replay.rs +0 -0
  111. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/segment.rs +0 -0
  112. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/testing.rs +0 -0
  113. {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/wal.rs +0 -0
  114. {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/_native.pyi +0 -0
  115. {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/py.typed +0 -0
  116. {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/types.py +0 -0
@@ -651,7 +651,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
651
651
 
652
652
  [[package]]
653
653
  name = "lora-analyzer"
654
- version = "0.5.0"
654
+ version = "0.5.1"
655
655
  dependencies = [
656
656
  "lora-ast",
657
657
  "lora-parser",
@@ -661,14 +661,14 @@ dependencies = [
661
661
 
662
662
  [[package]]
663
663
  name = "lora-ast"
664
- version = "0.5.0"
664
+ version = "0.5.1"
665
665
  dependencies = [
666
666
  "smallvec 2.0.0-alpha.12",
667
667
  ]
668
668
 
669
669
  [[package]]
670
670
  name = "lora-compiler"
671
- version = "0.5.0"
671
+ version = "0.5.1"
672
672
  dependencies = [
673
673
  "lora-analyzer",
674
674
  "lora-ast",
@@ -676,7 +676,7 @@ dependencies = [
676
676
 
677
677
  [[package]]
678
678
  name = "lora-database"
679
- version = "0.5.0"
679
+ version = "0.5.1"
680
680
  dependencies = [
681
681
  "anyhow",
682
682
  "criterion",
@@ -694,7 +694,7 @@ dependencies = [
694
694
 
695
695
  [[package]]
696
696
  name = "lora-executor"
697
- version = "0.5.0"
697
+ version = "0.5.1"
698
698
  dependencies = [
699
699
  "lora-analyzer",
700
700
  "lora-ast",
@@ -708,7 +708,7 @@ dependencies = [
708
708
 
709
709
  [[package]]
710
710
  name = "lora-ffi"
711
- version = "0.5.0"
711
+ version = "0.5.1"
712
712
  dependencies = [
713
713
  "lora-database",
714
714
  "lora-store",
@@ -718,7 +718,7 @@ dependencies = [
718
718
 
719
719
  [[package]]
720
720
  name = "lora-node"
721
- version = "0.5.0"
721
+ version = "0.5.1"
722
722
  dependencies = [
723
723
  "anyhow",
724
724
  "lora-database",
@@ -732,7 +732,7 @@ dependencies = [
732
732
 
733
733
  [[package]]
734
734
  name = "lora-parser"
735
- version = "0.5.0"
735
+ version = "0.5.1"
736
736
  dependencies = [
737
737
  "lora-ast",
738
738
  "pest",
@@ -743,7 +743,7 @@ dependencies = [
743
743
 
744
744
  [[package]]
745
745
  name = "lora-python"
746
- version = "0.5.0"
746
+ version = "0.5.1"
747
747
  dependencies = [
748
748
  "anyhow",
749
749
  "lora-database",
@@ -755,7 +755,7 @@ dependencies = [
755
755
 
756
756
  [[package]]
757
757
  name = "lora-server"
758
- version = "0.5.0"
758
+ version = "0.5.1"
759
759
  dependencies = [
760
760
  "anyhow",
761
761
  "axum",
@@ -769,7 +769,7 @@ dependencies = [
769
769
 
770
770
  [[package]]
771
771
  name = "lora-store"
772
- version = "0.5.0"
772
+ version = "0.5.1"
773
773
  dependencies = [
774
774
  "bincode",
775
775
  "crc32fast",
@@ -781,7 +781,7 @@ dependencies = [
781
781
 
782
782
  [[package]]
783
783
  name = "lora-wal"
784
- version = "0.5.0"
784
+ version = "0.5.1"
785
785
  dependencies = [
786
786
  "bincode",
787
787
  "crc32fast",
@@ -792,7 +792,7 @@ dependencies = [
792
792
 
793
793
  [[package]]
794
794
  name = "lora-wasm"
795
- version = "0.5.0"
795
+ version = "0.5.1"
796
796
  dependencies = [
797
797
  "anyhow",
798
798
  "console_error_panic_hook",
@@ -807,7 +807,7 @@ dependencies = [
807
807
 
808
808
  [[package]]
809
809
  name = "lora_ruby"
810
- version = "0.5.0"
810
+ version = "0.5.1"
811
811
  dependencies = [
812
812
  "anyhow",
813
813
  "lora-database",
@@ -4,7 +4,7 @@ resolver = "2"
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2021"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  license = "BUSL-1.1"
9
9
  authors = ["LoraDB, Inc."]
10
10
  repository = "https://github.com/lora-db/lora"
@@ -15,14 +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.5.0" }
19
- lora-parser = { path = "crates/lora-parser", version = "=0.5.0" }
20
- lora-analyzer = { path = "crates/lora-analyzer", version = "=0.5.0" }
21
- lora-compiler = { path = "crates/lora-compiler", version = "=0.5.0" }
22
- lora-store = { path = "crates/lora-store", version = "=0.5.0" }
23
- lora-wal = { path = "crates/lora-wal", version = "=0.5.0" }
24
- lora-executor = { path = "crates/lora-executor", version = "=0.5.0" }
25
- lora-database = { path = "crates/lora-database", version = "=0.5.0" }
18
+ lora-ast = { path = "crates/lora-ast", version = "=0.5.1" }
19
+ lora-parser = { path = "crates/lora-parser", version = "=0.5.1" }
20
+ lora-analyzer = { path = "crates/lora-analyzer", version = "=0.5.1" }
21
+ lora-compiler = { path = "crates/lora-compiler", version = "=0.5.1" }
22
+ lora-store = { path = "crates/lora-store", version = "=0.5.1" }
23
+ lora-wal = { path = "crates/lora-wal", version = "=0.5.1" }
24
+ lora-executor = { path = "crates/lora-executor", version = "=0.5.1" }
25
+ lora-database = { path = "crates/lora-database", version = "=0.5.1" }
26
26
 
27
27
  # External crates.
28
28
  anyhow = "1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lora-python
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -70,11 +70,11 @@ Initialization rule:
70
70
  from lora_python import Database
71
71
 
72
72
  scratch = Database.create() # in-memory
73
- persistent = Database.create("./app") # persistent: directory string
73
+ persistent = Database.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
74
74
  ```
75
75
 
76
- If you want persistence, pass a directory string to `Database.create(...)`
77
- or `Database(...)`.
76
+ If you want persistence, pass a database name and `database_dir` to
77
+ `Database.create(...)` or `Database(...)`.
78
78
 
79
79
  ## Async usage (non-blocking)
80
80
 
@@ -95,7 +95,7 @@ Async initialization follows the same rule:
95
95
 
96
96
  ```python
97
97
  db = await AsyncDatabase.create() # in-memory
98
- db = await AsyncDatabase.create("./app") # persistent: directory string
98
+ db = await AsyncDatabase.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
99
99
  ```
100
100
 
101
101
  `AsyncDatabase.execute` dispatches the query onto the default asyncio
@@ -146,16 +146,16 @@ All three are available as `lora_python.LoraError`, etc.
146
146
 
147
147
  ## Persistence
148
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
149
+ `Database.create("app", {"database_dir": "./data"})`, `Database("app", {"database_dir": "./data"})`, and
150
+ `await AsyncDatabase.create("app", {"database_dir": "./data"})` open or create
151
+ an archive-backed persistent database at `./data/app.loradb`. Reopening the same path
152
152
  replays committed writes before returning the handle.
153
153
 
154
- Call `db.close()` / `await db.close()` before reopening the same WAL
155
- directory inside one process.
154
+ Call `db.close()` / `await db.close()` before reopening the same archive
155
+ inside one process.
156
156
 
157
157
  This first Python persistence slice intentionally stays small: the
158
- binding exposes WAL-backed initialization plus the existing
158
+ binding exposes archive-backed initialization plus the existing
159
159
  `save_snapshot` / `load_snapshot` APIs, but not checkpoint, truncate,
160
160
  status, or sync-mode controls.
161
161
 
@@ -40,11 +40,11 @@ Initialization rule:
40
40
  from lora_python import Database
41
41
 
42
42
  scratch = Database.create() # in-memory
43
- persistent = Database.create("./app") # persistent: directory string
43
+ persistent = Database.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
44
44
  ```
45
45
 
46
- If you want persistence, pass a directory string to `Database.create(...)`
47
- or `Database(...)`.
46
+ If you want persistence, pass a database name and `database_dir` to
47
+ `Database.create(...)` or `Database(...)`.
48
48
 
49
49
  ## Async usage (non-blocking)
50
50
 
@@ -65,7 +65,7 @@ Async initialization follows the same rule:
65
65
 
66
66
  ```python
67
67
  db = await AsyncDatabase.create() # in-memory
68
- db = await AsyncDatabase.create("./app") # persistent: directory string
68
+ db = await AsyncDatabase.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
69
69
  ```
70
70
 
71
71
  `AsyncDatabase.execute` dispatches the query onto the default asyncio
@@ -116,16 +116,16 @@ All three are available as `lora_python.LoraError`, etc.
116
116
 
117
117
  ## Persistence
118
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
119
+ `Database.create("app", {"database_dir": "./data"})`, `Database("app", {"database_dir": "./data"})`, and
120
+ `await AsyncDatabase.create("app", {"database_dir": "./data"})` open or create
121
+ an archive-backed persistent database at `./data/app.loradb`. Reopening the same path
122
122
  replays committed writes before returning the handle.
123
123
 
124
- Call `db.close()` / `await db.close()` before reopening the same WAL
125
- directory inside one process.
124
+ Call `db.close()` / `await db.close()` before reopening the same archive
125
+ inside one process.
126
126
 
127
127
  This first Python persistence slice intentionally stays small: the
128
- binding exposes WAL-backed initialization plus the existing
128
+ binding exposes archive-backed initialization plus the existing
129
129
  `save_snapshot` / `load_snapshot` APIs, but not checkpoint, truncate,
130
130
  status, or sync-mode controls.
131
131
 
@@ -204,7 +204,7 @@ fn bench_named_archive_write_heavy(c: &mut Criterion) {
204
204
 
205
205
  // One timed iteration performs a realistic write burst. For the
206
206
  // persistent variant, dropping the DB at the end joins the archive writer
207
- // and includes the final `.lora` ZIP flush, so the result measures more
207
+ // and includes the final `.loradb` ZIP flush, so the result measures more
208
208
  // than just "enqueue dirty flag".
209
209
  const WRITES: usize = 1_000;
210
210
 
@@ -15,11 +15,11 @@ const MANIFEST_JSON: &str = r#"{"format":"lora.archive","version":1}"#;
15
15
  const WAL_PREFIX: &str = "wal/";
16
16
  const ARCHIVE_FLUSH_DEBOUNCE: Duration = Duration::from_secs(1);
17
17
 
18
- /// ZIP-backed `.lora` database file.
18
+ /// ZIP-backed `.loradb` database file.
19
19
  ///
20
20
  /// Every persist rewrites a complete ZIP archive from the current WAL work
21
21
  /// directory to a temp file, fsyncs it, and atomically renames it over the
22
- /// `.lora` target. Any ZIP-compatible tool (WinRAR, Explorer, unzip, 7-Zip)
22
+ /// `.loradb` target. Any ZIP-compatible tool (WinRAR, Explorer, unzip, 7-Zip)
23
23
  /// can inspect the resulting database file.
24
24
  pub(crate) struct WalArchive {
25
25
  work_dir: PathBuf,
@@ -108,6 +108,12 @@ impl Drop for WalArchive {
108
108
  {
109
109
  let (lock, cv) = &*self.state;
110
110
  let mut state = lock.lock().unwrap();
111
+ // The async archive worker may have already consumed the dirty flag
112
+ // before Group-mode WAL bytes were forced out of the in-memory
113
+ // segment buffer. Drop runs after the WAL handle is dropped, so
114
+ // always take one final archive snapshot from the now-flushed work
115
+ // directory.
116
+ state.dirty = true;
111
117
  state.shutdown = true;
112
118
  state.force = true;
113
119
  cv.notify_one();
@@ -191,7 +197,7 @@ fn write_archive_atomic(
191
197
  archive_path: &Path,
192
198
  max_archive_bytes: u64,
193
199
  ) -> Result<(), WalError> {
194
- let tmp_path = archive_path.with_extension("lora.tmp");
200
+ let tmp_path = archive_path.with_extension("loradb.tmp");
195
201
  {
196
202
  let file = OpenOptions::new()
197
203
  .create(true)
@@ -85,9 +85,9 @@ impl Database<InMemoryGraph> {
85
85
  /// Open or create a named portable database rooted under
86
86
  /// `options.database_dir`.
87
87
  ///
88
- /// The database name is a logical identifier, not a path. It must contain
89
- /// only ASCII letters, digits, `_`, `-`, and `.`, and is resolved to
90
- /// `<database_dir>/<database_name>.lora` before the WAL backend opens.
88
+ /// The database name may be either a portable basename (`app` or
89
+ /// `app.loradb`) or a safe relative path (`tenant/app`). It is resolved
90
+ /// under `options.database_dir` before the WAL archive backend opens.
91
91
  pub fn open_named(
92
92
  database_name: impl AsRef<str>,
93
93
  options: DatabaseOpenOptions,
@@ -0,0 +1,204 @@
1
+ use std::fmt;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use lora_wal::{SyncMode, WalConfig};
5
+
6
+ /// Hard ceiling for one portable database archive/root.
7
+ ///
8
+ /// The current WAL backend still stores segment files under this root, but
9
+ /// callers should treat the resolved `.loradb` path as the database artifact.
10
+ /// The archive backend will use the same limit when it starts writing framed
11
+ /// compressed chunks directly into portable files.
12
+ pub const DEFAULT_DATABASE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
13
+
14
+ /// Options for opening a named filesystem-backed database.
15
+ #[derive(Debug, Clone)]
16
+ pub struct DatabaseOpenOptions {
17
+ pub database_dir: PathBuf,
18
+ pub sync_mode: SyncMode,
19
+ pub segment_target_bytes: u64,
20
+ pub max_database_bytes: u64,
21
+ }
22
+
23
+ impl Default for DatabaseOpenOptions {
24
+ fn default() -> Self {
25
+ Self {
26
+ database_dir: PathBuf::from("."),
27
+ sync_mode: SyncMode::Group { interval_ms: 1_000 },
28
+ segment_target_bytes: 8 * 1024 * 1024,
29
+ max_database_bytes: DEFAULT_DATABASE_MAX_BYTES,
30
+ }
31
+ }
32
+ }
33
+
34
+ impl DatabaseOpenOptions {
35
+ pub fn with_database_dir(mut self, database_dir: impl Into<PathBuf>) -> Self {
36
+ self.database_dir = database_dir.into();
37
+ self
38
+ }
39
+
40
+ pub fn wal_config_for(&self, name: &DatabaseName) -> WalConfig {
41
+ WalConfig::Enabled {
42
+ dir: self.database_path_for(name),
43
+ sync_mode: self.sync_mode,
44
+ segment_target_bytes: self.segment_target_bytes,
45
+ }
46
+ }
47
+
48
+ pub fn database_path_for(&self, name: &DatabaseName) -> PathBuf {
49
+ self.database_dir.join(name.relative_path())
50
+ }
51
+ }
52
+
53
+ /// Validated logical database name/path.
54
+ ///
55
+ /// The input is serialized as a safe relative path under
56
+ /// [`DatabaseOpenOptions::database_dir`]. For example:
57
+ ///
58
+ /// - `app` -> `app.loradb`
59
+ /// - `app.loradb` -> `app.loradb`
60
+ /// - `./tenant-a/app` -> `tenant-a/app.loradb`
61
+ ///
62
+ /// Absolute paths, parent-directory traversal, empty components, and characters
63
+ /// outside ASCII letters/digits plus `+`, `_`, `-` are rejected. The final path
64
+ /// component may additionally end in `.loradb`.
65
+ #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66
+ pub struct DatabaseName {
67
+ raw: String,
68
+ relative_path: PathBuf,
69
+ }
70
+
71
+ impl DatabaseName {
72
+ pub fn parse(value: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
73
+ let value = value.as_ref();
74
+ if value.is_empty() {
75
+ return Err(DatabaseNameError::Empty);
76
+ }
77
+
78
+ if value.starts_with('/') || value.starts_with('\\') || looks_like_windows_absolute(value) {
79
+ return Err(DatabaseNameError::AbsolutePath(value.to_string()));
80
+ }
81
+
82
+ let mut parts: Vec<&str> = value.split(['/', '\\']).collect();
83
+ if parts.iter().any(|part| part.is_empty()) {
84
+ return Err(DatabaseNameError::InvalidCharacters(value.to_string()));
85
+ }
86
+ while parts.first() == Some(&".") {
87
+ parts.remove(0);
88
+ }
89
+ while parts.last() == Some(&".") {
90
+ parts.pop();
91
+ }
92
+ if parts.is_empty() {
93
+ return Err(DatabaseNameError::Reserved(value.to_string()));
94
+ }
95
+
96
+ let mut path = PathBuf::new();
97
+ for (idx, part) in parts.iter().enumerate() {
98
+ if *part == "." {
99
+ continue;
100
+ }
101
+ if *part == ".." {
102
+ return Err(DatabaseNameError::Reserved(value.to_string()));
103
+ }
104
+ let is_basename = idx == parts.len() - 1;
105
+ let serialized = serialize_component(part, is_basename)
106
+ .ok_or_else(|| DatabaseNameError::InvalidCharacters(value.to_string()))?;
107
+ path.push(serialized);
108
+ }
109
+ if path.as_os_str().is_empty() {
110
+ return Err(DatabaseNameError::Reserved(value.to_string()));
111
+ }
112
+ Ok(Self {
113
+ raw: value.to_string(),
114
+ relative_path: path,
115
+ })
116
+ }
117
+
118
+ pub fn as_str(&self) -> &str {
119
+ &self.raw
120
+ }
121
+
122
+ pub fn relative_path(&self) -> &Path {
123
+ &self.relative_path
124
+ }
125
+ }
126
+
127
+ impl fmt::Display for DatabaseName {
128
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129
+ self.raw.fmt(f)
130
+ }
131
+ }
132
+
133
+ impl TryFrom<&str> for DatabaseName {
134
+ type Error = DatabaseNameError;
135
+
136
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
137
+ Self::parse(value)
138
+ }
139
+ }
140
+
141
+ #[derive(Debug, Clone, PartialEq, Eq)]
142
+ pub enum DatabaseNameError {
143
+ Empty,
144
+ Reserved(String),
145
+ AbsolutePath(String),
146
+ InvalidCharacters(String),
147
+ }
148
+
149
+ impl fmt::Display for DatabaseNameError {
150
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151
+ match self {
152
+ Self::Empty => write!(f, "database name must not be empty"),
153
+ Self::Reserved(name) => write!(f, "database name '{name}' is reserved"),
154
+ Self::AbsolutePath(name) => write!(
155
+ f,
156
+ "invalid database name '{name}': use a relative path under database_dir"
157
+ ),
158
+ Self::InvalidCharacters(name) => write!(
159
+ f,
160
+ "invalid database name '{name}': use relative path components containing only letters, digits, '+', '_', '-', with an optional .loradb suffix on the basename"
161
+ ),
162
+ }
163
+ }
164
+ }
165
+
166
+ impl std::error::Error for DatabaseNameError {}
167
+
168
+ pub fn resolve_database_path(
169
+ database_name: &str,
170
+ database_dir: impl AsRef<Path>,
171
+ ) -> Result<PathBuf, DatabaseNameError> {
172
+ let name = DatabaseName::parse(database_name)?;
173
+ Ok(database_dir.as_ref().join(name.relative_path()))
174
+ }
175
+
176
+ fn serialize_component(component: &str, is_basename: bool) -> Option<String> {
177
+ if component.is_empty() {
178
+ return None;
179
+ }
180
+
181
+ if is_basename {
182
+ if let Some(stem) = component.strip_suffix(".loradb") {
183
+ return (!stem.is_empty() && is_portable_component(stem))
184
+ .then(|| component.to_string());
185
+ }
186
+ return is_portable_component(component).then(|| format!("{component}.loradb"));
187
+ }
188
+
189
+ is_portable_component(component).then(|| component.to_string())
190
+ }
191
+
192
+ fn is_portable_component(value: &str) -> bool {
193
+ value
194
+ .bytes()
195
+ .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'_' | b'-'))
196
+ }
197
+
198
+ fn looks_like_windows_absolute(value: &str) -> bool {
199
+ let bytes = value.as_bytes();
200
+ bytes.len() >= 3
201
+ && bytes[0].is_ascii_alphabetic()
202
+ && bytes[1] == b':'
203
+ && matches!(bytes[2], b'/' | b'\\')
204
+ }
@@ -90,14 +90,34 @@ fn disabled_config_behaves_like_in_memory() {
90
90
 
91
91
  #[test]
92
92
  fn database_name_validation_accepts_only_portable_names() {
93
- for valid in ["app", "tenant_01", "a-b.c", "A123"] {
93
+ for valid in [
94
+ "app",
95
+ "app.loradb",
96
+ "tenant_01",
97
+ "tenant+01",
98
+ "a-b",
99
+ "A123",
100
+ "./database-dir/application",
101
+ "database_dir/app.loradb",
102
+ ] {
94
103
  assert!(
95
104
  DatabaseName::parse(valid).is_ok(),
96
105
  "{valid} should be valid"
97
106
  );
98
107
  }
99
108
 
100
- for invalid in ["", ".", "..", "../x", "x/y", "has space", "ümlaut"] {
109
+ for invalid in [
110
+ "",
111
+ ".",
112
+ "..",
113
+ "../x",
114
+ "/absolute/app",
115
+ "x//y",
116
+ "a-b.c",
117
+ "app.txt",
118
+ "has space",
119
+ "ümlaut",
120
+ ] {
101
121
  assert!(
102
122
  DatabaseName::parse(invalid).is_err(),
103
123
  "{invalid:?} should be invalid"
@@ -109,7 +129,13 @@ fn database_name_validation_accepts_only_portable_names() {
109
129
  fn named_database_resolves_to_lora_root_under_database_dir() {
110
130
  let dir = TmpDir::new("named-path");
111
131
  let path = resolve_database_path("app_01", dir.path()).unwrap();
112
- assert_eq!(path, dir.path().join("app_01.lora"));
132
+ assert_eq!(path, dir.path().join("app_01.loradb"));
133
+
134
+ let path = resolve_database_path("./tenant-a/application", dir.path()).unwrap();
135
+ assert_eq!(path, dir.path().join("tenant-a").join("application.loradb"));
136
+
137
+ let path = resolve_database_path("tenant_b/app.loradb", dir.path()).unwrap();
138
+ assert_eq!(path, dir.path().join("tenant_b").join("app.loradb"));
113
139
  }
114
140
 
115
141
  #[test]
@@ -126,12 +152,12 @@ fn named_database_persists_under_lora_root() {
126
152
  }
127
153
 
128
154
  assert!(
129
- dir.path().join("app.lora").is_file(),
130
- "named databases should persist as a portable .lora archive file"
155
+ dir.path().join("app.loradb").is_file(),
156
+ "named databases should persist as a portable .loradb archive file"
131
157
  );
132
- let bytes = std::fs::read(dir.path().join("app.lora")).unwrap();
158
+ let bytes = std::fs::read(dir.path().join("app.loradb")).unwrap();
133
159
  assert_eq!(&bytes[..4], b"PK\x03\x04");
134
- let file = std::fs::File::open(dir.path().join("app.lora")).unwrap();
160
+ let file = std::fs::File::open(dir.path().join("app.loradb")).unwrap();
135
161
  let mut zip = zip::ZipArchive::new(file).unwrap();
136
162
  assert!(zip.by_name("manifest.json").is_ok());
137
163
  assert!(zip.by_name("wal/0000000001.wal").is_ok());
@@ -161,7 +187,7 @@ fn named_database_recovers_write_burst_from_zip_archive() {
161
187
  assert_eq!(db.node_count(), 250);
162
188
  }
163
189
 
164
- let archive_path = dir.path().join("burst.lora");
190
+ let archive_path = dir.path().join("burst.loradb");
165
191
  assert!(archive_path.is_file());
166
192
  let file = std::fs::File::open(&archive_path).unwrap();
167
193
  let mut zip = zip::ZipArchive::new(file).unwrap();
@@ -183,6 +209,44 @@ fn named_database_recovers_write_burst_from_zip_archive() {
183
209
  assert_eq!(row_array.last().unwrap()["id"], serde_json::json!(249));
184
210
  }
185
211
 
212
+ #[test]
213
+ fn named_database_final_archive_flush_captures_group_buffer() {
214
+ let dir = TmpDir::new("named-group-final-flush");
215
+
216
+ {
217
+ let db = Database::open_named(
218
+ "app",
219
+ DatabaseOpenOptions {
220
+ database_dir: dir.path().to_path_buf(),
221
+ sync_mode: SyncMode::Group {
222
+ interval_ms: 60_000,
223
+ },
224
+ ..DatabaseOpenOptions::default()
225
+ },
226
+ )
227
+ .unwrap();
228
+ db.execute(
229
+ "CREATE (:Person {name: 'Ada'})-[:KNOWS]->(:Person {name: 'Grace'})",
230
+ rows(),
231
+ )
232
+ .unwrap();
233
+
234
+ // Let the archive debounce worker observe the dirty flag before the
235
+ // Group-mode WAL flusher runs. The final archive flush on drop must
236
+ // still snapshot the force-flushed WAL bytes, not the earlier empty
237
+ // segment file.
238
+ std::thread::sleep(std::time::Duration::from_millis(1_200));
239
+ }
240
+
241
+ let db = Database::open_named(
242
+ "app",
243
+ DatabaseOpenOptions::default().with_database_dir(dir.path()),
244
+ )
245
+ .unwrap();
246
+ assert_eq!(db.node_count(), 2);
247
+ assert_eq!(db.relationship_count(), 1);
248
+ }
249
+
186
250
  #[test]
187
251
  fn fresh_open_then_crash_recover_replays_committed_writes() {
188
252
  let dir = TmpDir::new("recover");