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.
- {lora_python-0.5.0 → lora_python-0.5.1}/Cargo.lock +14 -14
- {lora_python-0.5.0 → lora_python-0.5.1}/Cargo.toml +9 -9
- {lora_python-0.5.0 → lora_python-0.5.1}/PKG-INFO +11 -11
- {lora_python-0.5.0 → lora_python-0.5.1}/README.md +10 -10
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/wal_benchmarks.rs +1 -1
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/archive.rs +9 -3
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/database.rs +3 -3
- lora_python-0.5.1/crates/lora-database/src/named.rs +204 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/wal.rs +72 -8
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/README.md +10 -10
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/examples/basic.py +1 -1
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/recorder_adapter.rs +1 -1
- {lora_python-0.5.0 → lora_python-0.5.1}/pyproject.toml +1 -1
- {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/__init__.py +1 -1
- {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/_async.py +1 -1
- lora_python-0.5.0/crates/lora-database/src/named.rs +0 -124
- {lora_python-0.5.0 → lora_python-0.5.1}/LICENSE +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/analyzer.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/errors.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/resolved.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/scope.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-analyzer/src/symbols.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/src/ast.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-ast/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/logical.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/optimizer.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/pattern.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/physical.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-compiler/src/planner.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/fixtures.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/stream.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/src/transaction.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/advanced_queries.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/aggregation.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/backend_stub.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/create.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/errors.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/expressions.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/functions_extended.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/invariants.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/match.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/merge.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/ordering.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/parameters.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/parser.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/paths.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/projection.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/seeds.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/snapshot.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/spatial.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/temporal.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/test_helpers.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/transactions.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/types_advanced.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/union.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/update.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/vectors.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/where_clause.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-database/tests/with.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/errors.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/eval.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/executor.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/pull.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-executor/src/value.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/cypher.pest +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/error.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-parser/src/parser.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/.gitignore +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/LICENSE +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/build.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/examples/async_demo.py +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/tests/test_async.py +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-python/tests/test_sync.py +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/graph.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/memory.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/mutation.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/snapshot.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/spatial.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/temporal.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-store/src/vector.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/Cargo.toml +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/config.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/dir.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/error.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lib.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lock.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/lsn.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/record.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/replay.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/segment.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/testing.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/crates/lora-wal/src/wal.rs +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/_native.pyi +0 -0
- {lora_python-0.5.0 → lora_python-0.5.1}/python/lora_python/py.typed +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
19
|
-
lora-parser = { path = "crates/lora-parser", version = "=0.5.
|
|
20
|
-
lora-analyzer = { path = "crates/lora-analyzer", version = "=0.5.
|
|
21
|
-
lora-compiler = { path = "crates/lora-compiler", version = "=0.5.
|
|
22
|
-
lora-store = { path = "crates/lora-store", version = "=0.5.
|
|
23
|
-
lora-wal = { path = "crates/lora-wal", version = "=0.5.
|
|
24
|
-
lora-executor = { path = "crates/lora-executor", version = "=0.5.
|
|
25
|
-
lora-database = { path = "crates/lora-database", version = "=0.5.
|
|
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.
|
|
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("
|
|
73
|
+
persistent = Database.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
If you want persistence, pass a
|
|
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("
|
|
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("
|
|
150
|
-
`await AsyncDatabase.create("
|
|
151
|
-
persistent database
|
|
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
|
|
155
|
-
|
|
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
|
|
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("
|
|
43
|
+
persistent = Database.create("app", {"database_dir": "./data"}) # persistent: ./data/app.loradb
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
If you want persistence, pass a
|
|
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("
|
|
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("
|
|
120
|
-
`await AsyncDatabase.create("
|
|
121
|
-
persistent database
|
|
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
|
|
125
|
-
|
|
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
|
|
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 `.
|
|
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 `.
|
|
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
|
-
/// `.
|
|
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("
|
|
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
|
|
89
|
-
///
|
|
90
|
-
///
|
|
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 [
|
|
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 [
|
|
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.
|
|
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.
|
|
130
|
-
"named databases should persist as a portable .
|
|
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.
|
|
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.
|
|
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.
|
|
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");
|