lora-python 0.5.5__tar.gz → 0.5.6__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 (115) hide show
  1. {lora_python-0.5.5 → lora_python-0.5.6}/Cargo.lock +14 -14
  2. {lora_python-0.5.5 → lora_python-0.5.6}/Cargo.toml +9 -9
  3. {lora_python-0.5.5 → lora_python-0.5.6}/PKG-INFO +1 -1
  4. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/archive.rs +90 -7
  5. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/database.rs +41 -22
  6. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/wal.rs +89 -4
  7. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/config.rs +5 -3
  8. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/recorder_adapter.rs +6 -2
  9. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/wal.rs +6 -13
  10. {lora_python-0.5.5 → lora_python-0.5.6}/pyproject.toml +1 -1
  11. {lora_python-0.5.5 → lora_python-0.5.6}/LICENSE +0 -0
  12. {lora_python-0.5.5 → lora_python-0.5.6}/README.md +0 -0
  13. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/Cargo.toml +0 -0
  14. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/analyzer.rs +0 -0
  15. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/errors.rs +0 -0
  16. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/lib.rs +0 -0
  17. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/resolved.rs +0 -0
  18. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/scope.rs +0 -0
  19. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-analyzer/src/symbols.rs +0 -0
  20. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-ast/Cargo.toml +0 -0
  21. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-ast/src/ast.rs +0 -0
  22. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-ast/src/lib.rs +0 -0
  23. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/Cargo.toml +0 -0
  24. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/lib.rs +0 -0
  25. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/logical.rs +0 -0
  26. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/optimizer.rs +0 -0
  27. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/pattern.rs +0 -0
  28. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/physical.rs +0 -0
  29. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-compiler/src/planner.rs +0 -0
  30. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/Cargo.toml +0 -0
  31. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/advanced_benchmarks.rs +0 -0
  32. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/engine_benchmarks.rs +0 -0
  33. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/fixtures.rs +0 -0
  34. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/perf_smoke_baseline.json +0 -0
  35. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/perf_smoke_benchmarks.rs +0 -0
  36. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/scale_benchmarks.rs +0 -0
  37. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/temporal_spatial_benchmarks.rs +0 -0
  38. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/benches/wal_benchmarks.rs +0 -0
  39. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/lib.rs +0 -0
  40. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/named.rs +0 -0
  41. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/stream.rs +0 -0
  42. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/src/transaction.rs +0 -0
  43. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/advanced_queries.rs +0 -0
  44. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/aggregation.rs +0 -0
  45. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/backend_stub.rs +0 -0
  46. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/create.rs +0 -0
  47. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/errors.rs +0 -0
  48. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/expressions.rs +0 -0
  49. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/functions_extended.rs +0 -0
  50. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/invariants.rs +0 -0
  51. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/match.rs +0 -0
  52. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/merge.rs +0 -0
  53. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/ordering.rs +0 -0
  54. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/parameters.rs +0 -0
  55. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/parser.rs +0 -0
  56. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/paths.rs +0 -0
  57. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/projection.rs +0 -0
  58. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/seeds.rs +0 -0
  59. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/snapshot.rs +0 -0
  60. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/spatial.rs +0 -0
  61. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/temporal.rs +0 -0
  62. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/test_helpers.rs +0 -0
  63. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/transactions.rs +0 -0
  64. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/types_advanced.rs +0 -0
  65. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/union.rs +0 -0
  66. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/update.rs +0 -0
  67. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/vectors.rs +0 -0
  68. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/where_clause.rs +0 -0
  69. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-database/tests/with.rs +0 -0
  70. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/Cargo.toml +0 -0
  71. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/errors.rs +0 -0
  72. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/eval.rs +0 -0
  73. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/executor.rs +0 -0
  74. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/lib.rs +0 -0
  75. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/pull.rs +0 -0
  76. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-executor/src/value.rs +0 -0
  77. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-parser/Cargo.toml +0 -0
  78. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-parser/src/cypher.pest +0 -0
  79. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-parser/src/error.rs +0 -0
  80. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-parser/src/lib.rs +0 -0
  81. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-parser/src/parser.rs +0 -0
  82. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/.gitignore +0 -0
  83. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/Cargo.toml +0 -0
  84. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/LICENSE +0 -0
  85. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/README.md +0 -0
  86. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/build.rs +0 -0
  87. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/examples/async_demo.py +0 -0
  88. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/examples/basic.py +0 -0
  89. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/src/lib.rs +0 -0
  90. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/tests/test_async.py +0 -0
  91. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-python/tests/test_sync.py +0 -0
  92. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/Cargo.toml +0 -0
  93. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/graph.rs +0 -0
  94. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/lib.rs +0 -0
  95. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/memory.rs +0 -0
  96. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/mutation.rs +0 -0
  97. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/snapshot.rs +0 -0
  98. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/spatial.rs +0 -0
  99. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/temporal.rs +0 -0
  100. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-store/src/vector.rs +0 -0
  101. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/Cargo.toml +0 -0
  102. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/dir.rs +0 -0
  103. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/error.rs +0 -0
  104. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/lib.rs +0 -0
  105. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/lock.rs +0 -0
  106. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/lsn.rs +0 -0
  107. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/record.rs +0 -0
  108. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/replay.rs +0 -0
  109. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/segment.rs +0 -0
  110. {lora_python-0.5.5 → lora_python-0.5.6}/crates/lora-wal/src/testing.rs +0 -0
  111. {lora_python-0.5.5 → lora_python-0.5.6}/python/lora_python/__init__.py +0 -0
  112. {lora_python-0.5.5 → lora_python-0.5.6}/python/lora_python/_async.py +0 -0
  113. {lora_python-0.5.5 → lora_python-0.5.6}/python/lora_python/_native.pyi +0 -0
  114. {lora_python-0.5.5 → lora_python-0.5.6}/python/lora_python/py.typed +0 -0
  115. {lora_python-0.5.5 → lora_python-0.5.6}/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.5"
