@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5

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 (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -0,0 +1,962 @@
1
+ use std::collections::HashMap;
2
+ use std::fmt;
3
+
4
+ use crate::backend::{
5
+ Backend, BackendError, BackendWrite, CommitResult, Key, PutBatch, PutEntry, SpaceId,
6
+ StoredValue, WriteOptions,
7
+ };
8
+ use crate::storage::{StorageSpace, StorageWriteSetStats};
9
+ use ahash::RandomState;
10
+ use bytes::Bytes;
11
+
12
+ type FastHashBuilder = RandomState;
13
+
14
+ pub trait IntoStorageSpace {
15
+ fn into_storage_space(self) -> StorageSpace;
16
+ }
17
+
18
+ impl IntoStorageSpace for StorageSpace {
19
+ fn into_storage_space(self) -> StorageSpace {
20
+ self
21
+ }
22
+ }
23
+
24
+ pub trait IntoStorageKey {
25
+ fn into_storage_key(self) -> Key;
26
+ }
27
+
28
+ impl IntoStorageKey for Key {
29
+ fn into_storage_key(self) -> Key {
30
+ self
31
+ }
32
+ }
33
+
34
+ impl IntoStorageKey for Vec<u8> {
35
+ fn into_storage_key(self) -> Key {
36
+ Key(Bytes::from(self))
37
+ }
38
+ }
39
+
40
+ impl IntoStorageKey for &[u8] {
41
+ fn into_storage_key(self) -> Key {
42
+ Key(Bytes::copy_from_slice(self))
43
+ }
44
+ }
45
+
46
+ pub trait IntoStorageValue {
47
+ fn into_storage_value(self) -> StoredValue;
48
+ }
49
+
50
+ impl IntoStorageValue for StoredValue {
51
+ fn into_storage_value(self) -> StoredValue {
52
+ self
53
+ }
54
+ }
55
+
56
+ impl IntoStorageValue for Vec<u8> {
57
+ fn into_storage_value(self) -> StoredValue {
58
+ StoredValue {
59
+ bytes: Bytes::from(self),
60
+ }
61
+ }
62
+ }
63
+
64
+ impl IntoStorageValue for &[u8] {
65
+ fn into_storage_value(self) -> StoredValue {
66
+ StoredValue {
67
+ bytes: Bytes::copy_from_slice(self),
68
+ }
69
+ }
70
+ }
71
+
72
+ #[derive(Clone, Debug)]
73
+ pub struct StorageWriteSet {
74
+ groups: Vec<StorageWriteGroup>,
75
+ group_index: HashMap<SpaceId, usize, FastHashBuilder>,
76
+ stats: StorageWriteSetStats,
77
+ }
78
+
79
+ #[derive(Clone, Debug)]
80
+ struct StorageWriteGroup {
81
+ space: StorageSpace,
82
+ puts: Vec<PutEntry>,
83
+ deletes: Vec<Key>,
84
+ conflicting_declarations: Vec<StorageSpace>,
85
+ }
86
+
87
+ #[derive(Clone, Debug, PartialEq, Eq)]
88
+ pub enum StorageWriteSetError {
89
+ ConflictingSpaceDeclaration {
90
+ id: SpaceId,
91
+ existing_name: &'static str,
92
+ incoming_name: &'static str,
93
+ },
94
+ DuplicateMutation {
95
+ space: StorageSpace,
96
+ key: Key,
97
+ },
98
+ Backend(BackendError),
99
+ }
100
+
101
+ impl StorageWriteSet {
102
+ /// Creates an empty canonical write set.
103
+ ///
104
+ /// Callers must stage at most one final mutation for each `(space, key)`.
105
+ /// The set validates that contract before lowering or commit.
106
+ pub fn new() -> Self {
107
+ Self::default()
108
+ }
109
+
110
+ /// Creates a canonical write set with capacity hints.
111
+ pub fn with_capacity(_expected_mutations: usize, expected_spaces: usize) -> Self {
112
+ Self {
113
+ groups: Vec::with_capacity(expected_spaces),
114
+ group_index: HashMap::with_capacity_and_hasher(
115
+ expected_spaces,
116
+ FastHashBuilder::with_seeds(0, 0, 0, 0),
117
+ ),
118
+ stats: StorageWriteSetStats::default(),
119
+ }
120
+ }
121
+
122
+ pub fn is_empty(&self) -> bool {
123
+ self.groups
124
+ .iter()
125
+ .all(|group| group.puts.is_empty() && group.deletes.is_empty())
126
+ }
127
+
128
+ pub(crate) async fn apply<B>(
129
+ self,
130
+ writer: &mut crate::storage::context::StorageWriteTransaction<'_, B>,
131
+ ) -> Result<StorageWriteSetStats, crate::LixError>
132
+ where
133
+ B: Backend,
134
+ {
135
+ writer.write_set(self)
136
+ }
137
+
138
+ pub fn put<S, K, V>(&mut self, space: S, key: K, value: V)
139
+ where
140
+ S: IntoStorageSpace,
141
+ K: IntoStorageKey,
142
+ V: IntoStorageValue,
143
+ {
144
+ let value = value.into_storage_value();
145
+ self.stats.staged_puts += 1;
146
+ self.stats.written_bytes += value.bytes.len() as u64;
147
+ self.group_mut(space.into_storage_space())
148
+ .puts
149
+ .push(PutEntry {
150
+ key: key.into_storage_key(),
151
+ value,
152
+ });
153
+ }
154
+
155
+ pub fn delete<S, K>(&mut self, space: S, key: K)
156
+ where
157
+ S: IntoStorageSpace,
158
+ K: IntoStorageKey,
159
+ {
160
+ self.stats.staged_deletes += 1;
161
+ self.group_mut(space.into_storage_space())
162
+ .deletes
163
+ .push(key.into_storage_key());
164
+ }
165
+
166
+ /// Reserves capacity for a storage space's grouped puts and deletes.
167
+ ///
168
+ /// This is most useful with canonical construction, where domain stores can
169
+ /// often count final mutations before staging them.
170
+ pub fn reserve_space(
171
+ &mut self,
172
+ space: StorageSpace,
173
+ expected_puts: usize,
174
+ expected_deletes: usize,
175
+ ) {
176
+ let group = self.group_mut(space);
177
+ group.puts.reserve(expected_puts);
178
+ group.deletes.reserve(expected_deletes);
179
+ }
180
+
181
+ pub(crate) fn move_space_to_end(&mut self, space: StorageSpace) {
182
+ let Some(index) = self.group_index.get(&space.id).copied() else {
183
+ return;
184
+ };
185
+ if index + 1 == self.groups.len() {
186
+ return;
187
+ }
188
+ let group = self.groups.remove(index);
189
+ self.groups.push(group);
190
+ self.group_index.clear();
191
+ for (index, group) in self.groups.iter().enumerate() {
192
+ self.group_index.insert(group.space.id, index);
193
+ }
194
+ }
195
+
196
+ pub fn extend(&mut self, other: StorageWriteSet) {
197
+ for group in other.groups {
198
+ let space = group.space;
199
+ let conflicting_declarations = group.conflicting_declarations;
200
+ for put in group.puts {
201
+ self.put(space, put.key, put.value);
202
+ }
203
+ for delete in group.deletes {
204
+ self.delete(space, delete);
205
+ }
206
+
207
+ let target = self.group_mut(space);
208
+ target
209
+ .conflicting_declarations
210
+ .extend(conflicting_declarations);
211
+ }
212
+ }
213
+
214
+ pub fn stats(&self) -> StorageWriteSetStats {
215
+ self.stats.clone()
216
+ }
217
+
218
+ /// Validates the canonical write-set contract.
219
+ ///
220
+ /// This performs the full duplicate/conflicting-declaration scan before
221
+ /// lowering so the backend never receives ambiguous final mutations.
222
+ pub fn validate(&self) -> Result<(), StorageWriteSetError> {
223
+ for group in &self.groups {
224
+ if let Some(incoming) = group.conflicting_declarations.first() {
225
+ return Err(StorageWriteSetError::ConflictingSpaceDeclaration {
226
+ id: group.space.id,
227
+ existing_name: group.space.name,
228
+ incoming_name: incoming.name,
229
+ });
230
+ }
231
+ }
232
+
233
+ let mut seen = HashMap::<(SpaceId, Key), StorageSpace, FastHashBuilder>::with_hasher(
234
+ FastHashBuilder::with_seeds(0, 0, 0, 0),
235
+ );
236
+ for group in &self.groups {
237
+ for put in &group.puts {
238
+ let key = (group.space.id, put.key.clone());
239
+ if seen.insert(key, group.space).is_some() {
240
+ return Err(StorageWriteSetError::DuplicateMutation {
241
+ space: group.space,
242
+ key: put.key.clone(),
243
+ });
244
+ }
245
+ }
246
+ for delete in &group.deletes {
247
+ let key = (group.space.id, delete.clone());
248
+ if seen.insert(key, group.space).is_some() {
249
+ return Err(StorageWriteSetError::DuplicateMutation {
250
+ space: group.space,
251
+ key: delete.clone(),
252
+ });
253
+ }
254
+ }
255
+ }
256
+ Ok(())
257
+ }
258
+
259
+ pub fn lower_into<W>(self, write: &mut W) -> Result<StorageWriteSetStats, StorageWriteSetError>
260
+ where
261
+ W: BackendWrite,
262
+ {
263
+ self.validate()?;
264
+ self.lower_validated_into(write)
265
+ }
266
+
267
+ fn lower_validated_into<W>(
268
+ self,
269
+ write: &mut W,
270
+ ) -> Result<StorageWriteSetStats, StorageWriteSetError>
271
+ where
272
+ W: BackendWrite,
273
+ {
274
+ let StorageWriteSet {
275
+ groups, mut stats, ..
276
+ } = self;
277
+
278
+ for group in groups {
279
+ if !group.puts.is_empty() {
280
+ stats.put_batches += 1;
281
+ stats.backend_calls += 1;
282
+ let entries = group
283
+ .puts
284
+ .into_iter()
285
+ .map(|entry| PutEntry {
286
+ key: group.space.encode_key(&entry.key),
287
+ value: entry.value,
288
+ })
289
+ .collect();
290
+ write
291
+ .put_many(PutBatch { entries })
292
+ .map_err(StorageWriteSetError::Backend)?;
293
+ }
294
+ if !group.deletes.is_empty() {
295
+ stats.delete_batches += 1;
296
+ stats.backend_calls += 1;
297
+ let deletes = group
298
+ .deletes
299
+ .iter()
300
+ .map(|key| group.space.encode_key(key))
301
+ .collect::<Vec<_>>();
302
+ write
303
+ .delete_many(&deletes)
304
+ .map_err(StorageWriteSetError::Backend)?;
305
+ }
306
+ }
307
+
308
+ Ok(stats)
309
+ }
310
+
311
+ pub fn commit<B>(
312
+ self,
313
+ backend: &B,
314
+ opts: WriteOptions,
315
+ ) -> Result<(CommitResult, StorageWriteSetStats), StorageWriteSetError>
316
+ where
317
+ B: Backend,
318
+ {
319
+ self.validate()?;
320
+ let mut write = backend
321
+ .begin_write(opts)
322
+ .map_err(StorageWriteSetError::Backend)?;
323
+ let stats = match self.lower_validated_into(&mut write) {
324
+ Ok(stats) => stats,
325
+ Err(error) => {
326
+ let _ = write.rollback();
327
+ return Err(error);
328
+ }
329
+ };
330
+ let result = write.commit().map_err(StorageWriteSetError::Backend)?;
331
+ Ok((result, stats))
332
+ }
333
+
334
+ fn group_mut(&mut self, space: StorageSpace) -> &mut StorageWriteGroup {
335
+ if let Some(index) = self.group_index.get(&space.id).copied() {
336
+ let group = &mut self.groups[index];
337
+ if group.space.name != space.name {
338
+ group.conflicting_declarations.push(space);
339
+ }
340
+ return group;
341
+ }
342
+
343
+ let index = self.groups.len();
344
+ self.group_index.insert(space.id, index);
345
+ self.stats.touched_spaces += 1;
346
+ self.groups.push(StorageWriteGroup::new(space));
347
+ let group = &mut self.groups[index];
348
+ if group.space.name != space.name {
349
+ group.conflicting_declarations.push(space);
350
+ }
351
+ group
352
+ }
353
+ }
354
+
355
+ impl Default for StorageWriteSet {
356
+ fn default() -> Self {
357
+ Self {
358
+ groups: Vec::new(),
359
+ group_index: HashMap::with_hasher(FastHashBuilder::with_seeds(0, 0, 0, 0)),
360
+ stats: StorageWriteSetStats::default(),
361
+ }
362
+ }
363
+ }
364
+
365
+ impl StorageWriteGroup {
366
+ fn new(space: StorageSpace) -> Self {
367
+ Self {
368
+ space,
369
+ puts: Vec::new(),
370
+ deletes: Vec::new(),
371
+ conflicting_declarations: Vec::new(),
372
+ }
373
+ }
374
+ }
375
+
376
+ impl fmt::Display for StorageWriteSetError {
377
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378
+ match self {
379
+ StorageWriteSetError::ConflictingSpaceDeclaration {
380
+ id,
381
+ existing_name,
382
+ incoming_name,
383
+ } => write!(
384
+ f,
385
+ "conflicting storage space declarations for {id:?}: {existing_name} vs {incoming_name}"
386
+ ),
387
+ StorageWriteSetError::DuplicateMutation { space, key } => {
388
+ write!(f, "duplicate storage mutation for {space}/{key:?}")
389
+ }
390
+ StorageWriteSetError::Backend(error) => write!(f, "{error}"),
391
+ }
392
+ }
393
+ }
394
+
395
+ impl std::error::Error for StorageWriteSetError {}
396
+
397
+ impl From<BackendError> for StorageWriteSetError {
398
+ fn from(error: BackendError) -> Self {
399
+ Self::Backend(error)
400
+ }
401
+ }
402
+
403
+ #[cfg(test)]
404
+ mod tests {
405
+ use bytes::Bytes;
406
+
407
+ use std::cell::{Cell, RefCell};
408
+ use std::rc::Rc;
409
+
410
+ use crate::backend::{
411
+ Backend, BackendCapabilities, BackendError, BackendRangeScan, BackendRead, BackendWrite,
412
+ BufferedRangeScan, CommitResult, DurableWriteLock, GetOptions, InMemoryBackend, Key,
413
+ KeyRange, PointVisitor, PutBatch, ReadOptions, ScanOptions, ScanResult, ScanVisitor,
414
+ SpaceId, StoredValue, WriteConcurrency, WriteOptions, WriteStats,
415
+ };
416
+ use crate::storage::{StorageSpace, StorageWriteSet, StorageWriteSetError};
417
+
418
+ fn key(bytes: &'static str) -> Key {
419
+ Key(Bytes::from_static(bytes.as_bytes()))
420
+ }
421
+
422
+ fn value(bytes: &'static str) -> StoredValue {
423
+ StoredValue {
424
+ bytes: Bytes::from_static(bytes.as_bytes()),
425
+ }
426
+ }
427
+
428
+ fn space(id: u32) -> StorageSpace {
429
+ match id {
430
+ 1 => StorageSpace::new(SpaceId(1), "test.space.one"),
431
+ 2 => StorageSpace::new(SpaceId(2), "test.space.two"),
432
+ _ => StorageSpace::new(SpaceId(id), "test.space.other"),
433
+ }
434
+ }
435
+
436
+ #[test]
437
+ fn write_set_rejects_duplicate_final_mutations() {
438
+ let backend = InMemoryBackend::new();
439
+ let mut writes = StorageWriteSet::new();
440
+ writes.put(space(1), key("a"), value("A"));
441
+ writes.delete(space(1), key("a"));
442
+
443
+ let error = writes
444
+ .commit(&backend, WriteOptions::default())
445
+ .expect_err("duplicate mutation should fail");
446
+
447
+ assert!(matches!(
448
+ error,
449
+ StorageWriteSetError::DuplicateMutation {
450
+ space: duplicate_space,
451
+ key: ref duplicate_key
452
+ } if duplicate_space == space(1) && *duplicate_key == key("a")
453
+ ));
454
+ assert!(error
455
+ .to_string()
456
+ .contains("duplicate storage mutation for test.space.one"));
457
+ }
458
+
459
+ #[test]
460
+ fn duplicate_puts_are_rejected() {
461
+ let backend = InMemoryBackend::new();
462
+ let mut writes = StorageWriteSet::new();
463
+ writes.put(space(1), key("a"), value("A"));
464
+ writes.put(space(1), key("a"), value("B"));
465
+
466
+ assert!(matches!(
467
+ writes.commit(&backend, WriteOptions::default()),
468
+ Err(StorageWriteSetError::DuplicateMutation {
469
+ space: duplicate_space,
470
+ key: duplicate_key
471
+ }) if duplicate_space == space(1) && duplicate_key == key("a")
472
+ ));
473
+ }
474
+
475
+ #[test]
476
+ fn duplicate_deletes_are_rejected() {
477
+ let backend = InMemoryBackend::new();
478
+ let mut writes = StorageWriteSet::new();
479
+ writes.delete(space(1), key("a"));
480
+ writes.delete(space(1), key("a"));
481
+
482
+ assert!(matches!(
483
+ writes.commit(&backend, WriteOptions::default()),
484
+ Err(StorageWriteSetError::DuplicateMutation {
485
+ space: duplicate_space,
486
+ key: duplicate_key
487
+ }) if duplicate_space == space(1) && duplicate_key == key("a")
488
+ ));
489
+ }
490
+
491
+ #[test]
492
+ fn put_records_stats_without_immediate_duplicate_validation() {
493
+ let mut writes = StorageWriteSet::new();
494
+ writes.put(space(1), key("a"), value("A"));
495
+ writes.put(space(1), key("a"), value("B"));
496
+
497
+ let stats = writes.stats();
498
+ assert_eq!(stats.staged_puts, 2);
499
+ assert_eq!(stats.written_bytes, 2);
500
+
501
+ assert!(matches!(
502
+ writes.validate(),
503
+ Err(StorageWriteSetError::DuplicateMutation {
504
+ space: duplicate_space,
505
+ key: duplicate_key
506
+ }) if duplicate_space == space(1) && duplicate_key == key("a")
507
+ ));
508
+ }
509
+
510
+ #[test]
511
+ fn delete_records_stats_without_immediate_duplicate_validation() {
512
+ let mut writes = StorageWriteSet::new();
513
+ writes.put(space(1), key("a"), value("A"));
514
+ writes.delete(space(1), key("a"));
515
+
516
+ let stats = writes.stats();
517
+ assert_eq!(stats.staged_puts, 1);
518
+ assert_eq!(stats.staged_deletes, 1);
519
+
520
+ assert!(matches!(
521
+ writes.validate(),
522
+ Err(StorageWriteSetError::DuplicateMutation {
523
+ space: duplicate_space,
524
+ key: duplicate_key
525
+ }) if duplicate_space == space(1) && duplicate_key == key("a")
526
+ ));
527
+ }
528
+
529
+ #[test]
530
+ fn canonical_staging_tracks_stats_and_lowers_without_duplicate_index() {
531
+ let mut writes = StorageWriteSet::with_capacity(3, 2);
532
+ writes.reserve_space(space(1), 2, 0);
533
+ writes.reserve_space(space(2), 0, 1);
534
+ writes.put(space(1), key("a"), value("A"));
535
+ writes.put(space(1), key("b"), value("B"));
536
+ writes.delete(space(2), key("c"));
537
+
538
+ let stats = writes.stats();
539
+ assert_eq!(stats.staged_puts, 2);
540
+ assert_eq!(stats.staged_deletes, 1);
541
+ assert_eq!(stats.touched_spaces, 2);
542
+ assert_eq!(stats.written_bytes, 2);
543
+
544
+ let mut write = CountingWrite::default();
545
+ let stats = writes.lower_into(&mut write).expect("lower");
546
+
547
+ assert_eq!(write.put_batches.borrow().len(), 1);
548
+ assert_eq!(write.delete_batches.borrow().len(), 1);
549
+ assert_eq!(stats.put_batches, 1);
550
+ assert_eq!(stats.delete_batches, 1);
551
+ assert_eq!(stats.backend_calls, 2);
552
+ }
553
+
554
+ #[test]
555
+ fn conflicting_space_declarations_are_rejected() {
556
+ let backend = InMemoryBackend::new();
557
+ let mut writes = StorageWriteSet::new();
558
+ writes.put(
559
+ StorageSpace::new(SpaceId(1), "test.space.one"),
560
+ key("a"),
561
+ value("A"),
562
+ );
563
+ writes.put(
564
+ StorageSpace::new(SpaceId(1), "test.space.renamed"),
565
+ key("b"),
566
+ value("B"),
567
+ );
568
+
569
+ let error = writes
570
+ .commit(&backend, WriteOptions::default())
571
+ .expect_err("conflicting space declaration should fail");
572
+
573
+ assert!(matches!(
574
+ error,
575
+ StorageWriteSetError::ConflictingSpaceDeclaration {
576
+ id: SpaceId(1),
577
+ existing_name: "test.space.one",
578
+ incoming_name: "test.space.renamed",
579
+ }
580
+ ));
581
+ }
582
+
583
+ #[test]
584
+ fn write_set_groups_by_space_id_not_name() {
585
+ let mut writes = StorageWriteSet::new();
586
+ writes.put(
587
+ StorageSpace::new(SpaceId(1), "test.space.one"),
588
+ key("a"),
589
+ value("A"),
590
+ );
591
+ writes.put(
592
+ StorageSpace::new(SpaceId(1), "test.space.renamed"),
593
+ key("b"),
594
+ value("B"),
595
+ );
596
+
597
+ let stats = writes.stats();
598
+
599
+ assert_eq!(stats.touched_spaces, 1);
600
+ assert!(matches!(
601
+ writes.validate(),
602
+ Err(StorageWriteSetError::ConflictingSpaceDeclaration {
603
+ id: SpaceId(1),
604
+ existing_name: "test.space.one",
605
+ incoming_name: "test.space.renamed",
606
+ })
607
+ ));
608
+ }
609
+
610
+ #[test]
611
+ fn conflicting_space_declaration_across_extend_is_rejected() {
612
+ let backend = InMemoryBackend::new();
613
+ let mut left = StorageWriteSet::new();
614
+ left.put(
615
+ StorageSpace::new(SpaceId(1), "test.space.one"),
616
+ key("a"),
617
+ value("A"),
618
+ );
619
+
620
+ let mut right = StorageWriteSet::new();
621
+ right.put(
622
+ StorageSpace::new(SpaceId(1), "test.space.renamed"),
623
+ key("b"),
624
+ value("B"),
625
+ );
626
+
627
+ left.extend(right);
628
+
629
+ assert!(matches!(
630
+ left.commit(&backend, WriteOptions::default()),
631
+ Err(StorageWriteSetError::ConflictingSpaceDeclaration {
632
+ id: SpaceId(1),
633
+ existing_name: "test.space.one",
634
+ incoming_name: "test.space.renamed",
635
+ })
636
+ ));
637
+ }
638
+
639
+ #[test]
640
+ fn duplicate_validation_happens_before_opening_backend_write() {
641
+ let backend = CountingBackend::default();
642
+ let mut writes = StorageWriteSet::new();
643
+ writes.put(space(1), key("a"), value("A"));
644
+ writes.put(space(1), key("a"), value("B"));
645
+
646
+ assert!(matches!(
647
+ writes.commit(&backend, WriteOptions::default()),
648
+ Err(StorageWriteSetError::DuplicateMutation { .. })
649
+ ));
650
+ assert_eq!(backend.state.begin_write_calls.get(), 0);
651
+ assert_eq!(backend.state.commit_calls.get(), 0);
652
+ assert_eq!(backend.state.rollback_calls.get(), 0);
653
+ }
654
+
655
+ #[test]
656
+ fn conflicting_space_validation_happens_before_opening_backend_write() {
657
+ let backend = CountingBackend::default();
658
+ let mut writes = StorageWriteSet::new();
659
+ writes.put(
660
+ StorageSpace::new(SpaceId(1), "test.space.one"),
661
+ key("a"),
662
+ value("A"),
663
+ );
664
+ writes.put(
665
+ StorageSpace::new(SpaceId(1), "test.space.renamed"),
666
+ key("b"),
667
+ value("B"),
668
+ );
669
+
670
+ assert!(matches!(
671
+ writes.commit(&backend, WriteOptions::default()),
672
+ Err(StorageWriteSetError::ConflictingSpaceDeclaration { .. })
673
+ ));
674
+ assert_eq!(backend.state.begin_write_calls.get(), 0);
675
+ assert_eq!(backend.state.commit_calls.get(), 0);
676
+ assert_eq!(backend.state.rollback_calls.get(), 0);
677
+ }
678
+
679
+ #[test]
680
+ fn lower_failure_rolls_back_once() {
681
+ let backend = CountingBackend::failing(FailPoint::PutMany);
682
+ let mut writes = StorageWriteSet::new();
683
+ writes.put(space(1), key("a"), value("A"));
684
+
685
+ assert!(matches!(
686
+ writes.commit(&backend, WriteOptions::default()),
687
+ Err(StorageWriteSetError::Backend(BackendError::Io(message))) if message == "put_many failed"
688
+ ));
689
+ assert_eq!(backend.state.begin_write_calls.get(), 1);
690
+ assert_eq!(backend.state.commit_calls.get(), 0);
691
+ assert_eq!(backend.state.rollback_calls.get(), 1);
692
+ }
693
+
694
+ #[test]
695
+ fn delete_lower_failure_rolls_back_once() {
696
+ let backend = CountingBackend::failing(FailPoint::DeleteMany);
697
+ let mut writes = StorageWriteSet::new();
698
+ writes.delete(space(1), key("a"));
699
+
700
+ assert!(matches!(
701
+ writes.commit(&backend, WriteOptions::default()),
702
+ Err(StorageWriteSetError::Backend(BackendError::Io(message))) if message == "delete_many failed"
703
+ ));
704
+ assert_eq!(backend.state.begin_write_calls.get(), 1);
705
+ assert_eq!(backend.state.commit_calls.get(), 0);
706
+ assert_eq!(backend.state.rollback_calls.get(), 1);
707
+ }
708
+
709
+ #[test]
710
+ fn commit_failure_is_reported_without_successful_commit_stats() {
711
+ let backend = CountingBackend::failing(FailPoint::Commit);
712
+ let mut writes = StorageWriteSet::new();
713
+ writes.put(space(1), key("a"), value("A"));
714
+
715
+ assert!(matches!(
716
+ writes.commit(&backend, WriteOptions::default()),
717
+ Err(StorageWriteSetError::Backend(BackendError::Durability))
718
+ ));
719
+ assert_eq!(backend.state.begin_write_calls.get(), 1);
720
+ assert_eq!(backend.state.commit_calls.get(), 1);
721
+ assert_eq!(backend.state.rollback_calls.get(), 0);
722
+ assert!(backend.state.put_batches.borrow().is_empty());
723
+ }
724
+
725
+ #[test]
726
+ fn same_key_in_different_spaces_is_allowed() {
727
+ let backend = InMemoryBackend::new();
728
+ let mut writes = StorageWriteSet::new();
729
+ writes.put(space(1), key("a"), value("A"));
730
+ writes.put(space(2), key("a"), value("B"));
731
+
732
+ writes
733
+ .commit(&backend, WriteOptions::default())
734
+ .expect("different spaces are independent");
735
+ }
736
+
737
+ #[test]
738
+ fn lower_into_groups_by_space_and_operation() {
739
+ let mut writes = StorageWriteSet::new();
740
+ writes.put(space(1), key("a"), value("A"));
741
+ writes.put(space(1), key("b"), value("B"));
742
+ writes.put(space(2), key("a"), value("C"));
743
+ writes.delete(space(1), key("c"));
744
+ writes.delete(space(2), key("d"));
745
+
746
+ let mut write = CountingWrite::default();
747
+ let stats = writes.lower_into(&mut write).expect("lower");
748
+
749
+ assert_eq!(write.put_batches.borrow().len(), 2);
750
+ assert_eq!(write.delete_batches.borrow().len(), 2);
751
+ assert_eq!(write.commit_calls.get(), 0);
752
+ assert_eq!(stats.put_batches, 2);
753
+ assert_eq!(stats.delete_batches, 2);
754
+ assert_eq!(stats.backend_calls, 4);
755
+ }
756
+
757
+ #[test]
758
+ fn commit_uses_one_backend_write_and_one_commit() {
759
+ let backend = CountingBackend::default();
760
+ let mut writes = StorageWriteSet::new();
761
+ writes.put(space(1), key("a"), value("A"));
762
+ writes.delete(space(1), key("b"));
763
+
764
+ writes
765
+ .commit(&backend, WriteOptions::default())
766
+ .expect("commit");
767
+
768
+ assert_eq!(backend.state.begin_write_calls.get(), 1);
769
+ assert_eq!(backend.state.commit_calls.get(), 1);
770
+ assert_eq!(backend.state.rollback_calls.get(), 0);
771
+ assert_eq!(backend.state.put_batches.borrow().len(), 1);
772
+ assert_eq!(backend.state.delete_batches.borrow().len(), 1);
773
+ }
774
+
775
+ #[derive(Clone, Default)]
776
+ struct CountingBackend {
777
+ state: Rc<CountingState>,
778
+ }
779
+
780
+ #[derive(Default)]
781
+ struct CountingState {
782
+ begin_write_calls: Cell<u64>,
783
+ commit_calls: Cell<u64>,
784
+ rollback_calls: Cell<u64>,
785
+ put_batches: RefCell<Vec<(SpaceId, Vec<Key>)>>,
786
+ delete_batches: RefCell<Vec<(SpaceId, Vec<Key>)>>,
787
+ fail_point: Cell<Option<FailPoint>>,
788
+ }
789
+
790
+ #[derive(Clone, Default)]
791
+ struct CountingRead;
792
+
793
+ struct CountingWrite {
794
+ state: Rc<CountingState>,
795
+ commit_calls: Cell<u64>,
796
+ put_batches: RefCell<Vec<(SpaceId, Vec<Key>)>>,
797
+ delete_batches: RefCell<Vec<(SpaceId, Vec<Key>)>>,
798
+ }
799
+
800
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
801
+ enum FailPoint {
802
+ PutMany,
803
+ DeleteMany,
804
+ Commit,
805
+ }
806
+
807
+ impl CountingBackend {
808
+ fn failing(fail_point: FailPoint) -> Self {
809
+ let backend = Self::default();
810
+ backend.state.fail_point.set(Some(fail_point));
811
+ backend
812
+ }
813
+ }
814
+
815
+ impl Default for CountingWrite {
816
+ fn default() -> Self {
817
+ Self {
818
+ state: Rc::new(CountingState::default()),
819
+ commit_calls: Cell::new(0),
820
+ put_batches: RefCell::new(Vec::new()),
821
+ delete_batches: RefCell::new(Vec::new()),
822
+ }
823
+ }
824
+ }
825
+
826
+ impl Backend for CountingBackend {
827
+ type Read<'a>
828
+ = CountingRead
829
+ where
830
+ Self: 'a;
831
+
832
+ type Write<'a>
833
+ = CountingWrite
834
+ where
835
+ Self: 'a;
836
+
837
+ fn capabilities(&self) -> BackendCapabilities {
838
+ BackendCapabilities::v0(WriteConcurrency::SingleWriter)
839
+ }
840
+
841
+ fn begin_read(&self, _opts: ReadOptions) -> Result<Self::Read<'_>, BackendError> {
842
+ Ok(CountingRead)
843
+ }
844
+
845
+ fn begin_write(&self, _opts: WriteOptions) -> Result<Self::Write<'_>, BackendError> {
846
+ self.state
847
+ .begin_write_calls
848
+ .set(self.state.begin_write_calls.get() + 1);
849
+ Ok(CountingWrite {
850
+ state: Rc::clone(&self.state),
851
+ commit_calls: Cell::new(0),
852
+ put_batches: RefCell::new(Vec::new()),
853
+ delete_batches: RefCell::new(Vec::new()),
854
+ })
855
+ }
856
+
857
+ fn durable_write_lock(&self) -> DurableWriteLock {
858
+ DurableWriteLock::new()
859
+ }
860
+ }
861
+
862
+ impl BackendRead for CountingRead {
863
+ type RangeScan<'a> = BufferedRangeScan;
864
+
865
+ fn visit_keys<V>(
866
+ &self,
867
+ _keys: &[Key],
868
+ _opts: GetOptions<'_>,
869
+ _visitor: &mut V,
870
+ ) -> Result<(), BackendError>
871
+ where
872
+ V: PointVisitor + ?Sized,
873
+ {
874
+ unimplemented!("not used by write-set tests")
875
+ }
876
+
877
+ fn with_range_scan<T, F>(
878
+ &self,
879
+ _range: KeyRange,
880
+ _opts: ScanOptions<'_>,
881
+ _f: F,
882
+ ) -> Result<T, BackendError>
883
+ where
884
+ F: FnOnce(&mut Self::RangeScan<'_>) -> Result<T, BackendError>,
885
+ {
886
+ unimplemented!("not used by write-set tests")
887
+ }
888
+ }
889
+
890
+ impl BackendWrite for CountingWrite {
891
+ fn put_many(&mut self, entries: PutBatch) -> Result<(), BackendError> {
892
+ if self.state.fail_point.get() == Some(FailPoint::PutMany) {
893
+ return Err(BackendError::Io("put_many failed".to_string()));
894
+ }
895
+ let space = entries
896
+ .entries
897
+ .first()
898
+ .map(|entry| physical_space(&entry.key))
899
+ .unwrap_or(SpaceId(0));
900
+ let keys = entries
901
+ .entries
902
+ .into_iter()
903
+ .map(|entry| logical_key(entry.key))
904
+ .collect();
905
+ self.put_batches.borrow_mut().push((space, keys));
906
+ Ok(())
907
+ }
908
+
909
+ fn delete_many(&mut self, keys: &[Key]) -> Result<(), BackendError> {
910
+ if self.state.fail_point.get() == Some(FailPoint::DeleteMany) {
911
+ return Err(BackendError::Io("delete_many failed".to_string()));
912
+ }
913
+ let space = keys.first().map(physical_space).unwrap_or(SpaceId(0));
914
+ self.delete_batches
915
+ .borrow_mut()
916
+ .push((space, keys.iter().cloned().map(logical_key).collect()));
917
+ Ok(())
918
+ }
919
+
920
+ fn delete_range(&mut self, _range: KeyRange) -> Result<(), BackendError> {
921
+ Ok(())
922
+ }
923
+
924
+ fn commit(self) -> Result<CommitResult, BackendError> {
925
+ self.state
926
+ .commit_calls
927
+ .set(self.state.commit_calls.get() + 1);
928
+ if self.state.fail_point.get() == Some(FailPoint::Commit) {
929
+ return Err(BackendError::Durability);
930
+ }
931
+ self.state
932
+ .put_batches
933
+ .borrow_mut()
934
+ .extend(self.put_batches.into_inner());
935
+ self.state
936
+ .delete_batches
937
+ .borrow_mut()
938
+ .extend(self.delete_batches.into_inner());
939
+ Ok(CommitResult {
940
+ commit_id: None,
941
+ stats: WriteStats::default(),
942
+ })
943
+ }
944
+
945
+ fn rollback(self) -> Result<(), BackendError> {
946
+ self.state
947
+ .rollback_calls
948
+ .set(self.state.rollback_calls.get() + 1);
949
+ Ok(())
950
+ }
951
+ }
952
+
953
+ fn physical_space(key: &Key) -> SpaceId {
954
+ SpaceId(u32::from_be_bytes(
955
+ key.0[..4].try_into().expect("space prefix"),
956
+ ))
957
+ }
958
+
959
+ fn logical_key(key: Key) -> Key {
960
+ Key(key.0.slice(4..))
961
+ }
962
+ }