commitkv 1.0.1__tar.gz → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commitkv
3
- Version: 1.0.1
3
+ Version: 1.1.1
4
4
  License-File: LICENSE
5
5
  Summary: Compressed key-value store for git-managed caches: append-only segments that never exceed a size limit
6
6
  License-Expression: Apache-2.0
@@ -11,6 +11,13 @@ Project-URL: Repository, https://github.com/WillBooster/commitkv
11
11
 
12
12
  # commitkv
13
13
 
14
+ [![npm version](https://img.shields.io/npm/v/commitkv.svg)](https://www.npmjs.com/package/commitkv)
15
+ [![license](https://img.shields.io/npm/l/commitkv.svg)](https://www.npmjs.com/package/commitkv)
16
+ [![Test](https://github.com/WillBooster/commitkv/actions/workflows/test.yml/badge.svg)](https://github.com/WillBooster/commitkv/actions/workflows/test.yml)
17
+ [![Test rust](https://github.com/WillBooster/commitkv/actions/workflows/test-rust.yml/badge.svg)](https://github.com/WillBooster/commitkv/actions/workflows/test-rust.yml)
18
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
19
+ [![wbfy](https://img.shields.io/badge/wbfy-20.43.2-1e90ff.svg)](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
20
+
14
21
  A compressed key-value store for caches that are committed to git, usable from Python,
15
22
  Node.js, and Bun.
16
23
 
@@ -80,6 +87,14 @@ store.refresh(); // pick up what other stores wrote to the directory
80
87
  writing (the first `put`, and the first after a failed one); records that other stores write
81
88
  stay invisible until one of those happens. Call `refresh` after
82
89
  replacing segments under an open store, for example by checking out another branch.
90
+ - Stores that write to a directory at the same time each append to a segment of their own, and
91
+ the next run continues at most one of them. Open one store per directory in a process and share
92
+ it; a store per use leaves a small segment behind on every run, and a small segment
93
+ compresses worse because its values have few earlier ones to refer to.
94
+ - `keys` lists the keys in the order their current values lie in the segments, oldest segment
95
+ first. One store's writes are listed in the order it made them; the writes of several stores
96
+ are grouped by segment, so their order does not tell which was made first. Read every value
97
+ in that order: a scattered order decodes the surrounding values again for most of them.
83
98
  - Among records of a key that stores wrote without seeing each other's, which one wins is
84
99
  unspecified, but every reader picks the same one.
85
100
  - `max_segment_bytes` / `maxSegmentBytes` must be between 1,024 and 100,000,000; the upper
@@ -1,5 +1,12 @@
1
1
  # commitkv
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/commitkv.svg)](https://www.npmjs.com/package/commitkv)
4
+ [![license](https://img.shields.io/npm/l/commitkv.svg)](https://www.npmjs.com/package/commitkv)
5
+ [![Test](https://github.com/WillBooster/commitkv/actions/workflows/test.yml/badge.svg)](https://github.com/WillBooster/commitkv/actions/workflows/test.yml)
6
+ [![Test rust](https://github.com/WillBooster/commitkv/actions/workflows/test-rust.yml/badge.svg)](https://github.com/WillBooster/commitkv/actions/workflows/test-rust.yml)
7
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
8
+ [![wbfy](https://img.shields.io/badge/wbfy-20.43.2-1e90ff.svg)](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
9
+
3
10
  A compressed key-value store for caches that are committed to git, usable from Python,
4
11
  Node.js, and Bun.
5
12
 
@@ -69,6 +76,14 @@ store.refresh(); // pick up what other stores wrote to the directory
69
76
  writing (the first `put`, and the first after a failed one); records that other stores write
70
77
  stay invisible until one of those happens. Call `refresh` after
71
78
  replacing segments under an open store, for example by checking out another branch.
79
+ - Stores that write to a directory at the same time each append to a segment of their own, and
80
+ the next run continues at most one of them. Open one store per directory in a process and share
81
+ it; a store per use leaves a small segment behind on every run, and a small segment
82
+ compresses worse because its values have few earlier ones to refer to.
83
+ - `keys` lists the keys in the order their current values lie in the segments, oldest segment
84
+ first. One store's writes are listed in the order it made them; the writes of several stores
85
+ are grouped by segment, so their order does not tell which was made first. Read every value
86
+ in that order: a scattered order decodes the surrounding values again for most of them.
72
87
  - Among records of a key that stores wrote without seeing each other's, which one wins is
73
88
  unspecified, but every reader picks the same one.
74
89
  - `max_segment_bytes` / `maxSegmentBytes` must be between 1,024 and 100,000,000; the upper
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commitkv"
3
- version = "1.0.1"
3
+ version = "1.1.1"
4
4
  description = "Compressed key-value store for git-managed caches: append-only segments that never exceed a size limit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,10 +11,10 @@ Repository = "https://github.com/WillBooster/commitkv"
11
11
  Issues = "https://github.com/WillBooster/commitkv/issues"
12
12
 
13
13
  [dependency-groups]
14
- dev = ["pytest>=8"]
14
+ dev = ["black>=26.10.1", "flake8>=7.4.1", "isort>=9.0.2", "pytest==9.1.1"]
15
15
 
16
16
  [build-system]
17
- requires = ["maturin>=1.9,<2"]
17
+ requires = ["maturin==1.15.0"]
18
18
  build-backend = "maturin"
19
19
 
20
20
  [tool.maturin]
@@ -25,6 +25,9 @@ class Store:
25
25
 
26
26
  def __contains__(self, key: str | bytes) -> bool: ...
27
27
  def __len__(self) -> int: ...
28
- def keys(self) -> list[bytes]: ...
28
+ def keys(self) -> list[bytes]:
29
+ """Lists the keys in the order their current values lie in the segments, oldest segment
30
+ first, which is the order one store wrote them; reading every value is fastest in this
31
+ order."""
29
32
  def refresh(self) -> None:
30
33
  """Picks up records that other stores wrote to the directory."""
@@ -214,9 +214,14 @@ impl Store {
214
214
  self.len() == 0
215
215
  }
216
216
 
217
+ /// The keys in the order their current records lie in the segments, oldest segment first,
218
+ /// so reading every value in this order decodes each group once. This is the order one
219
+ /// store wrote them; stores that write at the same time fill segments of their own.
217
220
  pub fn keys(&self) -> Vec<Vec<u8>> {
218
221
  let state = self.read_state();
219
- state.index.keys().map(|key| key.to_vec()).collect()
222
+ let mut entries: Vec<_> = state.index.iter().collect();
223
+ entries.sort_unstable_by_key(|(_, location)| state.position(location));
224
+ entries.into_iter().map(|(key, _)| key.to_vec()).collect()
220
225
  }
221
226
 
222
227
  /// Picks up records that other stores wrote to the directory since the last call, and
@@ -611,6 +616,12 @@ impl State {
611
616
  index
612
617
  }
613
618
 
619
+ /// Orders records by age: segment names order by creation time and offsets order within a
620
+ /// segment.
621
+ fn position(&self, location: &Location) -> (&str, u64) {
622
+ (&self.segments[location.segment].name, location.end)
623
+ }
624
+
614
625
  /// Adds a record that follows the scanned part of its segment; `false` when the record
615
626
  /// cannot be placed, which leaves the state unchanged.
616
627
  fn apply(&mut self, segment_index: usize, record: &Record) -> bool {
@@ -648,12 +659,11 @@ impl State {
648
659
  segment.scanned = location.end;
649
660
  segment.group_raw = group_raw_end;
650
661
  segment.raw_total = raw_total;
651
- // The newest record of a key wins: segment names order by creation time and offsets
652
- // order within a segment.
653
- let newer = self.index.get(record.key).is_none_or(|current| {
654
- (&self.segments[current.segment].name, current.end)
655
- < (&self.segments[segment_index].name, location.end)
656
- });
662
+ // The newest record of a key wins.
663
+ let newer = self
664
+ .index
665
+ .get(record.key)
666
+ .is_none_or(|current| self.position(current) < self.position(&location));
657
667
  if newer {
658
668
  self.index.insert(record.key.into(), location);
659
669
  }
@@ -55,6 +55,21 @@ fn values_survive_reopening_and_the_latest_value_of_a_key_wins() {
55
55
  assert!(keys.binary_search(&b"large".to_vec()).is_ok());
56
56
  }
57
57
 
58
+ #[test]
59
+ fn keys_are_listed_in_the_order_a_store_wrote_their_current_values() {
60
+ let dir = tempfile::tempdir().unwrap();
61
+ {
62
+ let store = Store::open(dir.path(), small_segments()).unwrap();
63
+ for i in 0..400 {
64
+ store.put(&key(i), &noise(i as u64, 1000)).unwrap();
65
+ }
66
+ store.put(&key(3), b"replaced").unwrap();
67
+ }
68
+ assert!(segment_paths(dir.path()).len() > 1);
69
+ let expected: Vec<Vec<u8>> = (0..400).filter(|&i| i != 3).chain([3]).map(key).collect();
70
+ assert_eq!(open(dir.path()).keys(), expected);
71
+ }
72
+
58
73
  #[test]
59
74
  fn no_segment_exceeds_the_limit() {
60
75
  let dir = tempfile::tempdir().unwrap();
File without changes
File without changes
File without changes