654
+ version = "0.5.6"
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.5"
664
+ version = "0.5.6"
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.5"
671
+ version = "0.5.6"
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.5"
679
+ version = "0.5.6"
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.5"
697
+ version = "0.5.6"
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.5"
711
+ version = "0.5.6"
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.5"
721
+ version = "0.5.6"
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.5"
735
+ version = "0.5.6"
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.5"
746
+ version = "0.5.6"
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.5"
758
+ version = "0.5.6"
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.5"
772
+ version = "0.5.6"
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.5"
784
+ version = "0.5.6"
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.5"
795
+ version = "0.5.6"
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.5"
810
+ version = "0.5.6"
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.5"
7
+ version = "0.5.6"
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.5" }
19
- lora-parser = { path = "crates/lora-parser", version = "=0.5.5" }
20
- lora-analyzer = { path = "crates/lora-analyzer", version = "=0.5.5" }
21
- lora-compiler = { path = "crates/lora-compiler", version = "=0.5.5" }
22
- lora-store = { path = "crates/lora-store", version = "=0.5.5" }
23
- lora-wal = { path = "crates/lora-wal", version = "=0.5.5" }
24
- lora-executor = { path = "crates/lora-executor", version = "=0.5.5" }
25
- lora-database = { path = "crates/lora-database", version = "=0.5.5" }
18
+ lora-ast = { path = "crates/lora-ast", version = "=0.5.6" }
19
+ lora-parser = { path = "crates/lora-parser", version = "=0.5.6" }
20
+ lora-analyzer = { path = "crates/lora-analyzer", version = "=0.5.6" }
21
+ lora-compiler = { path = "crates/lora-compiler", version = "=0.5.6" }
22
+ lora-store = { path = "crates/lora-store", version = "=0.5.6" }
23
+ lora-wal = { path = "crates/lora-wal", version = "=0.5.6" }
24
+ lora-executor = { path = "crates/lora-executor", version = "=0.5.6" }
25
+ lora-database = { path = "crates/lora-database", version = "=0.5.6" }
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.5
3
+ Version: 0.5.6
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -24,8 +24,11 @@ static ARCHIVE_TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
24
24
  /// `.loradb` target. Any ZIP-compatible tool (WinRAR, Explorer, unzip, 7-Zip)
25
25
  /// can inspect the resulting database file.
26
26
  pub(crate) struct WalArchive {
27
+ archive_path: PathBuf,
27
28
  work_dir: PathBuf,
29
+ max_archive_bytes: u64,
28
30
  state: Arc<(Mutex<ArchiveState>, Condvar)>,
31
+ write_lock: Arc<Mutex<()>>,
29
32
  worker: Option<JoinHandle<()>>,
30
33
  _archive_lock: ArchiveLock,
31
34
  }
@@ -56,16 +59,21 @@ impl WalArchive {
56
59
  prepare_work_dir(&archive_path, &work_dir, max_archive_bytes)?;
57
60
 
58
61
  let state = Arc::new((Mutex::new(ArchiveState::default()), Condvar::new()));
62
+ let write_lock = Arc::new(Mutex::new(()));
59
63
  let worker = Some(spawn_archive_worker(
60
64
  state.clone(),
65
+ write_lock.clone(),
61
66
  work_dir.clone(),
62
67
  archive_path.clone(),
63
68
  max_archive_bytes,
64
69
  ));
65
70
 
66
71
  Ok(Self {
72
+ archive_path,
67
73
  work_dir,
74
+ max_archive_bytes,
68
75
  state,
76
+ write_lock,
69
77
  worker,
70
78
  _archive_lock: archive_lock,
71
79
  })
@@ -95,6 +103,50 @@ impl WalMirror for WalArchive {
95
103
  cv.notify_one();
96
104
  Ok(())
97
105
  }
106
+
107
+ fn persist_force(&self, wal_dir: &Path) -> Result<(), WalError> {
108
+ if wal_dir != self.work_dir {
109
+ return Err(WalError::Malformed(format!(
110
+ "archive mirror received unexpected WAL dir: {}",
111
+ wal_dir.display()
112
+ )));
113
+ }
114
+ {
115
+ let (lock, _) = &*self.state;
116
+ let state = lock.lock().unwrap();
117
+ if let Some(failure) = &state.failure {
118
+ return Err(WalError::Malformed(format!(
119
+ "database archive writer failed: {failure}"
120
+ )));
121
+ }
122
+ }
123
+
124
+ let _write_guard = self.write_lock.lock().unwrap();
125
+ {
126
+ let (lock, _) = &*self.state;
127
+ let state = lock.lock().unwrap();
128
+ if let Some(failure) = &state.failure {
129
+ return Err(WalError::Malformed(format!(
130
+ "database archive writer failed: {failure}"
131
+ )));
132
+ }
133
+ }
134
+ let result =
135
+ write_archive_atomic(&self.work_dir, &self.archive_path, self.max_archive_bytes);
136
+ let (lock, _) = &*self.state;
137
+ let mut state = lock.lock().unwrap();
138
+ match result {
139
+ Ok(()) => {
140
+ state.dirty = false;
141
+ state.force = false;
142
+ Ok(())
143
+ }
144
+ Err(err) => {
145
+ state.failure = Some(err.to_string());
146
+ Err(err)
147
+ }
148
+ }
149
+ }
98
150
  }
99
151
 
100
152
  impl Drop for WalArchive {
@@ -102,10 +154,9 @@ impl Drop for WalArchive {
102
154
  {
103
155
  let (lock, cv) = &*self.state;
104
156
  let mut state = lock.lock().unwrap();
105
- // The async archive worker may have already consumed the dirty flag
106
- // before Group-mode WAL bytes were forced out of the in-memory
107
- // segment buffer. Drop runs after the WAL handle is dropped, so
108
- // always take one final archive snapshot from the now-flushed work
157
+ // The async archive worker may not have observed the latest dirty
158
+ // flag yet. Drop runs after the WAL handle is dropped, so always
159
+ // take one final archive snapshot from the fully flushed work
109
160
  // directory.
110
161
  state.dirty = true;
111
162
  state.shutdown = true;
@@ -132,6 +183,7 @@ impl Drop for WalArchive {
132
183
 
133
184
  fn spawn_archive_worker(
134
185
  state: Arc<(Mutex<ArchiveState>, Condvar)>,
186
+ write_lock: Arc<Mutex<()>>,
135
187
  work_dir: PathBuf,
136
188
  archive_path: PathBuf,
137
189
  max_archive_bytes: u64,
@@ -157,6 +209,7 @@ fn spawn_archive_worker(
157
209
  };
158
210
 
159
211
  if should_flush {
212
+ let _write_guard = write_lock.lock().unwrap();
160
213
  if let Err(err) = write_archive_atomic(&work_dir, &archive_path, max_archive_bytes) {
161
214
  let (lock, _) = &*state;
162
215
  let mut guard = lock.lock().unwrap();
@@ -373,9 +426,8 @@ fn cleanup_stale_temp_paths(archive_path: &Path) -> Result<(), WalError> {
373
426
  let Some(file_name) = file_name.to_str() else {
374
427
  continue;
375
428
  };
376
- let is_archive_tmp =
377
- file_name.starts_with(&archive_tmp_prefix) && file_name.ends_with(".tmp");
378
- let is_extract_tmp = file_name.starts_with(&extract_tmp_prefix);
429
+ let is_archive_tmp = is_generated_archive_tmp_name(file_name, &archive_tmp_prefix);
430
+ let is_extract_tmp = is_generated_extract_tmp_name(file_name, &extract_tmp_prefix);
379
431
  if !is_archive_tmp && !is_extract_tmp {
380
432
  continue;
381
433
  }
@@ -390,6 +442,37 @@ fn cleanup_stale_temp_paths(archive_path: &Path) -> Result<(), WalError> {
390
442
  Ok(())
391
443
  }
392
444
 
445
+ fn is_generated_archive_tmp_name(file_name: &str, prefix: &str) -> bool {
446
+ let Some(rest) = file_name.strip_prefix(prefix) else {
447
+ return false;
448
+ };
449
+ let Some(rest) = rest.strip_suffix(".tmp") else {
450
+ return false;
451
+ };
452
+ let mut parts = rest.split('.');
453
+ matches!(
454
+ (parts.next(), parts.next(), parts.next(), parts.next()),
455
+ (Some(pid), Some(nanos), Some(sequence), None)
456
+ if is_ascii_digits(pid) && is_ascii_digits(nanos) && is_ascii_digits(sequence)
457
+ )
458
+ }
459
+
460
+ fn is_generated_extract_tmp_name(file_name: &str, prefix: &str) -> bool {
461
+ let Some(rest) = file_name.strip_prefix(prefix) else {
462
+ return false;
463
+ };
464
+ let mut parts = rest.split('.');
465
+ matches!(
466
+ (parts.next(), parts.next(), parts.next(), parts.next()),
467
+ (Some(pid), Some(nanos), Some(sequence), None)
468
+ if is_ascii_digits(pid) && is_ascii_digits(nanos) && is_ascii_digits(sequence)
469
+ )
470
+ }
471
+
472
+ fn is_ascii_digits(value: &str) -> bool {
473
+ !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
474
+ }
475
+
393
476
  fn sorted_wal_files(wal_dir: &Path) -> Result<Vec<PathBuf>, WalError> {
394
477
  let mut entries = Vec::new();
395
478
  for entry in fs::read_dir(wal_dir)? {
@@ -140,6 +140,15 @@ impl Database<InMemoryGraph> {
140
140
  Ok(Transaction::new(live, self.wal.clone(), mode))
141
141
  }
142
142
 
143
+ /// Force any pending WAL bytes to durable storage and, for archive-backed
144
+ /// databases, refresh the portable `.loradb` file before returning.
145
+ pub fn sync(&self) -> Result<()> {
146
+ if let Some(wal) = &self.wal {
147
+ wal.force_fsync()?;
148
+ }
149
+ Ok(())
150
+ }
151
+
143
152
  /// Restore from a snapshot file then replay any WAL records past
144
153
  /// it.
145
154
  ///
@@ -596,32 +605,42 @@ where
596
605
  // instead, so swapping in a new backend only requires changing one type
597
606
  // parameter.
598
607
 
599
- /// Drop every node and relationship.
608
+ /// Drop every node and relationship, returning WAL/archive errors to the
609
+ /// caller.
600
610
  ///
601
- /// When a WAL is attached, `clear()` is wrapped in `arm`/`commit`
602
- /// so the `MutationEvent::Clear` fired by the store reaches the
603
- /// log inside a transaction (without arming, the recorder would
604
- /// poison itself on the first event). WAL failures here are
605
- /// best-effort: the in-memory state is still cleared so the
606
- /// caller's contract holds, but the recorder's poisoned flag
607
- /// will surface to the next query.
608
- pub fn clear(&self) {
611
+ /// When a WAL is attached, the clear is wrapped in `arm`/`commit` so the
612
+ /// `MutationEvent::Clear` fired by the store reaches the log inside a
613
+ /// transaction. If a failure happens after the in-memory graph has been
614
+ /// cleared, the recorder is poisoned by the failing WAL path and future
615
+ /// writes fail until the database is reopened from durable state.
616
+ pub fn try_clear(&self) -> Result<()> {
609
617
  let mut guard = self.write_store();
610
- match &self.wal {
611
- None => guard.clear(),
612
- Some(rec) => {
613
- let armed = rec.arm();
614
- guard.clear();
615
- if armed.is_ok() {
616
- // `clear()` always emits a `MutationEvent::Clear`,
617
- // so commit returns `WroteCommit::Yes` and we
618
- // flush. If that order ever changes, the worst
619
- // case is one redundant flush call.
620
- let _ = rec.commit();
621
- let _ = rec.flush();
622
- }
618
+ let Some(rec) = &self.wal else {
619
+ guard.clear();
620
+ return Ok(());
621
+ };
622
+
623
+ rec.arm().map_err(|e| anyhow!("WAL arm failed: {e}"))?;
624
+ guard.clear();
625
+ match rec.commit() {
626
+ Ok(WroteCommit::Yes) => {
627
+ rec.flush().map_err(|e| anyhow!("WAL flush failed: {e}"))?;
623
628
  }
629
+ Ok(WroteCommit::No) => {}
630
+ Err(e) => return Err(anyhow!("WAL commit failed: {e}")),
631
+ }
632
+ if let Some(reason) = rec.poisoned() {
633
+ return Err(anyhow!("WAL poisoned: {reason}"));
624
634
  }
635
+ Ok(())
636
+ }
637
+
638
+ /// Drop every node and relationship.
639
+ ///
640
+ /// This compatibility helper keeps the historical infallible Rust API.
641
+ /// Bindings that can report errors should call [`Self::try_clear`].
642
+ pub fn clear(&self) {
643
+ let _ = self.try_clear();
625
644
  }
626
645
 
627
646
  /// Number of nodes currently in the graph.
@@ -251,6 +251,92 @@ fn named_database_recovers_from_durable_sidecar_when_archive_lags() {
251
251
  );
252
252
  }
253
253
 
254
+ #[test]
255
+ fn named_database_sync_makes_archive_immediately_portable() {
256
+ let dir = TmpDir::new("named-sync-source");
257
+ let portable_dir = TmpDir::new("named-sync-copy");
258
+
259
+ let db = Database::open_named(
260
+ "app",
261
+ DatabaseOpenOptions {
262
+ sync_mode: SyncMode::Group {
263
+ interval_ms: 60_000,
264
+ },
265
+ ..DatabaseOpenOptions::default().with_database_dir(dir.path())
266
+ },
267
+ )
268
+ .unwrap();
269
+ db.execute("CREATE (:Synced {id: 1})", rows()).unwrap();
270
+ db.sync().unwrap();
271
+
272
+ std::fs::copy(
273
+ dir.path().join("app.loradb"),
274
+ portable_dir.path().join("app.loradb"),
275
+ )
276
+ .unwrap();
277
+
278
+ let recovered = Database::open_named(
279
+ "app",
280
+ DatabaseOpenOptions::default().with_database_dir(portable_dir.path()),
281
+ )
282
+ .unwrap();
283
+ assert_eq!(recovered.node_count(), 1);
284
+ }
285
+
286
+ #[test]
287
+ fn named_database_clear_persists_through_archive() {
288
+ let dir = TmpDir::new("named-clear");
289
+
290
+ {
291
+ let db = Database::open_named(
292
+ "app",
293
+ DatabaseOpenOptions::default().with_database_dir(dir.path()),
294
+ )
295
+ .unwrap();
296
+ db.execute("CREATE (:A {id: 1})-[:R]->(:B {id: 2})", rows())
297
+ .unwrap();
298
+ db.try_clear().unwrap();
299
+ assert_eq!(db.node_count(), 0);
300
+ assert_eq!(db.relationship_count(), 0);
301
+ }
302
+
303
+ let recovered = Database::open_named(
304
+ "app",
305
+ DatabaseOpenOptions::default().with_database_dir(dir.path()),
306
+ )
307
+ .unwrap();
308
+ assert_eq!(recovered.node_count(), 0);
309
+ assert_eq!(recovered.relationship_count(), 0);
310
+ }
311
+
312
+ #[test]
313
+ fn named_database_cleanup_only_removes_generated_temp_paths() {
314
+ let dir = TmpDir::new("named-temp-cleanup");
315
+ let unrelated_file = dir.path().join("app.loradb.user.tmp");
316
+ let unrelated_dir = dir.path().join("app.loradb.wal.extract.user");
317
+ let generated_file = dir.path().join("app.loradb.1.2.3.tmp");
318
+ let generated_dir = dir.path().join("app.loradb.wal.extract.1.2.3");
319
+
320
+ std::fs::write(&unrelated_file, b"keep").unwrap();
321
+ std::fs::create_dir(&unrelated_dir).unwrap();
322
+ std::fs::write(&generated_file, b"delete").unwrap();
323
+ std::fs::create_dir(&generated_dir).unwrap();
324
+
325
+ {
326
+ let db = Database::open_named(
327
+ "app",
328
+ DatabaseOpenOptions::default().with_database_dir(dir.path()),
329
+ )
330
+ .unwrap();
331
+ db.sync().unwrap();
332
+ }
333
+
334
+ assert!(unrelated_file.exists());
335
+ assert!(unrelated_dir.exists());
336
+ assert!(!generated_file.exists());
337
+ assert!(!generated_dir.exists());
338
+ }
339
+
254
340
  #[test]
255
341
  fn named_database_rejects_invalid_archive_without_publishing_partial_sidecar() {
256
342
  let dir = TmpDir::new("named-invalid-archive");
@@ -367,10 +453,9 @@ fn named_database_final_archive_flush_captures_group_buffer() {
367
453
  )
368
454
  .unwrap();
369
455
 
370
- // Let the archive debounce worker observe the dirty flag before the
371
- // Group-mode WAL flusher runs. The final archive flush on drop must
372
- // still snapshot the force-flushed WAL bytes, not the earlier empty
373
- // segment file.
456
+ // Let the archive debounce worker run before the clean shutdown path.
457
+ // The final archive flush on drop must still publish a complete,
458
+ // reopenable archive.
374
459
  std::thread::sleep(std::time::Duration::from_millis(1_200));
375
460
  }
376
461
 
@@ -14,9 +14,11 @@ pub enum SyncMode {
14
14
  #[default]
15
15
  PerCommit,
16
16
 
17
- /// Buffer commits and `fsync` on a fixed cadence on a background
18
- /// thread. Trades the last `interval_ms` of commits for higher
19
- /// throughput on bulk-load workloads.
17
+ /// Write commit bytes to the OS immediately, then `fsync` on a fixed
18
+ /// cadence on a background thread. This survives ordinary process death
19
+ /// after `flush()` returns, but can still trade the last `interval_ms` of
20
+ /// commits on power loss or kernel crash for higher throughput on
21
+ /// bulk-load workloads.
20
22
  ///
21
23
  /// A background fsync failure poisons the WAL: the next `commit` /
22
24
  /// `flush` / `force_fsync` returns [`crate::WalError::Poisoned`] and
@@ -76,6 +76,10 @@ pub enum WroteCommit {
76
76
  /// databases.
77
77
  pub trait WalMirror: Send + Sync {
78
78
  fn persist(&self, wal_dir: &Path) -> Result<(), WalError>;
79
+
80
+ fn persist_force(&self, wal_dir: &Path) -> Result<(), WalError> {
81
+ self.persist(wal_dir)
82
+ }
79
83
  }
80
84
 
81
85
  #[derive(Default)]
@@ -246,7 +250,7 @@ impl WalRecorder {
246
250
  state.poisoned = Some(e.to_string());
247
251
  })?;
248
252
  if let Some(mirror) = &self.mirror {
249
- mirror.persist(self.wal.dir()).inspect_err(|e| {
253
+ mirror.persist_force(self.wal.dir()).inspect_err(|e| {
250
254
  state.poisoned = Some(e.to_string());
251
255
  })?;
252
256
  }
@@ -274,7 +278,7 @@ impl WalRecorder {
274
278
  // full WAL history is the only safe way to let the archive recover by
275
279
  // itself after a checkpoint marker.
276
280
  if let Some(mirror) = &self.mirror {
277
- mirror.persist(self.wal.dir())?;
281
+ mirror.persist_force(self.wal.dir())?;
278
282
  return Ok(());
279
283
  }
280
284
  self.wal.truncate_up_to(fence_lsn)?;
@@ -378,8 +378,8 @@ impl Wal {
378
378
  /// - `PerCommit` — write the buffer to the OS, `fsync`, and
379
379
  /// advance `durable_lsn`. The strongest contract: every
380
380
  /// record up to `next_lsn - 1` is on disk.
381
- /// - `Group` — leave the buffer in memory. The background flusher
382
- /// writes, fsyncs, and advances `durable_lsn` on its cadence.
381
+ /// - `Group` — write the buffer to the OS, but let the background
382
+ /// flusher fsync and advance `durable_lsn` on its cadence.
383
383
  /// - `None` — write the buffer to the OS only, but advance
384
384
  /// `durable_lsn` anyway. The mode opts out of crash
385
385
  /// durability, so the checkpoint fence reports
@@ -407,9 +407,9 @@ impl Wal {
407
407
  let written_lsn = Lsn::new(state.next_lsn.raw().saturating_sub(1));
408
408
 
409
409
  // Decide whether this call is allowed to advance
410
- // `durable_lsn`. The bg flusher's job in Group mode is
411
- // exactly to do that "out of band"; PerCommit and None do it
412
- // inline; Group's user-driven `flush()` does not.
410
+ // `durable_lsn`. The bg flusher's job in Group mode is to advance
411
+ // that fence after fsync; PerCommit and None do it inline; Group's
412
+ // user-driven `flush()` only pushes bytes to the OS.
413
413
  let do_fsync = matches!(
414
414
  (kind, self.sync_mode),
415
415
  (FlushKind::ForceFsync, _) | (_, SyncMode::PerCommit)
@@ -419,14 +419,7 @@ impl Wal {
419
419
  (FlushKind::ForceFsync, _) | (_, SyncMode::PerCommit) | (_, SyncMode::None)
420
420
  );
421
421
 
422
- if matches!(
423
- (kind, self.sync_mode),
424
- (FlushKind::PerConfiguredMode, SyncMode::Group { .. })
425
- ) {
426
- // Group mode batches both the write syscall and the fsync. This
427
- // keeps the write-heavy hot path close to in-memory execution; the
428
- // background flusher (or Drop) will force the buffer out.
429
- } else if do_fsync {
422
+ if do_fsync {
430
423
  state.active_writer.flush_and_sync()?;
431
424
  } else {
432
425
  state.active_writer.flush_buffer()?;
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "lora-python"
7
- version = "0.5.5"
7
+ version = "0.5.6"
8
8
  description = "Python bindings for the Lora in-memory graph database"
9
9
  readme = "README.md"
10
10
  license = { text = "BUSL-1.1" }
File without changes
File without changes