@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.2

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 (191) hide show
  1. package/SKILL.md +305 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
@@ -0,0 +1,3406 @@
1
+ use crate::binary_cas::{BinaryCasContext, BlobHash, BlobWrite};
2
+ use crate::changelog::{
3
+ canonicalize_materialized_change, CanonicalChange, ChangelogContext, ChangelogScanRequest,
4
+ MaterializedCanonicalChange,
5
+ };
6
+ use crate::entity_identity::EntityIdentity;
7
+ use crate::entity_identity::EntityIdentityPart;
8
+ use crate::json_store::context::JsonStoreContext;
9
+ use crate::json_store::types::{JsonProjectionPath, JsonRef};
10
+ use crate::storage::{
11
+ KvGetGroup, KvGetRequest, KvScanRange, KvScanRequest, KvWriteBatch, StorageContext,
12
+ StorageWriteSet,
13
+ };
14
+ use crate::tracked_state::{
15
+ TrackedStateContext, TrackedStateDiffRequest, TrackedStateFilter, TrackedStateProjection,
16
+ TrackedStateRow, TrackedStateRowRequest, TrackedStateScanRequest,
17
+ };
18
+ use crate::untracked_state::{
19
+ canonicalize_materialized_row, MaterializedUntrackedStateRow, UntrackedStateContext,
20
+ UntrackedStateFilter, UntrackedStateProjection, UntrackedStateRowRequest,
21
+ UntrackedStateScanRequest,
22
+ };
23
+ use crate::{Backend, LixError, NullableKeyFilter};
24
+ use std::sync::Arc;
25
+ use std::time::{Duration, Instant};
26
+
27
+ #[derive(Debug, Clone, Copy)]
28
+ pub struct StorageBenchConfig {
29
+ pub rows: usize,
30
+ pub blob_bytes: usize,
31
+ pub state_payload_bytes: usize,
32
+ pub key_pattern: StorageBenchKeyPattern,
33
+ pub selectivity: StorageBenchSelectivity,
34
+ pub update_fraction: StorageBenchUpdateFraction,
35
+ }
36
+
37
+ impl StorageBenchConfig {
38
+ pub fn with_rows(mut self, rows: usize) -> Self {
39
+ self.rows = rows;
40
+ self
41
+ }
42
+
43
+ pub fn with_blob_bytes(mut self, blob_bytes: usize) -> Self {
44
+ self.blob_bytes = blob_bytes;
45
+ self
46
+ }
47
+
48
+ pub fn with_state_payload_bytes(mut self, state_payload_bytes: usize) -> Self {
49
+ self.state_payload_bytes = state_payload_bytes;
50
+ self
51
+ }
52
+
53
+ pub fn with_key_pattern(mut self, key_pattern: StorageBenchKeyPattern) -> Self {
54
+ self.key_pattern = key_pattern;
55
+ self
56
+ }
57
+
58
+ pub fn with_selectivity(mut self, selectivity: StorageBenchSelectivity) -> Self {
59
+ self.selectivity = selectivity;
60
+ self
61
+ }
62
+
63
+ pub fn with_update_fraction(mut self, update_fraction: StorageBenchUpdateFraction) -> Self {
64
+ self.update_fraction = update_fraction;
65
+ self
66
+ }
67
+ }
68
+
69
+ #[derive(Debug, Clone, Copy)]
70
+ pub enum StorageBenchKeyPattern {
71
+ Sequential,
72
+ Random,
73
+ }
74
+
75
+ #[derive(Debug, Clone, Copy)]
76
+ pub enum StorageBenchSelectivity {
77
+ Percent1,
78
+ Percent10,
79
+ Percent100,
80
+ }
81
+
82
+ impl StorageBenchSelectivity {
83
+ fn matches(self, index: usize) -> bool {
84
+ match self {
85
+ Self::Percent1 => index % 100 == 0,
86
+ Self::Percent10 => index % 10 == 0,
87
+ Self::Percent100 => true,
88
+ }
89
+ }
90
+
91
+ fn expected_rows(self, rows: usize) -> usize {
92
+ (0..rows).filter(|index| self.matches(*index)).count()
93
+ }
94
+ }
95
+
96
+ #[derive(Debug, Clone, Copy)]
97
+ pub enum StorageBenchUpdateFraction {
98
+ Percent10,
99
+ Percent100,
100
+ }
101
+
102
+ impl StorageBenchUpdateFraction {
103
+ fn rows(self, total_rows: usize) -> usize {
104
+ match self {
105
+ Self::Percent10 => total_rows.div_ceil(10),
106
+ Self::Percent100 => total_rows,
107
+ }
108
+ }
109
+ }
110
+
111
+ #[derive(Debug, Clone, Copy)]
112
+ pub struct StorageBenchReport {
113
+ pub measured_rows: usize,
114
+ pub verified_rows: usize,
115
+ pub elapsed: Duration,
116
+ }
117
+
118
+ pub struct StorageApiFixture {
119
+ storage: StorageContext,
120
+ rows: usize,
121
+ }
122
+
123
+ const STORAGE_API_NAMESPACE: &str = "bench.storage_api";
124
+ const STORAGE_API_ALT_NAMESPACE: &str = "bench.storage_api.alt";
125
+
126
+ pub async fn storage_api_write_kv_batch_puts(
127
+ backend: Arc<dyn Backend + Send + Sync>,
128
+ rows: usize,
129
+ ) -> Result<StorageBenchReport, LixError> {
130
+ let storage = StorageContext::new(backend);
131
+ let mut transaction = storage.begin_write_transaction().await?;
132
+ let mut batch = KvWriteBatch::new();
133
+ for index in 0..rows {
134
+ batch.put(
135
+ STORAGE_API_NAMESPACE,
136
+ storage_api_key(index),
137
+ storage_api_value(index),
138
+ );
139
+ }
140
+ let started_at = Instant::now();
141
+ let stats = transaction.write_kv_batch(batch).await?;
142
+ transaction.commit().await?;
143
+ Ok(StorageBenchReport {
144
+ measured_rows: stats.puts,
145
+ verified_rows: rows,
146
+ elapsed: started_at.elapsed(),
147
+ })
148
+ }
149
+
150
+ pub async fn storage_api_write_kv_batch_mixed_put_delete(
151
+ backend: Arc<dyn Backend + Send + Sync>,
152
+ rows: usize,
153
+ ) -> Result<StorageBenchReport, LixError> {
154
+ let fixture = prepare_storage_api_read(backend, rows).await?;
155
+ let mut transaction = fixture.storage.begin_write_transaction().await?;
156
+ let mut batch = KvWriteBatch::new();
157
+ for index in 0..rows {
158
+ if index % 2 == 0 {
159
+ batch.put(
160
+ STORAGE_API_NAMESPACE,
161
+ storage_api_key(index),
162
+ storage_api_updated_value(index),
163
+ );
164
+ } else {
165
+ batch.delete(STORAGE_API_NAMESPACE, storage_api_key(index));
166
+ }
167
+ }
168
+ let started_at = Instant::now();
169
+ let stats = transaction.write_kv_batch(batch).await?;
170
+ transaction.commit().await?;
171
+ Ok(StorageBenchReport {
172
+ measured_rows: stats.puts + stats.deletes,
173
+ verified_rows: rows,
174
+ elapsed: started_at.elapsed(),
175
+ })
176
+ }
177
+
178
+ pub async fn storage_api_write_kv_batch_multi_namespace(
179
+ backend: Arc<dyn Backend + Send + Sync>,
180
+ rows: usize,
181
+ ) -> Result<StorageBenchReport, LixError> {
182
+ let storage = StorageContext::new(backend);
183
+ let mut transaction = storage.begin_write_transaction().await?;
184
+ let mut batch = KvWriteBatch::new();
185
+ for index in 0..rows {
186
+ let namespace = if index % 2 == 0 {
187
+ STORAGE_API_NAMESPACE
188
+ } else {
189
+ STORAGE_API_ALT_NAMESPACE
190
+ };
191
+ batch.put(namespace, storage_api_key(index), storage_api_value(index));
192
+ }
193
+ let started_at = Instant::now();
194
+ let stats = transaction.write_kv_batch(batch).await?;
195
+ transaction.commit().await?;
196
+ Ok(StorageBenchReport {
197
+ measured_rows: stats.puts,
198
+ verified_rows: rows,
199
+ elapsed: started_at.elapsed(),
200
+ })
201
+ }
202
+
203
+ pub async fn storage_api_write_kv_batch_duplicate_keys(
204
+ backend: Arc<dyn Backend + Send + Sync>,
205
+ rows: usize,
206
+ ) -> Result<StorageBenchReport, LixError> {
207
+ let storage = StorageContext::new(backend);
208
+ let mut transaction = storage.begin_write_transaction().await?;
209
+ let mut batch = KvWriteBatch::new();
210
+ for index in 0..rows {
211
+ batch.put(
212
+ STORAGE_API_NAMESPACE,
213
+ storage_api_key(index % 100),
214
+ storage_api_value(index),
215
+ );
216
+ }
217
+ let started_at = Instant::now();
218
+ let stats = transaction.write_kv_batch(batch).await?;
219
+ transaction.commit().await?;
220
+ Ok(StorageBenchReport {
221
+ measured_rows: stats.puts,
222
+ verified_rows: rows,
223
+ elapsed: started_at.elapsed(),
224
+ })
225
+ }
226
+
227
+ pub async fn storage_api_write_kv_batch_value_size(
228
+ backend: Arc<dyn Backend + Send + Sync>,
229
+ rows: usize,
230
+ value_bytes: usize,
231
+ ) -> Result<StorageBenchReport, LixError> {
232
+ let storage = StorageContext::new(backend);
233
+ let mut transaction = storage.begin_write_transaction().await?;
234
+ let mut batch = KvWriteBatch::new();
235
+ for index in 0..rows {
236
+ batch.put(
237
+ STORAGE_API_NAMESPACE,
238
+ storage_api_key(index),
239
+ storage_api_value_with_bytes(index, value_bytes),
240
+ );
241
+ }
242
+ let started_at = Instant::now();
243
+ let stats = transaction.write_kv_batch(batch).await?;
244
+ transaction.commit().await?;
245
+ Ok(StorageBenchReport {
246
+ measured_rows: stats.puts,
247
+ verified_rows: rows,
248
+ elapsed: started_at.elapsed(),
249
+ })
250
+ }
251
+
252
+ pub async fn storage_api_write_and_commit(
253
+ backend: Arc<dyn Backend + Send + Sync>,
254
+ rows: usize,
255
+ ) -> Result<StorageBenchReport, LixError> {
256
+ let storage = StorageContext::new(backend);
257
+ let started_at = Instant::now();
258
+ let mut transaction = storage.begin_write_transaction().await?;
259
+ let mut batch = KvWriteBatch::new();
260
+ for index in 0..rows {
261
+ batch.put(
262
+ STORAGE_API_NAMESPACE,
263
+ storage_api_key(index),
264
+ storage_api_value(index),
265
+ );
266
+ }
267
+ let stats = transaction.write_kv_batch(batch).await?;
268
+ transaction.commit().await?;
269
+ Ok(StorageBenchReport {
270
+ measured_rows: stats.puts,
271
+ verified_rows: rows,
272
+ elapsed: started_at.elapsed(),
273
+ })
274
+ }
275
+
276
+ pub async fn storage_api_rollback_after_write(
277
+ backend: Arc<dyn Backend + Send + Sync>,
278
+ rows: usize,
279
+ ) -> Result<StorageBenchReport, LixError> {
280
+ let storage = StorageContext::new(backend);
281
+ let started_at = Instant::now();
282
+ let mut transaction = storage.begin_write_transaction().await?;
283
+ let mut batch = KvWriteBatch::new();
284
+ for index in 0..rows {
285
+ batch.put(
286
+ STORAGE_API_NAMESPACE,
287
+ storage_api_key(index),
288
+ storage_api_value(index),
289
+ );
290
+ }
291
+ let stats = transaction.write_kv_batch(batch).await?;
292
+ transaction.rollback().await?;
293
+ Ok(StorageBenchReport {
294
+ measured_rows: stats.puts,
295
+ verified_rows: rows,
296
+ elapsed: started_at.elapsed(),
297
+ })
298
+ }
299
+
300
+ pub async fn prepare_storage_api_read(
301
+ backend: Arc<dyn Backend + Send + Sync>,
302
+ rows: usize,
303
+ ) -> Result<StorageApiFixture, LixError> {
304
+ let storage = StorageContext::new(backend);
305
+ let mut transaction = storage.begin_write_transaction().await?;
306
+ let mut batch = KvWriteBatch::new();
307
+ for index in 0..rows {
308
+ batch.put(
309
+ STORAGE_API_NAMESPACE,
310
+ storage_api_key(index),
311
+ storage_api_value(index),
312
+ );
313
+ }
314
+ transaction.write_kv_batch(batch).await?;
315
+ transaction.commit().await?;
316
+ Ok(StorageApiFixture { storage, rows })
317
+ }
318
+
319
+ pub async fn storage_api_get_values_hits_prepared(
320
+ fixture: &StorageApiFixture,
321
+ reads: usize,
322
+ ) -> Result<StorageBenchReport, LixError> {
323
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
324
+ let keys = (0..reads)
325
+ .map(|index| storage_api_key(index % fixture.rows))
326
+ .collect::<Vec<_>>();
327
+ let started_at = Instant::now();
328
+ let result = transaction
329
+ .get_values(KvGetRequest {
330
+ groups: vec![KvGetGroup {
331
+ namespace: STORAGE_API_NAMESPACE.to_string(),
332
+ keys,
333
+ }],
334
+ })
335
+ .await?;
336
+ transaction.rollback().await?;
337
+ let verified_rows = result.groups[0]
338
+ .values_iter()
339
+ .filter(|value| value.is_some())
340
+ .count();
341
+ Ok(StorageBenchReport {
342
+ measured_rows: reads,
343
+ verified_rows,
344
+ elapsed: started_at.elapsed(),
345
+ })
346
+ }
347
+
348
+ pub async fn storage_api_exists_many_prepared(
349
+ fixture: &StorageApiFixture,
350
+ reads: usize,
351
+ ) -> Result<StorageBenchReport, LixError> {
352
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
353
+ let keys = (0..reads)
354
+ .map(|index| storage_api_key(index % fixture.rows))
355
+ .collect::<Vec<_>>();
356
+ let started_at = Instant::now();
357
+ let result = transaction
358
+ .exists_many(KvGetRequest {
359
+ groups: vec![KvGetGroup {
360
+ namespace: STORAGE_API_NAMESPACE.to_string(),
361
+ keys,
362
+ }],
363
+ })
364
+ .await?;
365
+ transaction.rollback().await?;
366
+ let verified_rows = result.groups[0]
367
+ .exists
368
+ .iter()
369
+ .filter(|exists| **exists)
370
+ .count();
371
+ Ok(StorageBenchReport {
372
+ measured_rows: reads,
373
+ verified_rows,
374
+ elapsed: started_at.elapsed(),
375
+ })
376
+ }
377
+
378
+ pub async fn storage_api_get_values_misses_prepared(
379
+ fixture: &StorageApiFixture,
380
+ reads: usize,
381
+ ) -> Result<StorageBenchReport, LixError> {
382
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
383
+ let keys = (0..reads)
384
+ .map(|index| storage_api_missing_key(index))
385
+ .collect::<Vec<_>>();
386
+ let started_at = Instant::now();
387
+ let result = transaction
388
+ .get_values(KvGetRequest {
389
+ groups: vec![KvGetGroup {
390
+ namespace: STORAGE_API_NAMESPACE.to_string(),
391
+ keys,
392
+ }],
393
+ })
394
+ .await?;
395
+ transaction.rollback().await?;
396
+ let verified_rows = result.groups[0]
397
+ .values_iter()
398
+ .filter(|value| value.is_none())
399
+ .count();
400
+ Ok(StorageBenchReport {
401
+ measured_rows: reads,
402
+ verified_rows,
403
+ elapsed: started_at.elapsed(),
404
+ })
405
+ }
406
+
407
+ pub async fn storage_api_get_values_mixed_hit_miss_prepared(
408
+ fixture: &StorageApiFixture,
409
+ reads: usize,
410
+ ) -> Result<StorageBenchReport, LixError> {
411
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
412
+ let keys = (0..reads)
413
+ .map(|index| {
414
+ if index % 2 == 0 {
415
+ storage_api_key(index % fixture.rows)
416
+ } else {
417
+ storage_api_missing_key(index)
418
+ }
419
+ })
420
+ .collect::<Vec<_>>();
421
+ let started_at = Instant::now();
422
+ let result = transaction
423
+ .get_values(KvGetRequest {
424
+ groups: vec![KvGetGroup {
425
+ namespace: STORAGE_API_NAMESPACE.to_string(),
426
+ keys,
427
+ }],
428
+ })
429
+ .await?;
430
+ transaction.rollback().await?;
431
+ let verified_rows = result.groups[0]
432
+ .values_iter()
433
+ .filter(|value| value.is_some())
434
+ .count();
435
+ Ok(StorageBenchReport {
436
+ measured_rows: reads,
437
+ verified_rows,
438
+ elapsed: started_at.elapsed(),
439
+ })
440
+ }
441
+
442
+ pub async fn storage_api_get_values_multi_namespace(
443
+ backend: Arc<dyn Backend + Send + Sync>,
444
+ reads: usize,
445
+ ) -> Result<StorageBenchReport, LixError> {
446
+ let storage = StorageContext::new(backend);
447
+ let mut transaction = storage.begin_write_transaction().await?;
448
+ let mut batch = KvWriteBatch::new();
449
+ for index in 0..reads {
450
+ let namespace = if index % 2 == 0 {
451
+ STORAGE_API_NAMESPACE
452
+ } else {
453
+ STORAGE_API_ALT_NAMESPACE
454
+ };
455
+ batch.put(namespace, storage_api_key(index), storage_api_value(index));
456
+ }
457
+ transaction.write_kv_batch(batch).await?;
458
+ transaction.commit().await?;
459
+
460
+ let mut transaction = storage.begin_read_transaction().await?;
461
+ let even_keys = (0..reads)
462
+ .step_by(2)
463
+ .map(storage_api_key)
464
+ .collect::<Vec<_>>();
465
+ let odd_keys = (1..reads)
466
+ .step_by(2)
467
+ .map(storage_api_key)
468
+ .collect::<Vec<_>>();
469
+ let started_at = Instant::now();
470
+ let result = transaction
471
+ .get_values(KvGetRequest {
472
+ groups: vec![
473
+ KvGetGroup {
474
+ namespace: STORAGE_API_NAMESPACE.to_string(),
475
+ keys: even_keys,
476
+ },
477
+ KvGetGroup {
478
+ namespace: STORAGE_API_ALT_NAMESPACE.to_string(),
479
+ keys: odd_keys,
480
+ },
481
+ ],
482
+ })
483
+ .await?;
484
+ transaction.rollback().await?;
485
+ let verified_rows = result
486
+ .groups
487
+ .iter()
488
+ .map(|group| group.values_iter().filter(|value| value.is_some()).count())
489
+ .sum();
490
+ Ok(StorageBenchReport {
491
+ measured_rows: reads,
492
+ verified_rows,
493
+ elapsed: started_at.elapsed(),
494
+ })
495
+ }
496
+
497
+ pub async fn storage_api_get_values_duplicate_keys_prepared(
498
+ fixture: &StorageApiFixture,
499
+ reads: usize,
500
+ ) -> Result<StorageBenchReport, LixError> {
501
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
502
+ let keys = (0..reads)
503
+ .map(|index| storage_api_key(index % 100))
504
+ .collect::<Vec<_>>();
505
+ let started_at = Instant::now();
506
+ let result = transaction
507
+ .get_values(KvGetRequest {
508
+ groups: vec![KvGetGroup {
509
+ namespace: STORAGE_API_NAMESPACE.to_string(),
510
+ keys,
511
+ }],
512
+ })
513
+ .await?;
514
+ transaction.rollback().await?;
515
+ let verified_rows = result.groups[0]
516
+ .values_iter()
517
+ .filter(|value| value.is_some())
518
+ .count();
519
+ Ok(StorageBenchReport {
520
+ measured_rows: reads,
521
+ verified_rows,
522
+ elapsed: started_at.elapsed(),
523
+ })
524
+ }
525
+
526
+ pub async fn storage_api_scan_keys_prefix_prepared(
527
+ fixture: &StorageApiFixture,
528
+ limit: usize,
529
+ ) -> Result<StorageBenchReport, LixError> {
530
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
531
+ let started_at = Instant::now();
532
+ let result = transaction
533
+ .scan_keys(KvScanRequest {
534
+ namespace: STORAGE_API_NAMESPACE.to_string(),
535
+ range: KvScanRange::prefix(b"key/".to_vec()),
536
+ after: None,
537
+ limit,
538
+ })
539
+ .await?;
540
+ transaction.rollback().await?;
541
+ Ok(StorageBenchReport {
542
+ measured_rows: result.keys.len(),
543
+ verified_rows: limit.min(fixture.rows),
544
+ elapsed: started_at.elapsed(),
545
+ })
546
+ }
547
+
548
+ pub async fn storage_api_scan_keys_after_pages_prepared(
549
+ fixture: &StorageApiFixture,
550
+ page_size: usize,
551
+ ) -> Result<StorageBenchReport, LixError> {
552
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
553
+ let started_at = Instant::now();
554
+ let mut after = None;
555
+ let mut measured_rows = 0usize;
556
+ loop {
557
+ let result = transaction
558
+ .scan_keys(KvScanRequest {
559
+ namespace: STORAGE_API_NAMESPACE.to_string(),
560
+ range: KvScanRange::prefix(b"key/".to_vec()),
561
+ after,
562
+ limit: page_size,
563
+ })
564
+ .await?;
565
+ if result.keys.is_empty() {
566
+ break;
567
+ }
568
+ measured_rows += result.keys.len();
569
+ let Some(resume_after) = result.resume_after else {
570
+ break;
571
+ };
572
+ after = Some(resume_after);
573
+ }
574
+ transaction.rollback().await?;
575
+ Ok(StorageBenchReport {
576
+ measured_rows,
577
+ verified_rows: fixture.rows,
578
+ elapsed: started_at.elapsed(),
579
+ })
580
+ }
581
+
582
+ pub async fn storage_api_scan_keys_empty_range_prepared(
583
+ fixture: &StorageApiFixture,
584
+ ) -> Result<StorageBenchReport, LixError> {
585
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
586
+ let started_at = Instant::now();
587
+ let result = transaction
588
+ .scan_keys(KvScanRequest {
589
+ namespace: STORAGE_API_NAMESPACE.to_string(),
590
+ range: KvScanRange::prefix(b"absent/".to_vec()),
591
+ after: None,
592
+ limit: fixture.rows,
593
+ })
594
+ .await?;
595
+ transaction.rollback().await?;
596
+ Ok(StorageBenchReport {
597
+ measured_rows: result.keys.len(),
598
+ verified_rows: 0,
599
+ elapsed: started_at.elapsed(),
600
+ })
601
+ }
602
+
603
+ pub async fn prepare_storage_api_selective_scan(
604
+ backend: Arc<dyn Backend + Send + Sync>,
605
+ rows: usize,
606
+ selectivity: StorageBenchSelectivity,
607
+ ) -> Result<StorageApiFixture, LixError> {
608
+ let storage = StorageContext::new(backend);
609
+ let mut transaction = storage.begin_write_transaction().await?;
610
+ let mut batch = KvWriteBatch::new();
611
+ for index in 0..rows {
612
+ let key = if selectivity.matches(index) {
613
+ storage_api_selective_key(index)
614
+ } else {
615
+ storage_api_key(index)
616
+ };
617
+ batch.put(STORAGE_API_NAMESPACE, key, storage_api_value(index));
618
+ }
619
+ transaction.write_kv_batch(batch).await?;
620
+ transaction.commit().await?;
621
+ Ok(StorageApiFixture { storage, rows })
622
+ }
623
+
624
+ pub async fn storage_api_scan_keys_selective_prefix_prepared(
625
+ fixture: &StorageApiFixture,
626
+ selectivity: StorageBenchSelectivity,
627
+ ) -> Result<StorageBenchReport, LixError> {
628
+ let mut transaction = fixture.storage.begin_read_transaction().await?;
629
+ let started_at = Instant::now();
630
+ let result = transaction
631
+ .scan_keys(KvScanRequest {
632
+ namespace: STORAGE_API_NAMESPACE.to_string(),
633
+ range: KvScanRange::prefix(b"selective/".to_vec()),
634
+ after: None,
635
+ limit: fixture.rows,
636
+ })
637
+ .await?;
638
+ transaction.rollback().await?;
639
+ Ok(StorageBenchReport {
640
+ measured_rows: result.keys.len(),
641
+ verified_rows: selectivity.expected_rows(fixture.rows),
642
+ elapsed: started_at.elapsed(),
643
+ })
644
+ }
645
+
646
+ pub async fn storage_api_transaction_commit_empty(
647
+ backend: Arc<dyn Backend + Send + Sync>,
648
+ ) -> Result<StorageBenchReport, LixError> {
649
+ let storage = StorageContext::new(backend);
650
+ let started_at = Instant::now();
651
+ let transaction = storage.begin_write_transaction().await?;
652
+ transaction.commit().await?;
653
+ Ok(StorageBenchReport {
654
+ measured_rows: 0,
655
+ verified_rows: 0,
656
+ elapsed: started_at.elapsed(),
657
+ })
658
+ }
659
+
660
+ fn storage_api_key(index: usize) -> Vec<u8> {
661
+ format!("key/{index:08}").into_bytes()
662
+ }
663
+
664
+ fn storage_api_selective_key(index: usize) -> Vec<u8> {
665
+ format!("selective/{index:08}").into_bytes()
666
+ }
667
+
668
+ fn storage_api_missing_key(index: usize) -> Vec<u8> {
669
+ format!("missing/{index:08}").into_bytes()
670
+ }
671
+
672
+ fn storage_api_value(index: usize) -> Vec<u8> {
673
+ format!("value/{index:08}/{}", "x".repeat(64)).into_bytes()
674
+ }
675
+
676
+ fn storage_api_value_with_bytes(index: usize, value_bytes: usize) -> Vec<u8> {
677
+ let prefix = format!("value/{index:08}/");
678
+ if value_bytes <= prefix.len() {
679
+ return prefix.into_bytes();
680
+ }
681
+ let mut value = prefix.into_bytes();
682
+ value.extend(std::iter::repeat_n(b'x', value_bytes - value.len()));
683
+ value
684
+ }
685
+
686
+ fn storage_api_updated_value(index: usize) -> Vec<u8> {
687
+ format!("updated/{index:08}/{}", "y".repeat(64)).into_bytes()
688
+ }
689
+
690
+ pub struct TrackedStateWriteRootFixture {
691
+ context: TrackedStateContext,
692
+ rows: Vec<TrackedStateRow>,
693
+ }
694
+
695
+ pub struct TrackedStateReadFixture {
696
+ context: TrackedStateContext,
697
+ rows: usize,
698
+ commit_id: String,
699
+ key_pattern: StorageBenchKeyPattern,
700
+ selectivity: StorageBenchSelectivity,
701
+ }
702
+
703
+ pub struct TrackedStateUpdateFixture {
704
+ context: TrackedStateContext,
705
+ rows: Vec<TrackedStateRow>,
706
+ }
707
+
708
+ pub struct TrackedStateDiffFixture {
709
+ context: TrackedStateContext,
710
+ left_commit_id: String,
711
+ right_commit_id: String,
712
+ expected_entries: usize,
713
+ }
714
+
715
+ pub struct UntrackedStateWriteFixture {
716
+ context: UntrackedStateContext,
717
+ rows: Vec<MaterializedUntrackedStateRow>,
718
+ }
719
+
720
+ pub struct UntrackedStateReadFixture {
721
+ context: UntrackedStateContext,
722
+ rows: usize,
723
+ key_pattern: StorageBenchKeyPattern,
724
+ selectivity: StorageBenchSelectivity,
725
+ }
726
+
727
+ pub struct ChangelogAppendFixture {
728
+ context: ChangelogContext,
729
+ changes: Vec<MaterializedCanonicalChange>,
730
+ }
731
+
732
+ pub struct ChangelogReadFixture {
733
+ context: ChangelogContext,
734
+ rows: usize,
735
+ }
736
+
737
+ pub struct ChangelogCodecFixture {
738
+ changes: Vec<CanonicalChange>,
739
+ encoded_changes: Vec<Vec<u8>>,
740
+ }
741
+
742
+ pub struct BinaryCasWriteFixture {
743
+ context: BinaryCasContext,
744
+ file_ids: Vec<String>,
745
+ payloads: Vec<Vec<u8>>,
746
+ }
747
+
748
+ pub struct BinaryCasReadFixture {
749
+ context: BinaryCasContext,
750
+ rows: usize,
751
+ hashes: Vec<BlobHash>,
752
+ }
753
+
754
+ #[derive(Debug, Clone, Copy)]
755
+ pub enum JsonStorePayloadShape {
756
+ SmallRaw1k,
757
+ MediumStructured16k,
758
+ LargeStructured128k,
759
+ LargeArray128k,
760
+ }
761
+
762
+ #[derive(Debug, Clone, Copy)]
763
+ pub enum JsonStoreProjectionShape {
764
+ TopLevelTarget,
765
+ TopLevelTenProps,
766
+ NestedTarget,
767
+ ArrayItem999,
768
+ Status,
769
+ }
770
+
771
+ pub struct JsonStoreWriteFixture {
772
+ context: JsonStoreContext,
773
+ documents: Vec<Vec<u8>>,
774
+ }
775
+
776
+ pub struct JsonStoreReadFixture {
777
+ context: JsonStoreContext,
778
+ refs: Vec<JsonRef>,
779
+ paths: Vec<JsonProjectionPath>,
780
+ }
781
+
782
+ pub async fn prepare_tracked_state_write_root(
783
+ config: StorageBenchConfig,
784
+ ) -> Result<TrackedStateWriteRootFixture, LixError> {
785
+ Ok(TrackedStateWriteRootFixture {
786
+ context: TrackedStateContext::new(),
787
+ rows: tracked_rows(config, "bench-tracked-commit"),
788
+ })
789
+ }
790
+
791
+ pub async fn tracked_state_write_root_prepared(
792
+ backend: &Arc<dyn Backend + Send + Sync>,
793
+ fixture: &TrackedStateWriteRootFixture,
794
+ ) -> Result<StorageBenchReport, LixError> {
795
+ write_tracked_root(
796
+ backend,
797
+ &fixture.context,
798
+ "bench-tracked-commit",
799
+ None,
800
+ &fixture.rows,
801
+ )
802
+ .await?;
803
+ Ok(report(
804
+ fixture.rows.len(),
805
+ fixture.rows.len(),
806
+ Duration::ZERO,
807
+ ))
808
+ }
809
+
810
+ pub async fn prepare_tracked_state_read(
811
+ backend: &Arc<dyn Backend + Send + Sync>,
812
+ config: StorageBenchConfig,
813
+ ) -> Result<TrackedStateReadFixture, LixError> {
814
+ let context = TrackedStateContext::new();
815
+ let rows = tracked_rows(config, "bench-tracked-commit");
816
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
817
+ Ok(TrackedStateReadFixture {
818
+ context,
819
+ rows: config.rows,
820
+ commit_id: "bench-tracked-commit".to_string(),
821
+ key_pattern: config.key_pattern,
822
+ selectivity: config.selectivity,
823
+ })
824
+ }
825
+
826
+ pub async fn prepare_tracked_state_read_file_selective(
827
+ backend: &Arc<dyn Backend + Send + Sync>,
828
+ config: StorageBenchConfig,
829
+ ) -> Result<TrackedStateReadFixture, LixError> {
830
+ let context = TrackedStateContext::new();
831
+ let rows = tracked_rows_file_selective(config, "bench-tracked-commit");
832
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
833
+ Ok(TrackedStateReadFixture {
834
+ context,
835
+ rows: config.rows,
836
+ commit_id: "bench-tracked-commit".to_string(),
837
+ key_pattern: config.key_pattern,
838
+ selectivity: config.selectivity,
839
+ })
840
+ }
841
+
842
+ pub async fn tracked_state_read_point_hit_prepared(
843
+ backend: &Arc<dyn Backend + Send + Sync>,
844
+ fixture: &TrackedStateReadFixture,
845
+ ) -> Result<StorageBenchReport, LixError> {
846
+ let mut verified_rows = 0;
847
+ let mut reader = fixture
848
+ .context
849
+ .reader(StorageContext::new(Arc::clone(backend)));
850
+ for index in 0..fixture.rows {
851
+ if reader
852
+ .load_row_at_commit(
853
+ &fixture.commit_id,
854
+ &TrackedStateRowRequest {
855
+ schema_key: tracked_schema_key(index, StorageBenchSelectivity::Percent100),
856
+ entity_id: EntityIdentity::single(entity_id(
857
+ "tracked",
858
+ index,
859
+ fixture.key_pattern,
860
+ )),
861
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
862
+ },
863
+ )
864
+ .await?
865
+ .is_some()
866
+ {
867
+ verified_rows += 1;
868
+ }
869
+ }
870
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
871
+ }
872
+
873
+ pub async fn tracked_state_read_point_hit_constant_prepared(
874
+ backend: &Arc<dyn Backend + Send + Sync>,
875
+ fixture: &TrackedStateReadFixture,
876
+ measured_reads: usize,
877
+ ) -> Result<StorageBenchReport, LixError> {
878
+ let mut verified_rows = 0;
879
+ let mut reader = fixture
880
+ .context
881
+ .reader(StorageContext::new(Arc::clone(backend)));
882
+ for index in 0..measured_reads.min(fixture.rows) {
883
+ if reader
884
+ .load_row_at_commit(
885
+ &fixture.commit_id,
886
+ &TrackedStateRowRequest {
887
+ schema_key: tracked_schema_key(index, StorageBenchSelectivity::Percent100),
888
+ entity_id: EntityIdentity::single(entity_id(
889
+ "tracked",
890
+ index,
891
+ fixture.key_pattern,
892
+ )),
893
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
894
+ },
895
+ )
896
+ .await?
897
+ .is_some()
898
+ {
899
+ verified_rows += 1;
900
+ }
901
+ }
902
+ Ok(report(
903
+ measured_reads.min(fixture.rows),
904
+ verified_rows,
905
+ Duration::ZERO,
906
+ ))
907
+ }
908
+
909
+ pub async fn tracked_state_read_point_miss_prepared(
910
+ backend: &Arc<dyn Backend + Send + Sync>,
911
+ fixture: &TrackedStateReadFixture,
912
+ ) -> Result<StorageBenchReport, LixError> {
913
+ let mut misses = 0;
914
+ let mut reader = fixture
915
+ .context
916
+ .reader(StorageContext::new(Arc::clone(backend)));
917
+ for index in 0..fixture.rows {
918
+ if reader
919
+ .load_row_at_commit(
920
+ &fixture.commit_id,
921
+ &TrackedStateRowRequest {
922
+ schema_key: "bench_tracked_entity".to_string(),
923
+ entity_id: EntityIdentity::single(format!("missing-{index}")),
924
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
925
+ },
926
+ )
927
+ .await?
928
+ .is_none()
929
+ {
930
+ misses += 1;
931
+ }
932
+ }
933
+ Ok(report(fixture.rows, misses, Duration::ZERO))
934
+ }
935
+
936
+ pub async fn tracked_state_scan_all_prepared(
937
+ backend: &Arc<dyn Backend + Send + Sync>,
938
+ fixture: &TrackedStateReadFixture,
939
+ ) -> Result<StorageBenchReport, LixError> {
940
+ let verified_rows = scan_tracked(backend, &fixture.context, &fixture.commit_id)
941
+ .await?
942
+ .len();
943
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
944
+ }
945
+
946
+ pub async fn tracked_state_scan_keys_only_prepared(
947
+ backend: &Arc<dyn Backend + Send + Sync>,
948
+ fixture: &TrackedStateReadFixture,
949
+ ) -> Result<StorageBenchReport, LixError> {
950
+ let mut reader = fixture
951
+ .context
952
+ .reader(StorageContext::new(Arc::clone(backend)));
953
+ let verified_rows = reader
954
+ .scan_rows_at_commit(
955
+ &fixture.commit_id,
956
+ &TrackedStateScanRequest {
957
+ projection: TrackedStateProjection {
958
+ columns: vec!["entity_id".to_string()],
959
+ },
960
+ ..Default::default()
961
+ },
962
+ )
963
+ .await?
964
+ .len();
965
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
966
+ }
967
+
968
+ pub async fn tracked_state_scan_headers_only_prepared(
969
+ backend: &Arc<dyn Backend + Send + Sync>,
970
+ fixture: &TrackedStateReadFixture,
971
+ ) -> Result<StorageBenchReport, LixError> {
972
+ let mut reader = fixture
973
+ .context
974
+ .reader(StorageContext::new(Arc::clone(backend)));
975
+ let verified_rows = reader
976
+ .scan_rows_at_commit(
977
+ &fixture.commit_id,
978
+ &TrackedStateScanRequest {
979
+ projection: TrackedStateProjection {
980
+ columns: tracked_state_header_columns(),
981
+ },
982
+ ..Default::default()
983
+ },
984
+ )
985
+ .await?
986
+ .len();
987
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
988
+ }
989
+
990
+ pub async fn tracked_state_scan_full_rows_prepared(
991
+ backend: &Arc<dyn Backend + Send + Sync>,
992
+ fixture: &TrackedStateReadFixture,
993
+ ) -> Result<StorageBenchReport, LixError> {
994
+ tracked_state_scan_all_prepared(backend, fixture).await
995
+ }
996
+
997
+ pub async fn tracked_state_scan_schema_prepared(
998
+ backend: &Arc<dyn Backend + Send + Sync>,
999
+ fixture: &TrackedStateReadFixture,
1000
+ ) -> Result<StorageBenchReport, LixError> {
1001
+ let mut reader = fixture
1002
+ .context
1003
+ .reader(StorageContext::new(Arc::clone(backend)));
1004
+ let verified_rows = reader
1005
+ .scan_rows_at_commit(
1006
+ &fixture.commit_id,
1007
+ &TrackedStateScanRequest {
1008
+ filter: TrackedStateFilter {
1009
+ schema_keys: vec![tracked_schema_key(0, StorageBenchSelectivity::Percent100)],
1010
+ ..Default::default()
1011
+ },
1012
+ ..Default::default()
1013
+ },
1014
+ )
1015
+ .await?
1016
+ .len();
1017
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1018
+ }
1019
+
1020
+ pub async fn tracked_state_scan_schema_selective_prepared(
1021
+ backend: &Arc<dyn Backend + Send + Sync>,
1022
+ fixture: &TrackedStateReadFixture,
1023
+ ) -> Result<StorageBenchReport, LixError> {
1024
+ let mut reader = fixture
1025
+ .context
1026
+ .reader(StorageContext::new(Arc::clone(backend)));
1027
+ let verified_rows = reader
1028
+ .scan_rows_at_commit(
1029
+ &fixture.commit_id,
1030
+ &TrackedStateScanRequest {
1031
+ filter: TrackedStateFilter {
1032
+ schema_keys: vec![TRACKED_MATCH_SCHEMA_KEY.to_string()],
1033
+ ..Default::default()
1034
+ },
1035
+ ..Default::default()
1036
+ },
1037
+ )
1038
+ .await?
1039
+ .len();
1040
+ Ok(report(
1041
+ fixture.selectivity.expected_rows(fixture.rows),
1042
+ verified_rows,
1043
+ Duration::ZERO,
1044
+ ))
1045
+ }
1046
+
1047
+ pub async fn tracked_state_scan_file_prepared(
1048
+ backend: &Arc<dyn Backend + Send + Sync>,
1049
+ fixture: &TrackedStateReadFixture,
1050
+ ) -> Result<StorageBenchReport, LixError> {
1051
+ let mut reader = fixture
1052
+ .context
1053
+ .reader(StorageContext::new(Arc::clone(backend)));
1054
+ let verified_rows = reader
1055
+ .scan_rows_at_commit(
1056
+ &fixture.commit_id,
1057
+ &TrackedStateScanRequest {
1058
+ filter: TrackedStateFilter {
1059
+ file_ids: vec![NullableKeyFilter::Value("bench.json".to_string())],
1060
+ ..Default::default()
1061
+ },
1062
+ ..Default::default()
1063
+ },
1064
+ )
1065
+ .await?
1066
+ .len();
1067
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1068
+ }
1069
+
1070
+ pub async fn tracked_state_scan_file_selective_prepared(
1071
+ backend: &Arc<dyn Backend + Send + Sync>,
1072
+ fixture: &TrackedStateReadFixture,
1073
+ ) -> Result<StorageBenchReport, LixError> {
1074
+ let mut reader = fixture
1075
+ .context
1076
+ .reader(StorageContext::new(Arc::clone(backend)));
1077
+ let verified_rows = reader
1078
+ .scan_rows_at_commit(
1079
+ &fixture.commit_id,
1080
+ &TrackedStateScanRequest {
1081
+ filter: TrackedStateFilter {
1082
+ file_ids: vec![NullableKeyFilter::Value("bench-match.json".to_string())],
1083
+ ..Default::default()
1084
+ },
1085
+ ..Default::default()
1086
+ },
1087
+ )
1088
+ .await?
1089
+ .len();
1090
+ Ok(report(
1091
+ fixture.selectivity.expected_rows(fixture.rows),
1092
+ verified_rows,
1093
+ Duration::ZERO,
1094
+ ))
1095
+ }
1096
+
1097
+ pub async fn tracked_state_scan_file_header_selective_prepared(
1098
+ backend: &Arc<dyn Backend + Send + Sync>,
1099
+ fixture: &TrackedStateReadFixture,
1100
+ ) -> Result<StorageBenchReport, LixError> {
1101
+ let mut reader = fixture
1102
+ .context
1103
+ .reader(StorageContext::new(Arc::clone(backend)));
1104
+ let verified_rows = reader
1105
+ .scan_rows_at_commit(
1106
+ &fixture.commit_id,
1107
+ &TrackedStateScanRequest {
1108
+ filter: TrackedStateFilter {
1109
+ file_ids: vec![NullableKeyFilter::Value("bench-match.json".to_string())],
1110
+ ..Default::default()
1111
+ },
1112
+ projection: TrackedStateProjection {
1113
+ columns: tracked_state_header_columns(),
1114
+ },
1115
+ ..Default::default()
1116
+ },
1117
+ )
1118
+ .await?
1119
+ .len();
1120
+ Ok(report(
1121
+ fixture.selectivity.expected_rows(fixture.rows),
1122
+ verified_rows,
1123
+ Duration::ZERO,
1124
+ ))
1125
+ }
1126
+
1127
+ pub async fn prepare_tracked_state_update(
1128
+ backend: &Arc<dyn Backend + Send + Sync>,
1129
+ config: StorageBenchConfig,
1130
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1131
+ prepare_tracked_state_update_rows(backend, config, config.update_fraction.rows(config.rows))
1132
+ .await
1133
+ }
1134
+
1135
+ pub async fn prepare_tracked_state_update_rows(
1136
+ backend: &Arc<dyn Backend + Send + Sync>,
1137
+ config: StorageBenchConfig,
1138
+ updated_rows: usize,
1139
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1140
+ let context = TrackedStateContext::new();
1141
+ let rows = tracked_rows(config, "bench-tracked-parent");
1142
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
1143
+ let mut updated_rows = tracked_rows(
1144
+ config.with_rows(updated_rows.min(config.rows)),
1145
+ "bench-tracked-child",
1146
+ );
1147
+ for (index, row) in updated_rows.iter_mut().enumerate() {
1148
+ row.snapshot_content = Some(updated_snapshot_content(index, config.state_payload_bytes));
1149
+ }
1150
+ Ok(TrackedStateUpdateFixture {
1151
+ context,
1152
+ rows: updated_rows,
1153
+ })
1154
+ }
1155
+
1156
+ pub async fn prepare_tracked_state_partial_snapshot_update_rows(
1157
+ backend: &Arc<dyn Backend + Send + Sync>,
1158
+ config: StorageBenchConfig,
1159
+ updated_rows: usize,
1160
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1161
+ let context = TrackedStateContext::new();
1162
+ let rows = tracked_rows(config, "bench-tracked-parent");
1163
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
1164
+ let mut updated_rows = tracked_rows(
1165
+ config.with_rows(updated_rows.min(config.rows)),
1166
+ "bench-tracked-child",
1167
+ );
1168
+ for (index, row) in updated_rows.iter_mut().enumerate() {
1169
+ row.snapshot_content = Some(partial_updated_snapshot_content(
1170
+ index,
1171
+ config.state_payload_bytes,
1172
+ ));
1173
+ }
1174
+ Ok(TrackedStateUpdateFixture {
1175
+ context,
1176
+ rows: updated_rows,
1177
+ })
1178
+ }
1179
+
1180
+ pub async fn prepare_tracked_state_append_child(
1181
+ backend: &Arc<dyn Backend + Send + Sync>,
1182
+ config: StorageBenchConfig,
1183
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1184
+ prepare_tracked_state_append_child_rows(backend, config, config.rows).await
1185
+ }
1186
+
1187
+ pub async fn prepare_tracked_state_append_child_rows(
1188
+ backend: &Arc<dyn Backend + Send + Sync>,
1189
+ config: StorageBenchConfig,
1190
+ appended_rows: usize,
1191
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1192
+ let context = TrackedStateContext::new();
1193
+ let rows = tracked_rows(config, "bench-tracked-parent");
1194
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
1195
+ let mut appended_rows = tracked_rows(
1196
+ config.with_rows(appended_rows.min(config.rows)),
1197
+ "bench-tracked-child",
1198
+ );
1199
+ for (index, row) in appended_rows.iter_mut().enumerate() {
1200
+ row.entity_id = EntityIdentity::single(entity_id("tracked-new", index, config.key_pattern));
1201
+ row.change_id = format!("tracked-new-change-{index}");
1202
+ }
1203
+ Ok(TrackedStateUpdateFixture {
1204
+ context,
1205
+ rows: appended_rows,
1206
+ })
1207
+ }
1208
+
1209
+ pub async fn prepare_tracked_state_tombstone_rows(
1210
+ backend: &Arc<dyn Backend + Send + Sync>,
1211
+ config: StorageBenchConfig,
1212
+ tombstone_rows: usize,
1213
+ ) -> Result<TrackedStateUpdateFixture, LixError> {
1214
+ let context = TrackedStateContext::new();
1215
+ let rows = tracked_rows(config, "bench-tracked-parent");
1216
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
1217
+ let mut tombstones = tracked_rows(
1218
+ config.with_rows(tombstone_rows.min(config.rows)),
1219
+ "bench-tracked-child",
1220
+ );
1221
+ for row in &mut tombstones {
1222
+ row.snapshot_content = None;
1223
+ }
1224
+ Ok(TrackedStateUpdateFixture {
1225
+ context,
1226
+ rows: tombstones,
1227
+ })
1228
+ }
1229
+
1230
+ pub async fn tracked_state_update_existing_prepared(
1231
+ backend: &Arc<dyn Backend + Send + Sync>,
1232
+ fixture: &TrackedStateUpdateFixture,
1233
+ ) -> Result<StorageBenchReport, LixError> {
1234
+ write_tracked_root(
1235
+ backend,
1236
+ &fixture.context,
1237
+ "bench-tracked-child",
1238
+ Some("bench-tracked-parent"),
1239
+ &fixture.rows,
1240
+ )
1241
+ .await?;
1242
+ Ok(report(
1243
+ fixture.rows.len(),
1244
+ fixture.rows.len(),
1245
+ Duration::ZERO,
1246
+ ))
1247
+ }
1248
+
1249
+ pub async fn prepare_tracked_state_diff_update_rows(
1250
+ backend: &Arc<dyn Backend + Send + Sync>,
1251
+ config: StorageBenchConfig,
1252
+ updated_rows: usize,
1253
+ ) -> Result<TrackedStateDiffFixture, LixError> {
1254
+ let fixture = prepare_tracked_state_update_rows(backend, config, updated_rows).await?;
1255
+ tracked_state_update_existing_prepared(backend, &fixture).await?;
1256
+ Ok(TrackedStateDiffFixture {
1257
+ context: fixture.context,
1258
+ left_commit_id: "bench-tracked-parent".to_string(),
1259
+ right_commit_id: "bench-tracked-child".to_string(),
1260
+ expected_entries: fixture.rows.len(),
1261
+ })
1262
+ }
1263
+
1264
+ pub async fn prepare_tracked_state_diff_tombstone_rows(
1265
+ backend: &Arc<dyn Backend + Send + Sync>,
1266
+ config: StorageBenchConfig,
1267
+ tombstone_rows: usize,
1268
+ ) -> Result<TrackedStateDiffFixture, LixError> {
1269
+ let fixture = prepare_tracked_state_tombstone_rows(backend, config, tombstone_rows).await?;
1270
+ tracked_state_update_existing_prepared(backend, &fixture).await?;
1271
+ Ok(TrackedStateDiffFixture {
1272
+ context: fixture.context,
1273
+ left_commit_id: "bench-tracked-parent".to_string(),
1274
+ right_commit_id: "bench-tracked-child".to_string(),
1275
+ expected_entries: fixture.rows.len(),
1276
+ })
1277
+ }
1278
+
1279
+ pub async fn prepare_tracked_state_diff_equal(
1280
+ backend: &Arc<dyn Backend + Send + Sync>,
1281
+ config: StorageBenchConfig,
1282
+ ) -> Result<TrackedStateDiffFixture, LixError> {
1283
+ let context = TrackedStateContext::new();
1284
+ let rows = tracked_rows(config, "bench-tracked-parent");
1285
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
1286
+ Ok(TrackedStateDiffFixture {
1287
+ context,
1288
+ left_commit_id: "bench-tracked-parent".to_string(),
1289
+ right_commit_id: "bench-tracked-parent".to_string(),
1290
+ expected_entries: 0,
1291
+ })
1292
+ }
1293
+
1294
+ pub async fn tracked_state_diff_commits_prepared(
1295
+ backend: &Arc<dyn Backend + Send + Sync>,
1296
+ fixture: &TrackedStateDiffFixture,
1297
+ ) -> Result<StorageBenchReport, LixError> {
1298
+ let mut reader = fixture
1299
+ .context
1300
+ .reader(StorageContext::new(Arc::clone(backend)));
1301
+ let diff = reader
1302
+ .diff_commits(
1303
+ &fixture.left_commit_id,
1304
+ &fixture.right_commit_id,
1305
+ &TrackedStateDiffRequest::default(),
1306
+ )
1307
+ .await?;
1308
+ Ok(report(
1309
+ fixture.expected_entries,
1310
+ diff.entries.len(),
1311
+ Duration::ZERO,
1312
+ ))
1313
+ }
1314
+
1315
+ pub async fn prepare_untracked_state_write_rows(
1316
+ config: StorageBenchConfig,
1317
+ ) -> Result<UntrackedStateWriteFixture, LixError> {
1318
+ Ok(UntrackedStateWriteFixture {
1319
+ context: UntrackedStateContext::new(),
1320
+ rows: untracked_rows(config),
1321
+ })
1322
+ }
1323
+
1324
+ pub async fn untracked_state_write_rows_prepared(
1325
+ backend: &Arc<dyn Backend + Send + Sync>,
1326
+ fixture: &UntrackedStateWriteFixture,
1327
+ ) -> Result<StorageBenchReport, LixError> {
1328
+ write_untracked_rows(backend, &fixture.context, &fixture.rows).await?;
1329
+ let verified_rows = scan_untracked(
1330
+ backend,
1331
+ &fixture.context,
1332
+ UntrackedStateScanRequest::default(),
1333
+ )
1334
+ .await?
1335
+ .len();
1336
+ Ok(report(fixture.rows.len(), verified_rows, Duration::ZERO))
1337
+ }
1338
+
1339
+ pub async fn prepare_untracked_state_read(
1340
+ backend: &Arc<dyn Backend + Send + Sync>,
1341
+ config: StorageBenchConfig,
1342
+ ) -> Result<UntrackedStateReadFixture, LixError> {
1343
+ let context = UntrackedStateContext::new();
1344
+ let rows = untracked_rows(config);
1345
+ write_untracked_rows(backend, &context, &rows).await?;
1346
+ Ok(UntrackedStateReadFixture {
1347
+ context,
1348
+ rows: config.rows,
1349
+ key_pattern: config.key_pattern,
1350
+ selectivity: config.selectivity,
1351
+ })
1352
+ }
1353
+
1354
+ pub async fn untracked_state_read_point_hit_prepared(
1355
+ backend: &Arc<dyn Backend + Send + Sync>,
1356
+ fixture: &UntrackedStateReadFixture,
1357
+ ) -> Result<StorageBenchReport, LixError> {
1358
+ let mut verified_rows = 0;
1359
+ let mut reader = fixture
1360
+ .context
1361
+ .reader(StorageContext::new(Arc::clone(backend)));
1362
+ for index in 0..fixture.rows {
1363
+ if reader
1364
+ .load_row(&UntrackedStateRowRequest {
1365
+ schema_key: untracked_schema_key(index, StorageBenchSelectivity::Percent100),
1366
+ version_id: "bench-version".to_string(),
1367
+ entity_id: EntityIdentity::single(entity_id(
1368
+ "untracked",
1369
+ index,
1370
+ fixture.key_pattern,
1371
+ )),
1372
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
1373
+ })
1374
+ .await?
1375
+ .is_some()
1376
+ {
1377
+ verified_rows += 1;
1378
+ }
1379
+ }
1380
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1381
+ }
1382
+
1383
+ pub async fn untracked_state_read_point_hit_constant_prepared(
1384
+ backend: &Arc<dyn Backend + Send + Sync>,
1385
+ fixture: &UntrackedStateReadFixture,
1386
+ measured_reads: usize,
1387
+ ) -> Result<StorageBenchReport, LixError> {
1388
+ let mut verified_rows = 0;
1389
+ let mut reader = fixture
1390
+ .context
1391
+ .reader(StorageContext::new(Arc::clone(backend)));
1392
+ for index in 0..measured_reads.min(fixture.rows) {
1393
+ if reader
1394
+ .load_row(&UntrackedStateRowRequest {
1395
+ schema_key: untracked_schema_key(index, StorageBenchSelectivity::Percent100),
1396
+ version_id: "bench-version".to_string(),
1397
+ entity_id: EntityIdentity::single(entity_id(
1398
+ "untracked",
1399
+ index,
1400
+ fixture.key_pattern,
1401
+ )),
1402
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
1403
+ })
1404
+ .await?
1405
+ .is_some()
1406
+ {
1407
+ verified_rows += 1;
1408
+ }
1409
+ }
1410
+ Ok(report(
1411
+ measured_reads.min(fixture.rows),
1412
+ verified_rows,
1413
+ Duration::ZERO,
1414
+ ))
1415
+ }
1416
+
1417
+ pub async fn untracked_state_read_point_miss_prepared(
1418
+ backend: &Arc<dyn Backend + Send + Sync>,
1419
+ fixture: &UntrackedStateReadFixture,
1420
+ ) -> Result<StorageBenchReport, LixError> {
1421
+ let mut misses = 0;
1422
+ let mut reader = fixture
1423
+ .context
1424
+ .reader(StorageContext::new(Arc::clone(backend)));
1425
+ for index in 0..fixture.rows {
1426
+ if reader
1427
+ .load_row(&UntrackedStateRowRequest {
1428
+ schema_key: "bench_untracked_entity".to_string(),
1429
+ version_id: "bench-version".to_string(),
1430
+ entity_id: EntityIdentity::single(format!("missing-{index}")),
1431
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
1432
+ })
1433
+ .await?
1434
+ .is_none()
1435
+ {
1436
+ misses += 1;
1437
+ }
1438
+ }
1439
+ Ok(report(fixture.rows, misses, Duration::ZERO))
1440
+ }
1441
+
1442
+ pub async fn untracked_state_scan_all_prepared(
1443
+ backend: &Arc<dyn Backend + Send + Sync>,
1444
+ fixture: &UntrackedStateReadFixture,
1445
+ ) -> Result<StorageBenchReport, LixError> {
1446
+ let verified_rows = scan_untracked(
1447
+ backend,
1448
+ &fixture.context,
1449
+ UntrackedStateScanRequest::default(),
1450
+ )
1451
+ .await?
1452
+ .len();
1453
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1454
+ }
1455
+
1456
+ pub async fn untracked_state_scan_keys_only_prepared(
1457
+ backend: &Arc<dyn Backend + Send + Sync>,
1458
+ fixture: &UntrackedStateReadFixture,
1459
+ ) -> Result<StorageBenchReport, LixError> {
1460
+ let verified_rows = scan_untracked(
1461
+ backend,
1462
+ &fixture.context,
1463
+ UntrackedStateScanRequest {
1464
+ projection: UntrackedStateProjection {
1465
+ columns: vec!["entity_id".to_string()],
1466
+ },
1467
+ ..Default::default()
1468
+ },
1469
+ )
1470
+ .await?
1471
+ .len();
1472
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1473
+ }
1474
+
1475
+ pub async fn untracked_state_scan_headers_only_prepared(
1476
+ backend: &Arc<dyn Backend + Send + Sync>,
1477
+ fixture: &UntrackedStateReadFixture,
1478
+ ) -> Result<StorageBenchReport, LixError> {
1479
+ let verified_rows = scan_untracked(
1480
+ backend,
1481
+ &fixture.context,
1482
+ UntrackedStateScanRequest {
1483
+ projection: UntrackedStateProjection {
1484
+ columns: untracked_state_header_columns(),
1485
+ },
1486
+ ..Default::default()
1487
+ },
1488
+ )
1489
+ .await?
1490
+ .len();
1491
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1492
+ }
1493
+
1494
+ pub async fn untracked_state_scan_full_rows_prepared(
1495
+ backend: &Arc<dyn Backend + Send + Sync>,
1496
+ fixture: &UntrackedStateReadFixture,
1497
+ ) -> Result<StorageBenchReport, LixError> {
1498
+ untracked_state_scan_all_prepared(backend, fixture).await
1499
+ }
1500
+
1501
+ pub async fn untracked_state_scan_version_prepared(
1502
+ backend: &Arc<dyn Backend + Send + Sync>,
1503
+ fixture: &UntrackedStateReadFixture,
1504
+ ) -> Result<StorageBenchReport, LixError> {
1505
+ let verified_rows = scan_untracked(
1506
+ backend,
1507
+ &fixture.context,
1508
+ UntrackedStateScanRequest {
1509
+ filter: UntrackedStateFilter {
1510
+ version_ids: vec!["bench-version".to_string()],
1511
+ ..Default::default()
1512
+ },
1513
+ ..Default::default()
1514
+ },
1515
+ )
1516
+ .await?
1517
+ .len();
1518
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1519
+ }
1520
+
1521
+ pub async fn untracked_state_scan_schema_prepared(
1522
+ backend: &Arc<dyn Backend + Send + Sync>,
1523
+ fixture: &UntrackedStateReadFixture,
1524
+ ) -> Result<StorageBenchReport, LixError> {
1525
+ let verified_rows = scan_untracked(
1526
+ backend,
1527
+ &fixture.context,
1528
+ UntrackedStateScanRequest {
1529
+ filter: UntrackedStateFilter {
1530
+ schema_keys: vec![untracked_schema_key(0, StorageBenchSelectivity::Percent100)],
1531
+ ..Default::default()
1532
+ },
1533
+ ..Default::default()
1534
+ },
1535
+ )
1536
+ .await?
1537
+ .len();
1538
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1539
+ }
1540
+
1541
+ pub async fn untracked_state_scan_schema_selective_prepared(
1542
+ backend: &Arc<dyn Backend + Send + Sync>,
1543
+ fixture: &UntrackedStateReadFixture,
1544
+ ) -> Result<StorageBenchReport, LixError> {
1545
+ let verified_rows = scan_untracked(
1546
+ backend,
1547
+ &fixture.context,
1548
+ UntrackedStateScanRequest {
1549
+ filter: UntrackedStateFilter {
1550
+ schema_keys: vec![UNTRACKED_MATCH_SCHEMA_KEY.to_string()],
1551
+ ..Default::default()
1552
+ },
1553
+ ..Default::default()
1554
+ },
1555
+ )
1556
+ .await?
1557
+ .len();
1558
+ Ok(report(
1559
+ fixture.selectivity.expected_rows(fixture.rows),
1560
+ verified_rows,
1561
+ Duration::ZERO,
1562
+ ))
1563
+ }
1564
+
1565
+ pub async fn prepare_untracked_state_overwrite(
1566
+ backend: &Arc<dyn Backend + Send + Sync>,
1567
+ config: StorageBenchConfig,
1568
+ ) -> Result<UntrackedStateWriteFixture, LixError> {
1569
+ let context = UntrackedStateContext::new();
1570
+ let rows = untracked_rows(config);
1571
+ write_untracked_rows(backend, &context, &rows).await?;
1572
+ let mut updated_rows =
1573
+ untracked_rows(config.with_rows(config.update_fraction.rows(config.rows)));
1574
+ for (index, row) in updated_rows.iter_mut().enumerate() {
1575
+ row.snapshot_content = Some(updated_snapshot_content(index, config.state_payload_bytes));
1576
+ }
1577
+ Ok(UntrackedStateWriteFixture {
1578
+ context,
1579
+ rows: updated_rows,
1580
+ })
1581
+ }
1582
+
1583
+ pub async fn prepare_untracked_state_insert_new_keys(
1584
+ backend: &Arc<dyn Backend + Send + Sync>,
1585
+ config: StorageBenchConfig,
1586
+ ) -> Result<UntrackedStateWriteFixture, LixError> {
1587
+ let context = UntrackedStateContext::new();
1588
+ let rows = untracked_rows(config);
1589
+ write_untracked_rows(backend, &context, &rows).await?;
1590
+ let mut new_rows = untracked_rows(config);
1591
+ for (index, row) in new_rows.iter_mut().enumerate() {
1592
+ row.entity_id =
1593
+ EntityIdentity::single(entity_id("untracked-new", index, config.key_pattern));
1594
+ }
1595
+ Ok(UntrackedStateWriteFixture {
1596
+ context,
1597
+ rows: new_rows,
1598
+ })
1599
+ }
1600
+
1601
+ pub async fn untracked_state_overwrite_existing_prepared(
1602
+ backend: &Arc<dyn Backend + Send + Sync>,
1603
+ fixture: &UntrackedStateWriteFixture,
1604
+ ) -> Result<StorageBenchReport, LixError> {
1605
+ write_untracked_rows(backend, &fixture.context, &fixture.rows).await?;
1606
+ let verified_rows = scan_untracked(
1607
+ backend,
1608
+ &fixture.context,
1609
+ UntrackedStateScanRequest::default(),
1610
+ )
1611
+ .await?
1612
+ .len();
1613
+ Ok(report(fixture.rows.len(), verified_rows, Duration::ZERO))
1614
+ }
1615
+
1616
+ pub async fn prepare_changelog_append_changes(
1617
+ config: StorageBenchConfig,
1618
+ ) -> Result<ChangelogAppendFixture, LixError> {
1619
+ Ok(ChangelogAppendFixture {
1620
+ context: ChangelogContext::new(),
1621
+ changes: changelog_materialized_changes(config),
1622
+ })
1623
+ }
1624
+
1625
+ pub async fn prepare_changelog_append_tombstones(
1626
+ config: StorageBenchConfig,
1627
+ ) -> Result<ChangelogAppendFixture, LixError> {
1628
+ Ok(ChangelogAppendFixture {
1629
+ context: ChangelogContext::new(),
1630
+ changes: changelog_tombstone_changes(config),
1631
+ })
1632
+ }
1633
+
1634
+ pub async fn prepare_changelog_append_metadata(
1635
+ config: StorageBenchConfig,
1636
+ ) -> Result<ChangelogAppendFixture, LixError> {
1637
+ Ok(ChangelogAppendFixture {
1638
+ context: ChangelogContext::new(),
1639
+ changes: changelog_metadata_changes(config),
1640
+ })
1641
+ }
1642
+
1643
+ pub async fn prepare_changelog_append_shared_payload(
1644
+ config: StorageBenchConfig,
1645
+ ) -> Result<ChangelogAppendFixture, LixError> {
1646
+ Ok(ChangelogAppendFixture {
1647
+ context: ChangelogContext::new(),
1648
+ changes: changelog_shared_payload_changes(config),
1649
+ })
1650
+ }
1651
+
1652
+ pub async fn prepare_changelog_append_shared_metadata(
1653
+ config: StorageBenchConfig,
1654
+ ) -> Result<ChangelogAppendFixture, LixError> {
1655
+ Ok(ChangelogAppendFixture {
1656
+ context: ChangelogContext::new(),
1657
+ changes: changelog_shared_metadata_changes(config),
1658
+ })
1659
+ }
1660
+
1661
+ pub async fn prepare_changelog_append_shared_payload_and_metadata(
1662
+ config: StorageBenchConfig,
1663
+ ) -> Result<ChangelogAppendFixture, LixError> {
1664
+ Ok(ChangelogAppendFixture {
1665
+ context: ChangelogContext::new(),
1666
+ changes: changelog_shared_payload_and_metadata_changes(config),
1667
+ })
1668
+ }
1669
+
1670
+ pub async fn prepare_changelog_append_composite_entity_ids(
1671
+ config: StorageBenchConfig,
1672
+ ) -> Result<ChangelogAppendFixture, LixError> {
1673
+ Ok(ChangelogAppendFixture {
1674
+ context: ChangelogContext::new(),
1675
+ changes: changelog_composite_entity_id_changes(config),
1676
+ })
1677
+ }
1678
+
1679
+ pub async fn prepare_changelog_codec(
1680
+ config: StorageBenchConfig,
1681
+ ) -> Result<ChangelogCodecFixture, LixError> {
1682
+ let changes = changelog_changes(config);
1683
+ let encoded_changes = changes
1684
+ .iter()
1685
+ .map(crate::changelog::codec::encode_change)
1686
+ .collect::<Result<Vec<_>, _>>()?;
1687
+ Ok(ChangelogCodecFixture {
1688
+ changes,
1689
+ encoded_changes,
1690
+ })
1691
+ }
1692
+
1693
+ pub async fn changelog_append_changes_prepared(
1694
+ backend: &Arc<dyn Backend + Send + Sync>,
1695
+ fixture: &ChangelogAppendFixture,
1696
+ ) -> Result<StorageBenchReport, LixError> {
1697
+ append_changelog_changes(backend, &fixture.context, &fixture.changes).await?;
1698
+ let reader = fixture
1699
+ .context
1700
+ .reader(StorageContext::new(Arc::clone(backend)));
1701
+ let verified_rows = reader
1702
+ .scan_changes(&ChangelogScanRequest::default())
1703
+ .await?
1704
+ .len();
1705
+ Ok(report(fixture.changes.len(), verified_rows, Duration::ZERO))
1706
+ }
1707
+
1708
+ pub async fn prepare_changelog_read(
1709
+ backend: &Arc<dyn Backend + Send + Sync>,
1710
+ config: StorageBenchConfig,
1711
+ ) -> Result<ChangelogReadFixture, LixError> {
1712
+ let context = ChangelogContext::new();
1713
+ let changes = changelog_materialized_changes(config);
1714
+ append_changelog_changes(backend, &context, &changes).await?;
1715
+ Ok(ChangelogReadFixture {
1716
+ context,
1717
+ rows: config.rows,
1718
+ })
1719
+ }
1720
+
1721
+ pub async fn prepare_changelog_read_with_selectivity(
1722
+ backend: &Arc<dyn Backend + Send + Sync>,
1723
+ config: StorageBenchConfig,
1724
+ ) -> Result<ChangelogReadFixture, LixError> {
1725
+ let context = ChangelogContext::new();
1726
+ let changes = changelog_selective_changes(config);
1727
+ append_changelog_changes(backend, &context, &changes).await?;
1728
+ Ok(ChangelogReadFixture {
1729
+ context,
1730
+ rows: config.rows,
1731
+ })
1732
+ }
1733
+
1734
+ pub async fn prepare_changelog_read_entity_history(
1735
+ backend: &Arc<dyn Backend + Send + Sync>,
1736
+ config: StorageBenchConfig,
1737
+ ) -> Result<ChangelogReadFixture, LixError> {
1738
+ let context = ChangelogContext::new();
1739
+ let changes = changelog_entity_history_changes(config);
1740
+ append_changelog_changes(backend, &context, &changes).await?;
1741
+ Ok(ChangelogReadFixture {
1742
+ context,
1743
+ rows: config.rows,
1744
+ })
1745
+ }
1746
+
1747
+ pub async fn changelog_encode_only_prepared(
1748
+ fixture: &ChangelogCodecFixture,
1749
+ ) -> Result<StorageBenchReport, LixError> {
1750
+ let mut verified_rows = 0;
1751
+ let mut encoded_bytes = 0;
1752
+ for change in &fixture.changes {
1753
+ encoded_bytes += crate::changelog::codec::encode_change(change)?.len();
1754
+ verified_rows += 1;
1755
+ }
1756
+ Ok(report(
1757
+ fixture.changes.len(),
1758
+ verified_rows + usize::from(encoded_bytes == 0),
1759
+ Duration::ZERO,
1760
+ ))
1761
+ }
1762
+
1763
+ pub async fn changelog_decode_only_prepared(
1764
+ fixture: &ChangelogCodecFixture,
1765
+ ) -> Result<StorageBenchReport, LixError> {
1766
+ let mut verified_rows = 0;
1767
+ let mut decoded_bytes = 0;
1768
+ for bytes in &fixture.encoded_changes {
1769
+ let change = crate::changelog::codec::decode_change(bytes)?;
1770
+ decoded_bytes += change.schema_key.len();
1771
+ verified_rows += 1;
1772
+ }
1773
+ Ok(report(
1774
+ fixture.encoded_changes.len(),
1775
+ verified_rows + usize::from(decoded_bytes == 0),
1776
+ Duration::ZERO,
1777
+ ))
1778
+ }
1779
+
1780
+ pub async fn changelog_load_change_hit_prepared(
1781
+ backend: &Arc<dyn Backend + Send + Sync>,
1782
+ fixture: &ChangelogReadFixture,
1783
+ ) -> Result<StorageBenchReport, LixError> {
1784
+ let reader = fixture
1785
+ .context
1786
+ .reader(StorageContext::new(Arc::clone(backend)));
1787
+ let mut verified_rows = 0;
1788
+ for index in 0..fixture.rows {
1789
+ if reader
1790
+ .load_change(&format!("bench-change-{index}"))
1791
+ .await?
1792
+ .is_some()
1793
+ {
1794
+ verified_rows += 1;
1795
+ }
1796
+ }
1797
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1798
+ }
1799
+
1800
+ pub async fn changelog_load_change_miss_prepared(
1801
+ backend: &Arc<dyn Backend + Send + Sync>,
1802
+ fixture: &ChangelogReadFixture,
1803
+ ) -> Result<StorageBenchReport, LixError> {
1804
+ let reader = fixture
1805
+ .context
1806
+ .reader(StorageContext::new(Arc::clone(backend)));
1807
+ let mut misses = 0;
1808
+ for index in 0..fixture.rows {
1809
+ if reader
1810
+ .load_change(&format!("missing-change-{index}"))
1811
+ .await?
1812
+ .is_none()
1813
+ {
1814
+ misses += 1;
1815
+ }
1816
+ }
1817
+ Ok(report(fixture.rows, misses, Duration::ZERO))
1818
+ }
1819
+
1820
+ pub async fn changelog_scan_all_prepared(
1821
+ backend: &Arc<dyn Backend + Send + Sync>,
1822
+ fixture: &ChangelogReadFixture,
1823
+ ) -> Result<StorageBenchReport, LixError> {
1824
+ let reader = fixture
1825
+ .context
1826
+ .reader(StorageContext::new(Arc::clone(backend)));
1827
+ let verified_rows = reader
1828
+ .scan_changes(&ChangelogScanRequest::default())
1829
+ .await?
1830
+ .len();
1831
+ Ok(report(fixture.rows, verified_rows, Duration::ZERO))
1832
+ }
1833
+
1834
+ pub async fn changelog_scan_full_changes_prepared(
1835
+ backend: &Arc<dyn Backend + Send + Sync>,
1836
+ fixture: &ChangelogReadFixture,
1837
+ ) -> Result<StorageBenchReport, LixError> {
1838
+ changelog_scan_all_prepared(backend, fixture).await
1839
+ }
1840
+
1841
+ pub async fn changelog_scan_limit_100_prepared(
1842
+ backend: &Arc<dyn Backend + Send + Sync>,
1843
+ fixture: &ChangelogReadFixture,
1844
+ ) -> Result<StorageBenchReport, LixError> {
1845
+ let reader = fixture
1846
+ .context
1847
+ .reader(StorageContext::new(Arc::clone(backend)));
1848
+ let expected = fixture.rows.min(100);
1849
+ let verified_rows = reader
1850
+ .scan_changes(&ChangelogScanRequest {
1851
+ limit: Some(expected),
1852
+ })
1853
+ .await?
1854
+ .len();
1855
+ Ok(report(expected, verified_rows, Duration::ZERO))
1856
+ }
1857
+
1858
+ pub async fn changelog_scan_schema_prepared(
1859
+ backend: &Arc<dyn Backend + Send + Sync>,
1860
+ fixture: &ChangelogReadFixture,
1861
+ selectivity: StorageBenchSelectivity,
1862
+ ) -> Result<StorageBenchReport, LixError> {
1863
+ let reader = fixture
1864
+ .context
1865
+ .reader(StorageContext::new(Arc::clone(backend)));
1866
+ let changes = reader
1867
+ .scan_changes(&ChangelogScanRequest::default())
1868
+ .await?;
1869
+ let verified_rows = changes
1870
+ .iter()
1871
+ .filter(|change| change.schema_key == CHANGELOG_MATCH_SCHEMA_KEY)
1872
+ .count();
1873
+ Ok(report(
1874
+ selectivity.expected_rows(fixture.rows),
1875
+ verified_rows,
1876
+ Duration::ZERO,
1877
+ ))
1878
+ }
1879
+
1880
+ pub async fn changelog_scan_entity_history_prepared(
1881
+ backend: &Arc<dyn Backend + Send + Sync>,
1882
+ fixture: &ChangelogReadFixture,
1883
+ ) -> Result<StorageBenchReport, LixError> {
1884
+ let reader = fixture
1885
+ .context
1886
+ .reader(StorageContext::new(Arc::clone(backend)));
1887
+ let changes = reader
1888
+ .scan_changes(&ChangelogScanRequest::default())
1889
+ .await?;
1890
+ let target = EntityIdentity::single(CHANGELOG_HISTORY_ENTITY_ID);
1891
+ let verified_rows = changes
1892
+ .iter()
1893
+ .filter(|change| change.entity_id == target)
1894
+ .count();
1895
+ Ok(report(
1896
+ fixture.rows.div_ceil(10),
1897
+ verified_rows,
1898
+ Duration::ZERO,
1899
+ ))
1900
+ }
1901
+
1902
+ pub async fn prepare_binary_cas_write_blobs(
1903
+ config: StorageBenchConfig,
1904
+ ) -> Result<BinaryCasWriteFixture, LixError> {
1905
+ Ok(BinaryCasWriteFixture {
1906
+ context: BinaryCasContext::new(),
1907
+ file_ids: binary_file_ids(config.rows),
1908
+ payloads: binary_payloads(config.rows, config.blob_bytes),
1909
+ })
1910
+ }
1911
+
1912
+ pub async fn prepare_binary_cas_write_duplicate_payload(
1913
+ config: StorageBenchConfig,
1914
+ ) -> Result<BinaryCasWriteFixture, LixError> {
1915
+ let payload = binary_payload(0, config.blob_bytes);
1916
+ Ok(BinaryCasWriteFixture {
1917
+ context: BinaryCasContext::new(),
1918
+ file_ids: binary_file_ids(config.rows),
1919
+ payloads: (0..config.rows).map(|_| payload.clone()).collect(),
1920
+ })
1921
+ }
1922
+
1923
+ pub async fn prepare_binary_cas_write_half_duplicate_payload(
1924
+ config: StorageBenchConfig,
1925
+ ) -> Result<BinaryCasWriteFixture, LixError> {
1926
+ Ok(BinaryCasWriteFixture {
1927
+ context: BinaryCasContext::new(),
1928
+ file_ids: binary_file_ids(config.rows),
1929
+ payloads: binary_half_duplicate_payloads(config.rows, config.blob_bytes),
1930
+ })
1931
+ }
1932
+
1933
+ pub async fn binary_cas_write_blobs_prepared(
1934
+ backend: &Arc<dyn Backend + Send + Sync>,
1935
+ fixture: &BinaryCasWriteFixture,
1936
+ ) -> Result<StorageBenchReport, LixError> {
1937
+ let writes = binary_blob_writes(&fixture.file_ids, &fixture.payloads);
1938
+ write_binary_blob_writes(backend, &fixture.context, &writes).await?;
1939
+ let verified_rows = count_binary_cas_manifests(backend).await?;
1940
+ Ok(report(writes.len(), verified_rows, Duration::ZERO))
1941
+ }
1942
+
1943
+ pub async fn prepare_binary_cas_read(
1944
+ backend: &Arc<dyn Backend + Send + Sync>,
1945
+ config: StorageBenchConfig,
1946
+ ) -> Result<BinaryCasReadFixture, LixError> {
1947
+ let context = BinaryCasContext::new();
1948
+ let payloads = binary_payloads(config.rows, config.blob_bytes);
1949
+ let file_ids = binary_file_ids(config.rows);
1950
+ let writes = binary_blob_writes(&file_ids, &payloads);
1951
+ write_binary_blob_writes(backend, &context, &writes).await?;
1952
+ let hashes = payloads
1953
+ .iter()
1954
+ .map(|payload| BlobHash::from_content(payload))
1955
+ .collect::<Vec<_>>();
1956
+ Ok(BinaryCasReadFixture {
1957
+ context,
1958
+ rows: config.rows,
1959
+ hashes,
1960
+ })
1961
+ }
1962
+
1963
+ pub async fn binary_cas_read_blob_hit_prepared(
1964
+ backend: &Arc<dyn Backend + Send + Sync>,
1965
+ fixture: &BinaryCasReadFixture,
1966
+ ) -> Result<StorageBenchReport, LixError> {
1967
+ let mut reader = fixture
1968
+ .context
1969
+ .reader(StorageContext::new(Arc::clone(backend)));
1970
+ let verified_rows = reader
1971
+ .load_bytes_many(&fixture.hashes)
1972
+ .await?
1973
+ .into_vec()
1974
+ .into_iter()
1975
+ .filter(|row| row.is_some())
1976
+ .count();
1977
+ Ok(report(fixture.hashes.len(), verified_rows, Duration::ZERO))
1978
+ }
1979
+
1980
+ pub async fn binary_cas_read_blob_miss_prepared(
1981
+ backend: &Arc<dyn Backend + Send + Sync>,
1982
+ fixture: &BinaryCasReadFixture,
1983
+ ) -> Result<StorageBenchReport, LixError> {
1984
+ let mut misses = 0;
1985
+ let mut reader = fixture
1986
+ .context
1987
+ .reader(StorageContext::new(Arc::clone(backend)));
1988
+ for index in 0..fixture.rows {
1989
+ let missing_hash = BlobHash::from_hex(&format!("{index:064x}"))?;
1990
+ if reader
1991
+ .load_bytes_many(&[missing_hash])
1992
+ .await?
1993
+ .get(0)
1994
+ .is_none()
1995
+ {
1996
+ misses += 1;
1997
+ }
1998
+ }
1999
+ Ok(report(fixture.rows, misses, Duration::ZERO))
2000
+ }
2001
+
2002
+ pub async fn prepare_json_store_write(
2003
+ shape: JsonStorePayloadShape,
2004
+ rows: usize,
2005
+ ) -> Result<JsonStoreWriteFixture, LixError> {
2006
+ Ok(JsonStoreWriteFixture {
2007
+ context: JsonStoreContext::new(),
2008
+ documents: json_documents(shape, rows),
2009
+ })
2010
+ }
2011
+
2012
+ pub async fn prepare_json_store_write_dedupe(
2013
+ shape: JsonStorePayloadShape,
2014
+ rows: usize,
2015
+ ) -> Result<JsonStoreWriteFixture, LixError> {
2016
+ let document = json_document(shape, 0);
2017
+ Ok(JsonStoreWriteFixture {
2018
+ context: JsonStoreContext::new(),
2019
+ documents: (0..rows).map(|_| document.clone()).collect(),
2020
+ })
2021
+ }
2022
+
2023
+ pub async fn json_store_write_prepared(
2024
+ backend: &Arc<dyn Backend + Send + Sync>,
2025
+ fixture: &JsonStoreWriteFixture,
2026
+ ) -> Result<StorageBenchReport, LixError> {
2027
+ let storage = StorageContext::new(Arc::clone(backend));
2028
+ let mut transaction = storage.begin_write_transaction().await?;
2029
+ {
2030
+ let mut writes = StorageWriteSet::new();
2031
+ let mut writer = fixture.context.writer();
2032
+ for document in &fixture.documents {
2033
+ writer.stage_bytes(&mut writes, document)?;
2034
+ }
2035
+ writes.apply(&mut transaction.as_mut()).await?;
2036
+ }
2037
+ transaction.commit().await?;
2038
+ Ok(report(
2039
+ fixture.documents.len(),
2040
+ fixture.documents.len(),
2041
+ Duration::ZERO,
2042
+ ))
2043
+ }
2044
+
2045
+ pub async fn prepare_json_store_read(
2046
+ backend: &Arc<dyn Backend + Send + Sync>,
2047
+ shape: JsonStorePayloadShape,
2048
+ rows: usize,
2049
+ ) -> Result<JsonStoreReadFixture, LixError> {
2050
+ prepare_json_store_projection_read(
2051
+ backend,
2052
+ shape,
2053
+ rows,
2054
+ JsonStoreProjectionShape::TopLevelTarget,
2055
+ )
2056
+ .await
2057
+ }
2058
+
2059
+ pub async fn prepare_json_store_projection_read(
2060
+ backend: &Arc<dyn Backend + Send + Sync>,
2061
+ shape: JsonStorePayloadShape,
2062
+ rows: usize,
2063
+ projection: JsonStoreProjectionShape,
2064
+ ) -> Result<JsonStoreReadFixture, LixError> {
2065
+ let context = JsonStoreContext::new();
2066
+ let documents = json_documents(shape, rows);
2067
+ let mut refs = Vec::with_capacity(documents.len());
2068
+ let storage = StorageContext::new(Arc::clone(backend));
2069
+ let mut transaction = storage.begin_write_transaction().await?;
2070
+ {
2071
+ let mut writes = StorageWriteSet::new();
2072
+ let mut writer = context.writer();
2073
+ for document in documents {
2074
+ refs.push(writer.stage_bytes(&mut writes, &document)?);
2075
+ }
2076
+ writes.apply(&mut transaction.as_mut()).await?;
2077
+ }
2078
+ transaction.commit().await?;
2079
+ Ok(JsonStoreReadFixture {
2080
+ context,
2081
+ refs,
2082
+ paths: json_projection_paths(projection),
2083
+ })
2084
+ }
2085
+
2086
+ pub async fn json_store_read_bytes_prepared(
2087
+ backend: &Arc<dyn Backend + Send + Sync>,
2088
+ fixture: &JsonStoreReadFixture,
2089
+ ) -> Result<StorageBenchReport, LixError> {
2090
+ let mut verified_rows = 0;
2091
+ let mut reader = fixture
2092
+ .context
2093
+ .reader(StorageContext::new(Arc::clone(backend)));
2094
+ for json_ref in &fixture.refs {
2095
+ if reader.load_bytes(json_ref).await?.is_some() {
2096
+ verified_rows += 1;
2097
+ }
2098
+ }
2099
+ Ok(report(fixture.refs.len(), verified_rows, Duration::ZERO))
2100
+ }
2101
+
2102
+ pub async fn json_store_read_value_prepared(
2103
+ backend: &Arc<dyn Backend + Send + Sync>,
2104
+ fixture: &JsonStoreReadFixture,
2105
+ ) -> Result<StorageBenchReport, LixError> {
2106
+ let mut verified_rows = 0;
2107
+ let mut reader = fixture
2108
+ .context
2109
+ .reader(StorageContext::new(Arc::clone(backend)));
2110
+ for json_ref in &fixture.refs {
2111
+ if reader.load_json_value(json_ref).await?.is_some() {
2112
+ verified_rows += 1;
2113
+ }
2114
+ }
2115
+ Ok(report(fixture.refs.len(), verified_rows, Duration::ZERO))
2116
+ }
2117
+
2118
+ pub async fn json_store_read_projection_prepared(
2119
+ backend: &Arc<dyn Backend + Send + Sync>,
2120
+ fixture: &JsonStoreReadFixture,
2121
+ ) -> Result<StorageBenchReport, LixError> {
2122
+ let mut verified_rows = 0;
2123
+ let mut reader = fixture
2124
+ .context
2125
+ .reader(StorageContext::new(Arc::clone(backend)));
2126
+ for json_ref in &fixture.refs {
2127
+ if reader
2128
+ .load_json_projection(json_ref, &fixture.paths)
2129
+ .await?
2130
+ .is_some()
2131
+ {
2132
+ verified_rows += 1;
2133
+ }
2134
+ }
2135
+ Ok(report(fixture.refs.len(), verified_rows, Duration::ZERO))
2136
+ }
2137
+
2138
+ pub async fn prepare_json_store_base_update_object(
2139
+ backend: &Arc<dyn Backend + Send + Sync>,
2140
+ rows: usize,
2141
+ ) -> Result<JsonStoreReadFixture, LixError> {
2142
+ prepare_json_store_base_update(backend, JsonStorePayloadShape::LargeStructured128k, rows).await
2143
+ }
2144
+
2145
+ pub async fn prepare_json_store_base_update_array(
2146
+ backend: &Arc<dyn Backend + Send + Sync>,
2147
+ rows: usize,
2148
+ ) -> Result<JsonStoreReadFixture, LixError> {
2149
+ prepare_json_store_base_update(backend, JsonStorePayloadShape::LargeArray128k, rows).await
2150
+ }
2151
+
2152
+ async fn prepare_json_store_base_update(
2153
+ backend: &Arc<dyn Backend + Send + Sync>,
2154
+ shape: JsonStorePayloadShape,
2155
+ rows: usize,
2156
+ ) -> Result<JsonStoreReadFixture, LixError> {
2157
+ let context = JsonStoreContext::new();
2158
+ let documents = json_documents(shape, rows);
2159
+ let mut refs = Vec::with_capacity(documents.len());
2160
+ let storage = StorageContext::new(Arc::clone(backend));
2161
+ let mut transaction = storage.begin_write_transaction().await?;
2162
+ {
2163
+ let mut writes = StorageWriteSet::new();
2164
+ let mut writer = context.writer();
2165
+ for document in documents {
2166
+ refs.push(writer.stage_bytes(&mut writes, &document)?);
2167
+ }
2168
+ writes.apply(&mut transaction.as_mut()).await?;
2169
+ }
2170
+ transaction.commit().await?;
2171
+ Ok(JsonStoreReadFixture {
2172
+ context,
2173
+ refs,
2174
+ paths: json_projection_paths(JsonStoreProjectionShape::TopLevelTarget),
2175
+ })
2176
+ }
2177
+
2178
+ pub async fn json_store_write_against_base_object_prepared(
2179
+ backend: &Arc<dyn Backend + Send + Sync>,
2180
+ fixture: &JsonStoreReadFixture,
2181
+ ) -> Result<StorageBenchReport, LixError> {
2182
+ json_store_write_against_base_prepared(
2183
+ backend,
2184
+ fixture,
2185
+ JsonStorePayloadShape::LargeStructured128k,
2186
+ )
2187
+ .await
2188
+ }
2189
+
2190
+ pub async fn json_store_write_against_base_array_prepared(
2191
+ backend: &Arc<dyn Backend + Send + Sync>,
2192
+ fixture: &JsonStoreReadFixture,
2193
+ ) -> Result<StorageBenchReport, LixError> {
2194
+ json_store_write_against_base_prepared(backend, fixture, JsonStorePayloadShape::LargeArray128k)
2195
+ .await
2196
+ }
2197
+
2198
+ async fn json_store_write_against_base_prepared(
2199
+ backend: &Arc<dyn Backend + Send + Sync>,
2200
+ fixture: &JsonStoreReadFixture,
2201
+ shape: JsonStorePayloadShape,
2202
+ ) -> Result<StorageBenchReport, LixError> {
2203
+ let storage = StorageContext::new(Arc::clone(backend));
2204
+ let mut transaction = storage.begin_write_transaction().await?;
2205
+ {
2206
+ let mut writes = StorageWriteSet::new();
2207
+ let mut writer = fixture.context.writer();
2208
+ for (index, _json_ref) in fixture.refs.iter().enumerate() {
2209
+ let updated = updated_json_document(shape, index);
2210
+ writer.stage_bytes(&mut writes, &updated)?;
2211
+ }
2212
+ writes.apply(&mut transaction.as_mut()).await?;
2213
+ }
2214
+ transaction.commit().await?;
2215
+ Ok(report(
2216
+ fixture.refs.len(),
2217
+ fixture.refs.len(),
2218
+ Duration::ZERO,
2219
+ ))
2220
+ }
2221
+
2222
+ pub async fn tracked_state_write_root(
2223
+ backend: &Arc<dyn Backend + Send + Sync>,
2224
+ config: StorageBenchConfig,
2225
+ ) -> Result<StorageBenchReport, LixError> {
2226
+ let rows = tracked_rows(config, "bench-tracked-commit");
2227
+ let context = TrackedStateContext::new();
2228
+ let started = Instant::now();
2229
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2230
+ let elapsed = started.elapsed();
2231
+ let verified_rows = scan_tracked(backend, &context, "bench-tracked-commit")
2232
+ .await?
2233
+ .len();
2234
+ Ok(report(rows.len(), verified_rows, elapsed))
2235
+ }
2236
+
2237
+ pub async fn tracked_state_read_point_hit(
2238
+ backend: &Arc<dyn Backend + Send + Sync>,
2239
+ config: StorageBenchConfig,
2240
+ ) -> Result<StorageBenchReport, LixError> {
2241
+ let context = TrackedStateContext::new();
2242
+ let rows = tracked_rows(config, "bench-tracked-commit");
2243
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2244
+
2245
+ let started = Instant::now();
2246
+ let mut verified_rows = 0;
2247
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2248
+ for index in 0..config.rows {
2249
+ if reader
2250
+ .load_row_at_commit(
2251
+ "bench-tracked-commit",
2252
+ &TrackedStateRowRequest {
2253
+ schema_key: tracked_schema_key(index, StorageBenchSelectivity::Percent100),
2254
+ entity_id: EntityIdentity::single(entity_id(
2255
+ "tracked",
2256
+ index,
2257
+ config.key_pattern,
2258
+ )),
2259
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
2260
+ },
2261
+ )
2262
+ .await?
2263
+ .is_some()
2264
+ {
2265
+ verified_rows += 1;
2266
+ }
2267
+ }
2268
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2269
+ }
2270
+
2271
+ pub async fn tracked_state_read_point_miss(
2272
+ backend: &Arc<dyn Backend + Send + Sync>,
2273
+ config: StorageBenchConfig,
2274
+ ) -> Result<StorageBenchReport, LixError> {
2275
+ let context = TrackedStateContext::new();
2276
+ let rows = tracked_rows(config, "bench-tracked-commit");
2277
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2278
+
2279
+ let started = Instant::now();
2280
+ let mut misses = 0;
2281
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2282
+ for index in 0..config.rows {
2283
+ if reader
2284
+ .load_row_at_commit(
2285
+ "bench-tracked-commit",
2286
+ &TrackedStateRowRequest {
2287
+ schema_key: tracked_schema_key(index, StorageBenchSelectivity::Percent100),
2288
+ entity_id: EntityIdentity::single(format!("missing-{index}")),
2289
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
2290
+ },
2291
+ )
2292
+ .await?
2293
+ .is_none()
2294
+ {
2295
+ misses += 1;
2296
+ }
2297
+ }
2298
+ Ok(report(config.rows, misses, started.elapsed()))
2299
+ }
2300
+
2301
+ pub async fn tracked_state_scan_all(
2302
+ backend: &Arc<dyn Backend + Send + Sync>,
2303
+ config: StorageBenchConfig,
2304
+ ) -> Result<StorageBenchReport, LixError> {
2305
+ let context = TrackedStateContext::new();
2306
+ let rows = tracked_rows(config, "bench-tracked-commit");
2307
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2308
+
2309
+ let started = Instant::now();
2310
+ let verified_rows = scan_tracked(backend, &context, "bench-tracked-commit")
2311
+ .await?
2312
+ .len();
2313
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2314
+ }
2315
+
2316
+ pub async fn tracked_state_scan_schema(
2317
+ backend: &Arc<dyn Backend + Send + Sync>,
2318
+ config: StorageBenchConfig,
2319
+ ) -> Result<StorageBenchReport, LixError> {
2320
+ let context = TrackedStateContext::new();
2321
+ let rows = tracked_rows(config, "bench-tracked-commit");
2322
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2323
+
2324
+ let started = Instant::now();
2325
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2326
+ let verified_rows = reader
2327
+ .scan_rows_at_commit(
2328
+ "bench-tracked-commit",
2329
+ &TrackedStateScanRequest {
2330
+ filter: TrackedStateFilter {
2331
+ schema_keys: vec![tracked_schema_key(0, StorageBenchSelectivity::Percent100)],
2332
+ ..Default::default()
2333
+ },
2334
+ ..Default::default()
2335
+ },
2336
+ )
2337
+ .await?
2338
+ .len();
2339
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2340
+ }
2341
+
2342
+ pub async fn tracked_state_scan_file(
2343
+ backend: &Arc<dyn Backend + Send + Sync>,
2344
+ config: StorageBenchConfig,
2345
+ ) -> Result<StorageBenchReport, LixError> {
2346
+ let context = TrackedStateContext::new();
2347
+ let rows = tracked_rows(config, "bench-tracked-commit");
2348
+ write_tracked_root(backend, &context, "bench-tracked-commit", None, &rows).await?;
2349
+
2350
+ let started = Instant::now();
2351
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2352
+ let verified_rows = reader
2353
+ .scan_rows_at_commit(
2354
+ "bench-tracked-commit",
2355
+ &TrackedStateScanRequest {
2356
+ filter: TrackedStateFilter {
2357
+ file_ids: vec![NullableKeyFilter::Value("bench.json".to_string())],
2358
+ ..Default::default()
2359
+ },
2360
+ ..Default::default()
2361
+ },
2362
+ )
2363
+ .await?
2364
+ .len();
2365
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2366
+ }
2367
+
2368
+ pub async fn tracked_state_update_existing(
2369
+ backend: &Arc<dyn Backend + Send + Sync>,
2370
+ config: StorageBenchConfig,
2371
+ ) -> Result<StorageBenchReport, LixError> {
2372
+ let context = TrackedStateContext::new();
2373
+ let rows = tracked_rows(config, "bench-tracked-parent");
2374
+ write_tracked_root(backend, &context, "bench-tracked-parent", None, &rows).await?;
2375
+ let mut updated_rows = tracked_rows(config, "bench-tracked-child");
2376
+ for (index, row) in updated_rows.iter_mut().enumerate() {
2377
+ row.snapshot_content = Some(updated_snapshot_content(index, config.state_payload_bytes));
2378
+ }
2379
+
2380
+ let started = Instant::now();
2381
+ write_tracked_root(
2382
+ backend,
2383
+ &context,
2384
+ "bench-tracked-child",
2385
+ Some("bench-tracked-parent"),
2386
+ &updated_rows,
2387
+ )
2388
+ .await?;
2389
+ let elapsed = started.elapsed();
2390
+ let verified_rows = scan_tracked(backend, &context, "bench-tracked-child")
2391
+ .await?
2392
+ .len();
2393
+ Ok(report(updated_rows.len(), verified_rows, elapsed))
2394
+ }
2395
+
2396
+ pub async fn untracked_state_write_rows(
2397
+ backend: &Arc<dyn Backend + Send + Sync>,
2398
+ config: StorageBenchConfig,
2399
+ ) -> Result<StorageBenchReport, LixError> {
2400
+ let rows = untracked_rows(config);
2401
+ let context = UntrackedStateContext::new();
2402
+ let started = Instant::now();
2403
+ write_untracked_rows(backend, &context, &rows).await?;
2404
+ let elapsed = started.elapsed();
2405
+ let verified_rows = scan_untracked(backend, &context, UntrackedStateScanRequest::default())
2406
+ .await?
2407
+ .len();
2408
+ Ok(report(rows.len(), verified_rows, elapsed))
2409
+ }
2410
+
2411
+ pub async fn untracked_state_read_point_hit(
2412
+ backend: &Arc<dyn Backend + Send + Sync>,
2413
+ config: StorageBenchConfig,
2414
+ ) -> Result<StorageBenchReport, LixError> {
2415
+ let context = UntrackedStateContext::new();
2416
+ let rows = untracked_rows(config);
2417
+ write_untracked_rows(backend, &context, &rows).await?;
2418
+
2419
+ let started = Instant::now();
2420
+ let mut verified_rows = 0;
2421
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2422
+ for index in 0..config.rows {
2423
+ if reader
2424
+ .load_row(&UntrackedStateRowRequest {
2425
+ schema_key: untracked_schema_key(index, StorageBenchSelectivity::Percent100),
2426
+ version_id: "bench-version".to_string(),
2427
+ entity_id: EntityIdentity::single(entity_id(
2428
+ "untracked",
2429
+ index,
2430
+ config.key_pattern,
2431
+ )),
2432
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
2433
+ })
2434
+ .await?
2435
+ .is_some()
2436
+ {
2437
+ verified_rows += 1;
2438
+ }
2439
+ }
2440
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2441
+ }
2442
+
2443
+ pub async fn untracked_state_read_point_miss(
2444
+ backend: &Arc<dyn Backend + Send + Sync>,
2445
+ config: StorageBenchConfig,
2446
+ ) -> Result<StorageBenchReport, LixError> {
2447
+ let context = UntrackedStateContext::new();
2448
+ let rows = untracked_rows(config);
2449
+ write_untracked_rows(backend, &context, &rows).await?;
2450
+
2451
+ let started = Instant::now();
2452
+ let mut misses = 0;
2453
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2454
+ for index in 0..config.rows {
2455
+ if reader
2456
+ .load_row(&UntrackedStateRowRequest {
2457
+ schema_key: "bench_untracked_entity".to_string(),
2458
+ version_id: "bench-version".to_string(),
2459
+ entity_id: EntityIdentity::single(format!("missing-{index}")),
2460
+ file_id: NullableKeyFilter::Value("bench.json".to_string()),
2461
+ })
2462
+ .await?
2463
+ .is_none()
2464
+ {
2465
+ misses += 1;
2466
+ }
2467
+ }
2468
+ Ok(report(config.rows, misses, started.elapsed()))
2469
+ }
2470
+
2471
+ pub async fn untracked_state_scan_all(
2472
+ backend: &Arc<dyn Backend + Send + Sync>,
2473
+ config: StorageBenchConfig,
2474
+ ) -> Result<StorageBenchReport, LixError> {
2475
+ let context = UntrackedStateContext::new();
2476
+ let rows = untracked_rows(config);
2477
+ write_untracked_rows(backend, &context, &rows).await?;
2478
+
2479
+ let started = Instant::now();
2480
+ let verified_rows = scan_untracked(backend, &context, UntrackedStateScanRequest::default())
2481
+ .await?
2482
+ .len();
2483
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2484
+ }
2485
+
2486
+ pub async fn untracked_state_scan_version(
2487
+ backend: &Arc<dyn Backend + Send + Sync>,
2488
+ config: StorageBenchConfig,
2489
+ ) -> Result<StorageBenchReport, LixError> {
2490
+ let context = UntrackedStateContext::new();
2491
+ let rows = untracked_rows(config);
2492
+ write_untracked_rows(backend, &context, &rows).await?;
2493
+
2494
+ let started = Instant::now();
2495
+ let verified_rows = scan_untracked(
2496
+ backend,
2497
+ &context,
2498
+ UntrackedStateScanRequest {
2499
+ filter: UntrackedStateFilter {
2500
+ version_ids: vec!["bench-version".to_string()],
2501
+ ..Default::default()
2502
+ },
2503
+ ..Default::default()
2504
+ },
2505
+ )
2506
+ .await?
2507
+ .len();
2508
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2509
+ }
2510
+
2511
+ pub async fn untracked_state_scan_schema(
2512
+ backend: &Arc<dyn Backend + Send + Sync>,
2513
+ config: StorageBenchConfig,
2514
+ ) -> Result<StorageBenchReport, LixError> {
2515
+ let context = UntrackedStateContext::new();
2516
+ let rows = untracked_rows(config);
2517
+ write_untracked_rows(backend, &context, &rows).await?;
2518
+
2519
+ let started = Instant::now();
2520
+ let verified_rows = scan_untracked(
2521
+ backend,
2522
+ &context,
2523
+ UntrackedStateScanRequest {
2524
+ filter: UntrackedStateFilter {
2525
+ schema_keys: vec![untracked_schema_key(0, StorageBenchSelectivity::Percent100)],
2526
+ ..Default::default()
2527
+ },
2528
+ ..Default::default()
2529
+ },
2530
+ )
2531
+ .await?
2532
+ .len();
2533
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2534
+ }
2535
+
2536
+ pub async fn untracked_state_overwrite_existing(
2537
+ backend: &Arc<dyn Backend + Send + Sync>,
2538
+ config: StorageBenchConfig,
2539
+ ) -> Result<StorageBenchReport, LixError> {
2540
+ let context = UntrackedStateContext::new();
2541
+ let rows = untracked_rows(config);
2542
+ write_untracked_rows(backend, &context, &rows).await?;
2543
+ let mut updated_rows = untracked_rows(config);
2544
+ for (index, row) in updated_rows.iter_mut().enumerate() {
2545
+ row.snapshot_content = Some(updated_snapshot_content(index, config.state_payload_bytes));
2546
+ }
2547
+
2548
+ let started = Instant::now();
2549
+ write_untracked_rows(backend, &context, &updated_rows).await?;
2550
+ let elapsed = started.elapsed();
2551
+ let verified_rows = scan_untracked(backend, &context, UntrackedStateScanRequest::default())
2552
+ .await?
2553
+ .len();
2554
+ Ok(report(updated_rows.len(), verified_rows, elapsed))
2555
+ }
2556
+
2557
+ pub async fn changelog_append_changes(
2558
+ backend: &Arc<dyn Backend + Send + Sync>,
2559
+ config: StorageBenchConfig,
2560
+ ) -> Result<StorageBenchReport, LixError> {
2561
+ let changes = changelog_materialized_changes(config);
2562
+ let context = ChangelogContext::new();
2563
+ let started = Instant::now();
2564
+ append_changelog_changes(backend, &context, &changes).await?;
2565
+ let elapsed = started.elapsed();
2566
+ let reader = context.reader(StorageContext::new(Arc::clone(backend)));
2567
+ let verified_rows = reader
2568
+ .scan_changes(&ChangelogScanRequest::default())
2569
+ .await?
2570
+ .len();
2571
+ Ok(report(changes.len(), verified_rows, elapsed))
2572
+ }
2573
+
2574
+ pub async fn changelog_load_change_hit(
2575
+ backend: &Arc<dyn Backend + Send + Sync>,
2576
+ config: StorageBenchConfig,
2577
+ ) -> Result<StorageBenchReport, LixError> {
2578
+ let context = ChangelogContext::new();
2579
+ let changes = changelog_materialized_changes(config);
2580
+ append_changelog_changes(backend, &context, &changes).await?;
2581
+ let reader = context.reader(StorageContext::new(Arc::clone(backend)));
2582
+
2583
+ let started = Instant::now();
2584
+ let mut verified_rows = 0;
2585
+ for index in 0..config.rows {
2586
+ if reader
2587
+ .load_change(&format!("bench-change-{index}"))
2588
+ .await?
2589
+ .is_some()
2590
+ {
2591
+ verified_rows += 1;
2592
+ }
2593
+ }
2594
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2595
+ }
2596
+
2597
+ pub async fn changelog_load_change_miss(
2598
+ backend: &Arc<dyn Backend + Send + Sync>,
2599
+ config: StorageBenchConfig,
2600
+ ) -> Result<StorageBenchReport, LixError> {
2601
+ let context = ChangelogContext::new();
2602
+ let changes = changelog_materialized_changes(config);
2603
+ append_changelog_changes(backend, &context, &changes).await?;
2604
+ let reader = context.reader(StorageContext::new(Arc::clone(backend)));
2605
+
2606
+ let started = Instant::now();
2607
+ let mut misses = 0;
2608
+ for index in 0..config.rows {
2609
+ if reader
2610
+ .load_change(&format!("missing-change-{index}"))
2611
+ .await?
2612
+ .is_none()
2613
+ {
2614
+ misses += 1;
2615
+ }
2616
+ }
2617
+ Ok(report(config.rows, misses, started.elapsed()))
2618
+ }
2619
+
2620
+ pub async fn changelog_scan_all(
2621
+ backend: &Arc<dyn Backend + Send + Sync>,
2622
+ config: StorageBenchConfig,
2623
+ ) -> Result<StorageBenchReport, LixError> {
2624
+ let context = ChangelogContext::new();
2625
+ let changes = changelog_materialized_changes(config);
2626
+ append_changelog_changes(backend, &context, &changes).await?;
2627
+ let reader = context.reader(StorageContext::new(Arc::clone(backend)));
2628
+
2629
+ let started = Instant::now();
2630
+ let verified_rows = reader
2631
+ .scan_changes(&ChangelogScanRequest::default())
2632
+ .await?
2633
+ .len();
2634
+ Ok(report(config.rows, verified_rows, started.elapsed()))
2635
+ }
2636
+
2637
+ pub async fn changelog_scan_limit_100(
2638
+ backend: &Arc<dyn Backend + Send + Sync>,
2639
+ config: StorageBenchConfig,
2640
+ ) -> Result<StorageBenchReport, LixError> {
2641
+ let context = ChangelogContext::new();
2642
+ let changes = changelog_materialized_changes(config);
2643
+ append_changelog_changes(backend, &context, &changes).await?;
2644
+ let reader = context.reader(StorageContext::new(Arc::clone(backend)));
2645
+ let expected = config.rows.min(100);
2646
+
2647
+ let started = Instant::now();
2648
+ let verified_rows = reader
2649
+ .scan_changes(&ChangelogScanRequest {
2650
+ limit: Some(expected),
2651
+ })
2652
+ .await?
2653
+ .len();
2654
+ Ok(report(expected, verified_rows, started.elapsed()))
2655
+ }
2656
+
2657
+ pub async fn binary_cas_write_blobs(
2658
+ backend: &Arc<dyn Backend + Send + Sync>,
2659
+ config: StorageBenchConfig,
2660
+ ) -> Result<StorageBenchReport, LixError> {
2661
+ let payloads = binary_payloads(config.rows, config.blob_bytes);
2662
+ let file_ids = binary_file_ids(config.rows);
2663
+ let writes = binary_blob_writes(&file_ids, &payloads);
2664
+ let context = BinaryCasContext::new();
2665
+
2666
+ let started = Instant::now();
2667
+ write_binary_blob_writes(backend, &context, &writes).await?;
2668
+ let elapsed = started.elapsed();
2669
+ let verified_rows = count_binary_cas_manifests(backend).await?;
2670
+ Ok(report(writes.len(), verified_rows, elapsed))
2671
+ }
2672
+
2673
+ pub async fn binary_cas_read_blob_hit(
2674
+ backend: &Arc<dyn Backend + Send + Sync>,
2675
+ config: StorageBenchConfig,
2676
+ ) -> Result<StorageBenchReport, LixError> {
2677
+ let context = BinaryCasContext::new();
2678
+ let payloads = binary_payloads(config.rows, config.blob_bytes);
2679
+ let file_ids = binary_file_ids(config.rows);
2680
+ let writes = binary_blob_writes(&file_ids, &payloads);
2681
+ write_binary_blob_writes(backend, &context, &writes).await?;
2682
+ let hashes = payloads
2683
+ .iter()
2684
+ .map(|payload| BlobHash::from_content(payload))
2685
+ .collect::<Vec<_>>();
2686
+
2687
+ let started = Instant::now();
2688
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2689
+ let verified_rows = reader
2690
+ .load_bytes_many(&hashes)
2691
+ .await?
2692
+ .into_vec()
2693
+ .into_iter()
2694
+ .filter(|row| row.is_some())
2695
+ .count();
2696
+ Ok(report(hashes.len(), verified_rows, started.elapsed()))
2697
+ }
2698
+
2699
+ pub async fn binary_cas_read_blob_miss(
2700
+ backend: &Arc<dyn Backend + Send + Sync>,
2701
+ config: StorageBenchConfig,
2702
+ ) -> Result<StorageBenchReport, LixError> {
2703
+ let context = BinaryCasContext::new();
2704
+ let payloads = binary_payloads(config.rows, config.blob_bytes);
2705
+ let file_ids = binary_file_ids(config.rows);
2706
+ let writes = binary_blob_writes(&file_ids, &payloads);
2707
+ write_binary_blob_writes(backend, &context, &writes).await?;
2708
+
2709
+ let started = Instant::now();
2710
+ let mut misses = 0;
2711
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2712
+ for index in 0..config.rows {
2713
+ let missing_hash = BlobHash::from_hex(&format!("{index:064x}"))?;
2714
+ if reader
2715
+ .load_bytes_many(&[missing_hash])
2716
+ .await?
2717
+ .get(0)
2718
+ .is_none()
2719
+ {
2720
+ misses += 1;
2721
+ }
2722
+ }
2723
+ Ok(report(config.rows, misses, started.elapsed()))
2724
+ }
2725
+
2726
+ pub async fn binary_cas_write_duplicate_payload(
2727
+ backend: &Arc<dyn Backend + Send + Sync>,
2728
+ config: StorageBenchConfig,
2729
+ ) -> Result<StorageBenchReport, LixError> {
2730
+ let payload = binary_payload(0, config.blob_bytes);
2731
+ let payloads = (0..config.rows)
2732
+ .map(|_| payload.clone())
2733
+ .collect::<Vec<_>>();
2734
+ let file_ids = binary_file_ids(config.rows);
2735
+ let writes = binary_blob_writes(&file_ids, &payloads);
2736
+ let context = BinaryCasContext::new();
2737
+
2738
+ let started = Instant::now();
2739
+ write_binary_blob_writes(backend, &context, &writes).await?;
2740
+ let elapsed = started.elapsed();
2741
+ let verified_rows = count_binary_cas_manifests(backend).await?;
2742
+ Ok(report(writes.len(), verified_rows, elapsed))
2743
+ }
2744
+
2745
+ async fn write_tracked_root(
2746
+ backend: &Arc<dyn Backend + Send + Sync>,
2747
+ context: &TrackedStateContext,
2748
+ commit_id: &str,
2749
+ parent_commit_id: Option<&str>,
2750
+ rows: &[TrackedStateRow],
2751
+ ) -> Result<(), LixError> {
2752
+ let storage = StorageContext::new(Arc::clone(backend));
2753
+ let mut transaction = storage.begin_write_transaction().await?;
2754
+ {
2755
+ let mut writes = StorageWriteSet::new();
2756
+ {
2757
+ let mut json_writer = JsonStoreContext::new().writer();
2758
+ context
2759
+ .writer()
2760
+ .stage_root(
2761
+ &mut transaction.as_mut(),
2762
+ &mut writes,
2763
+ &mut json_writer,
2764
+ commit_id,
2765
+ parent_commit_id,
2766
+ rows,
2767
+ )
2768
+ .await?;
2769
+ }
2770
+ writes.apply(&mut transaction.as_mut()).await?;
2771
+ }
2772
+ transaction.commit().await
2773
+ }
2774
+
2775
+ async fn scan_tracked(
2776
+ backend: &Arc<dyn Backend + Send + Sync>,
2777
+ context: &TrackedStateContext,
2778
+ commit_id: &str,
2779
+ ) -> Result<Vec<TrackedStateRow>, LixError> {
2780
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2781
+ reader
2782
+ .scan_rows_at_commit(commit_id, &TrackedStateScanRequest::default())
2783
+ .await
2784
+ }
2785
+
2786
+ async fn write_untracked_rows(
2787
+ backend: &Arc<dyn Backend + Send + Sync>,
2788
+ context: &UntrackedStateContext,
2789
+ rows: &[MaterializedUntrackedStateRow],
2790
+ ) -> Result<(), LixError> {
2791
+ let storage = StorageContext::new(Arc::clone(backend));
2792
+ let mut transaction = storage.begin_write_transaction().await?;
2793
+ {
2794
+ let mut writes = StorageWriteSet::new();
2795
+ let canonical_rows = {
2796
+ let mut json_writer = JsonStoreContext::new().writer();
2797
+ rows.iter()
2798
+ .map(|row| canonicalize_materialized_row(&mut writes, &mut json_writer, row))
2799
+ .collect::<Result<Vec<_>, _>>()?
2800
+ };
2801
+ let mut writer = context.writer(&mut writes);
2802
+ writer.stage_rows(&canonical_rows)?;
2803
+ writes.apply(&mut transaction.as_mut()).await?;
2804
+ }
2805
+ transaction.commit().await
2806
+ }
2807
+
2808
+ async fn scan_untracked(
2809
+ backend: &Arc<dyn Backend + Send + Sync>,
2810
+ context: &UntrackedStateContext,
2811
+ request: UntrackedStateScanRequest,
2812
+ ) -> Result<Vec<MaterializedUntrackedStateRow>, LixError> {
2813
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2814
+ reader.scan_rows(&request).await
2815
+ }
2816
+
2817
+ async fn append_changelog_changes(
2818
+ backend: &Arc<dyn Backend + Send + Sync>,
2819
+ context: &ChangelogContext,
2820
+ changes: &[MaterializedCanonicalChange],
2821
+ ) -> Result<(), LixError> {
2822
+ let storage = StorageContext::new(Arc::clone(backend));
2823
+ let mut transaction = storage.begin_write_transaction().await?;
2824
+ {
2825
+ let mut writes = StorageWriteSet::new();
2826
+ let canonical_changes = {
2827
+ let mut json_writer = JsonStoreContext::new().writer();
2828
+ changes
2829
+ .iter()
2830
+ .map(|change| {
2831
+ canonicalize_materialized_change(&mut writes, &mut json_writer, change)
2832
+ })
2833
+ .collect::<Result<Vec<_>, _>>()?
2834
+ };
2835
+ let mut writer = context.writer(&mut writes);
2836
+ writer.stage_changes(&canonical_changes)?;
2837
+ writes.apply(&mut transaction.as_mut()).await?;
2838
+ }
2839
+ transaction.commit().await
2840
+ }
2841
+
2842
+ async fn write_binary_blob_writes(
2843
+ backend: &Arc<dyn Backend + Send + Sync>,
2844
+ context: &BinaryCasContext,
2845
+ writes: &[BlobWrite<'_>],
2846
+ ) -> Result<(), LixError> {
2847
+ let storage = StorageContext::new(Arc::clone(backend));
2848
+ let mut transaction = storage.begin_write_transaction().await?;
2849
+ {
2850
+ let mut writeset = StorageWriteSet::new();
2851
+ let mut writer = context.writer(&mut writeset);
2852
+ writer.stage_many(writes)?;
2853
+ writeset.apply(&mut transaction.as_mut()).await?;
2854
+ }
2855
+ transaction.commit().await
2856
+ }
2857
+
2858
+ async fn count_binary_cas_manifests(
2859
+ backend: &Arc<dyn Backend + Send + Sync>,
2860
+ ) -> Result<usize, LixError> {
2861
+ let context = BinaryCasContext::new();
2862
+ let mut reader = context.reader(StorageContext::new(Arc::clone(backend)));
2863
+ reader.count_blob_manifests().await
2864
+ }
2865
+
2866
+ fn report(measured_rows: usize, verified_rows: usize, elapsed: Duration) -> StorageBenchReport {
2867
+ StorageBenchReport {
2868
+ measured_rows,
2869
+ verified_rows,
2870
+ elapsed,
2871
+ }
2872
+ }
2873
+
2874
+ const TRACKED_MATCH_SCHEMA_KEY: &str = "bench_tracked_entity";
2875
+ const TRACKED_OTHER_SCHEMA_KEY: &str = "bench_tracked_other_entity";
2876
+ const UNTRACKED_MATCH_SCHEMA_KEY: &str = "bench_untracked_entity";
2877
+ const UNTRACKED_OTHER_SCHEMA_KEY: &str = "bench_untracked_other_entity";
2878
+ const CHANGELOG_MATCH_SCHEMA_KEY: &str = "bench_changelog_entity";
2879
+ const CHANGELOG_OTHER_SCHEMA_KEY: &str = "bench_changelog_other_entity";
2880
+ const CHANGELOG_HISTORY_ENTITY_ID: &str = "change-entity-history-target";
2881
+
2882
+ fn tracked_rows(config: StorageBenchConfig, commit_id: &str) -> Vec<TrackedStateRow> {
2883
+ (0..config.rows)
2884
+ .map(|index| TrackedStateRow {
2885
+ entity_id: EntityIdentity::single(entity_id("tracked", index, config.key_pattern)),
2886
+ schema_key: tracked_schema_key(index, config.selectivity),
2887
+ file_id: Some("bench.json".to_string()),
2888
+ snapshot_content: Some(snapshot_content(index, config.state_payload_bytes)),
2889
+ metadata: None,
2890
+ schema_version: "1".to_string(),
2891
+ created_at: timestamp(index),
2892
+ updated_at: timestamp(index),
2893
+ change_id: format!("tracked-change-{index}"),
2894
+ commit_id: commit_id.to_string(),
2895
+ })
2896
+ .collect()
2897
+ }
2898
+
2899
+ fn tracked_rows_file_selective(
2900
+ config: StorageBenchConfig,
2901
+ commit_id: &str,
2902
+ ) -> Vec<TrackedStateRow> {
2903
+ (0..config.rows)
2904
+ .map(|index| TrackedStateRow {
2905
+ entity_id: EntityIdentity::single(entity_id("tracked", index, config.key_pattern)),
2906
+ schema_key: TRACKED_MATCH_SCHEMA_KEY.to_string(),
2907
+ file_id: Some(
2908
+ if config.selectivity.matches(index) {
2909
+ "bench-match.json"
2910
+ } else {
2911
+ "bench-other.json"
2912
+ }
2913
+ .to_string(),
2914
+ ),
2915
+ snapshot_content: Some(snapshot_content(index, config.state_payload_bytes)),
2916
+ metadata: None,
2917
+ schema_version: "1".to_string(),
2918
+ created_at: timestamp(index),
2919
+ updated_at: timestamp(index),
2920
+ change_id: format!("tracked-change-{index}"),
2921
+ commit_id: commit_id.to_string(),
2922
+ })
2923
+ .collect()
2924
+ }
2925
+
2926
+ fn untracked_rows(config: StorageBenchConfig) -> Vec<MaterializedUntrackedStateRow> {
2927
+ (0..config.rows)
2928
+ .map(|index| MaterializedUntrackedStateRow {
2929
+ entity_id: EntityIdentity::single(entity_id("untracked", index, config.key_pattern)),
2930
+ schema_key: untracked_schema_key(index, config.selectivity),
2931
+ file_id: Some("bench.json".to_string()),
2932
+ snapshot_content: Some(snapshot_content(index, config.state_payload_bytes)),
2933
+ metadata: None,
2934
+ schema_version: "1".to_string(),
2935
+ created_at: timestamp(index),
2936
+ updated_at: timestamp(index),
2937
+ global: false,
2938
+ version_id: "bench-version".to_string(),
2939
+ })
2940
+ .collect()
2941
+ }
2942
+
2943
+ fn changelog_changes(config: StorageBenchConfig) -> Vec<CanonicalChange> {
2944
+ changelog_materialized_changes(config)
2945
+ .into_iter()
2946
+ .map(canonical_changelog_bench_change)
2947
+ .collect()
2948
+ }
2949
+
2950
+ fn changelog_materialized_changes(config: StorageBenchConfig) -> Vec<MaterializedCanonicalChange> {
2951
+ (0..config.rows)
2952
+ .map(|index| MaterializedCanonicalChange {
2953
+ id: format!("bench-change-{index}"),
2954
+ entity_id: EntityIdentity::single(entity_id(
2955
+ "change-entity",
2956
+ index,
2957
+ config.key_pattern,
2958
+ )),
2959
+ schema_key: "bench_changelog_entity".to_string(),
2960
+ schema_version: "1".to_string(),
2961
+ file_id: Some("bench.json".to_string()),
2962
+ snapshot_content: Some(snapshot_content(index, config.state_payload_bytes)),
2963
+ metadata: None,
2964
+ created_at: timestamp(index),
2965
+ })
2966
+ .collect()
2967
+ }
2968
+
2969
+ fn canonical_changelog_bench_change(change: MaterializedCanonicalChange) -> CanonicalChange {
2970
+ let snapshot_ref = change
2971
+ .snapshot_content
2972
+ .as_ref()
2973
+ .map(|value| JsonRef::from_hash(blake3::hash(value.as_bytes())));
2974
+ let metadata_ref = change.metadata.as_ref().map(|value| {
2975
+ let bytes =
2976
+ serde_json::to_vec(value).expect("bench metadata should serialize to JSON bytes");
2977
+ JsonRef::from_hash(blake3::hash(&bytes))
2978
+ });
2979
+ CanonicalChange {
2980
+ id: change.id,
2981
+ entity_id: change.entity_id,
2982
+ schema_key: change.schema_key,
2983
+ schema_version: change.schema_version,
2984
+ file_id: change.file_id,
2985
+ snapshot_ref,
2986
+ metadata_ref,
2987
+ created_at: change.created_at,
2988
+ }
2989
+ }
2990
+
2991
+ fn changelog_tombstone_changes(config: StorageBenchConfig) -> Vec<MaterializedCanonicalChange> {
2992
+ changelog_materialized_changes(config)
2993
+ .into_iter()
2994
+ .map(|mut change| {
2995
+ change.snapshot_content = None;
2996
+ change.metadata = None;
2997
+ change
2998
+ })
2999
+ .collect()
3000
+ }
3001
+
3002
+ fn changelog_metadata_changes(config: StorageBenchConfig) -> Vec<MaterializedCanonicalChange> {
3003
+ changelog_materialized_changes(config)
3004
+ .into_iter()
3005
+ .enumerate()
3006
+ .map(|(index, mut change)| {
3007
+ change.metadata = Some(snapshot_metadata(index, config.state_payload_bytes));
3008
+ change
3009
+ })
3010
+ .collect()
3011
+ }
3012
+
3013
+ fn changelog_shared_payload_changes(
3014
+ config: StorageBenchConfig,
3015
+ ) -> Vec<MaterializedCanonicalChange> {
3016
+ let shared_snapshot_content = snapshot_content(0, config.state_payload_bytes);
3017
+ changelog_materialized_changes(config)
3018
+ .into_iter()
3019
+ .map(|mut change| {
3020
+ change.snapshot_content = Some(shared_snapshot_content.clone());
3021
+ change
3022
+ })
3023
+ .collect()
3024
+ }
3025
+
3026
+ fn changelog_shared_metadata_changes(
3027
+ config: StorageBenchConfig,
3028
+ ) -> Vec<MaterializedCanonicalChange> {
3029
+ let shared_metadata = snapshot_metadata(0, config.state_payload_bytes);
3030
+ changelog_materialized_changes(config)
3031
+ .into_iter()
3032
+ .map(|mut change| {
3033
+ change.snapshot_content = None;
3034
+ change.metadata = Some(shared_metadata.clone());
3035
+ change
3036
+ })
3037
+ .collect()
3038
+ }
3039
+
3040
+ fn changelog_shared_payload_and_metadata_changes(
3041
+ config: StorageBenchConfig,
3042
+ ) -> Vec<MaterializedCanonicalChange> {
3043
+ let shared_snapshot_content = snapshot_content(0, config.state_payload_bytes);
3044
+ let shared_metadata = snapshot_metadata(1, config.state_payload_bytes);
3045
+ changelog_materialized_changes(config)
3046
+ .into_iter()
3047
+ .map(|mut change| {
3048
+ change.snapshot_content = Some(shared_snapshot_content.clone());
3049
+ change.metadata = Some(shared_metadata.clone());
3050
+ change
3051
+ })
3052
+ .collect()
3053
+ }
3054
+
3055
+ fn changelog_composite_entity_id_changes(
3056
+ config: StorageBenchConfig,
3057
+ ) -> Vec<MaterializedCanonicalChange> {
3058
+ changelog_materialized_changes(config)
3059
+ .into_iter()
3060
+ .enumerate()
3061
+ .map(|(index, mut change)| {
3062
+ change.entity_id = EntityIdentity {
3063
+ parts: vec![
3064
+ EntityIdentityPart::String(entity_id(
3065
+ "change-composite",
3066
+ index,
3067
+ config.key_pattern,
3068
+ )),
3069
+ EntityIdentityPart::Number(index.to_string()),
3070
+ EntityIdentityPart::Bool(index % 2 == 0),
3071
+ ],
3072
+ };
3073
+ change
3074
+ })
3075
+ .collect()
3076
+ }
3077
+
3078
+ fn changelog_selective_changes(config: StorageBenchConfig) -> Vec<MaterializedCanonicalChange> {
3079
+ changelog_materialized_changes(config)
3080
+ .into_iter()
3081
+ .enumerate()
3082
+ .map(|(index, mut change)| {
3083
+ change.schema_key = changelog_schema_key(index, config.selectivity);
3084
+ change
3085
+ })
3086
+ .collect()
3087
+ }
3088
+
3089
+ fn changelog_entity_history_changes(
3090
+ config: StorageBenchConfig,
3091
+ ) -> Vec<MaterializedCanonicalChange> {
3092
+ changelog_materialized_changes(config)
3093
+ .into_iter()
3094
+ .enumerate()
3095
+ .map(|(index, mut change)| {
3096
+ if index % 10 == 0 {
3097
+ change.entity_id = EntityIdentity::single(CHANGELOG_HISTORY_ENTITY_ID);
3098
+ }
3099
+ change
3100
+ })
3101
+ .collect()
3102
+ }
3103
+
3104
+ fn tracked_schema_key(index: usize, selectivity: StorageBenchSelectivity) -> String {
3105
+ if selectivity.matches(index) {
3106
+ TRACKED_MATCH_SCHEMA_KEY
3107
+ } else {
3108
+ TRACKED_OTHER_SCHEMA_KEY
3109
+ }
3110
+ .to_string()
3111
+ }
3112
+
3113
+ fn untracked_schema_key(index: usize, selectivity: StorageBenchSelectivity) -> String {
3114
+ if selectivity.matches(index) {
3115
+ UNTRACKED_MATCH_SCHEMA_KEY
3116
+ } else {
3117
+ UNTRACKED_OTHER_SCHEMA_KEY
3118
+ }
3119
+ .to_string()
3120
+ }
3121
+
3122
+ fn changelog_schema_key(index: usize, selectivity: StorageBenchSelectivity) -> String {
3123
+ if selectivity.matches(index) {
3124
+ CHANGELOG_MATCH_SCHEMA_KEY
3125
+ } else {
3126
+ CHANGELOG_OTHER_SCHEMA_KEY
3127
+ }
3128
+ .to_string()
3129
+ }
3130
+
3131
+ fn entity_id(prefix: &str, index: usize, key_pattern: StorageBenchKeyPattern) -> String {
3132
+ match key_pattern {
3133
+ StorageBenchKeyPattern::Sequential => format!("{prefix}-{index}"),
3134
+ StorageBenchKeyPattern::Random => format!("{prefix}-{:016x}", randomish_index(index)),
3135
+ }
3136
+ }
3137
+
3138
+ fn randomish_index(index: usize) -> u64 {
3139
+ let mut value = index as u64;
3140
+ value ^= value >> 30;
3141
+ value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
3142
+ value ^= value >> 27;
3143
+ value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
3144
+ value ^ (value >> 31)
3145
+ }
3146
+
3147
+ fn binary_file_ids(rows: usize) -> Vec<String> {
3148
+ (0..rows)
3149
+ .map(|index| format!("bench-file-{index}"))
3150
+ .collect()
3151
+ }
3152
+
3153
+ fn binary_payloads(rows: usize, blob_bytes: usize) -> Vec<Vec<u8>> {
3154
+ (0..rows)
3155
+ .map(|index| binary_payload(index, blob_bytes))
3156
+ .collect()
3157
+ }
3158
+
3159
+ fn binary_half_duplicate_payloads(rows: usize, blob_bytes: usize) -> Vec<Vec<u8>> {
3160
+ (0..rows)
3161
+ .map(|index| {
3162
+ if index % 2 == 0 {
3163
+ binary_payload(0, blob_bytes)
3164
+ } else {
3165
+ binary_payload(index, blob_bytes)
3166
+ }
3167
+ })
3168
+ .collect()
3169
+ }
3170
+
3171
+ fn binary_blob_writes<'a>(_file_ids: &'a [String], payloads: &'a [Vec<u8>]) -> Vec<BlobWrite<'a>> {
3172
+ payloads
3173
+ .iter()
3174
+ .map(|payload| BlobWrite {
3175
+ bytes: payload.as_slice(),
3176
+ })
3177
+ .collect()
3178
+ }
3179
+
3180
+ fn snapshot_content(index: usize, target_bytes: usize) -> String {
3181
+ let mut value = serde_json::json!({
3182
+ "id": format!("entity-{index}"),
3183
+ "value": format!("value-{index}"),
3184
+ "index": index
3185
+ });
3186
+ pad_snapshot_content(&mut value, target_bytes);
3187
+ value.to_string()
3188
+ }
3189
+
3190
+ fn snapshot_metadata(index: usize, target_bytes: usize) -> serde_json::Value {
3191
+ serde_json::from_str(&snapshot_content(index, target_bytes))
3192
+ .expect("bench snapshot content should be valid JSON metadata")
3193
+ }
3194
+
3195
+ fn tracked_state_header_columns() -> Vec<String> {
3196
+ [
3197
+ "entity_id",
3198
+ "schema_key",
3199
+ "schema_version",
3200
+ "file_id",
3201
+ "metadata",
3202
+ "created_at",
3203
+ "updated_at",
3204
+ "change_id",
3205
+ "commit_id",
3206
+ ]
3207
+ .into_iter()
3208
+ .map(str::to_string)
3209
+ .collect()
3210
+ }
3211
+
3212
+ fn untracked_state_header_columns() -> Vec<String> {
3213
+ [
3214
+ "entity_id",
3215
+ "schema_key",
3216
+ "schema_version",
3217
+ "file_id",
3218
+ "metadata",
3219
+ "created_at",
3220
+ "updated_at",
3221
+ "global",
3222
+ "version_id",
3223
+ ]
3224
+ .into_iter()
3225
+ .map(str::to_string)
3226
+ .collect()
3227
+ }
3228
+
3229
+ fn updated_snapshot_content(index: usize, target_bytes: usize) -> String {
3230
+ let mut value = serde_json::json!({
3231
+ "id": format!("entity-{index}"),
3232
+ "value": format!("updated-{index}"),
3233
+ "index": index
3234
+ });
3235
+ pad_snapshot_content(&mut value, target_bytes);
3236
+ value.to_string()
3237
+ }
3238
+
3239
+ fn partial_updated_snapshot_content(index: usize, target_bytes: usize) -> String {
3240
+ let mut value = serde_json::json!({
3241
+ "id": format!("entity-{index}"),
3242
+ "value": format!("value-{index}"),
3243
+ "index": index,
3244
+ "done": true
3245
+ });
3246
+ pad_snapshot_content(&mut value, target_bytes);
3247
+ value.to_string()
3248
+ }
3249
+
3250
+ fn pad_snapshot_content(value: &mut serde_json::Value, target_bytes: usize) {
3251
+ let current = value.to_string().len();
3252
+ if target_bytes <= current {
3253
+ return;
3254
+ }
3255
+ value["padding"] = serde_json::Value::String("x".repeat(target_bytes - current));
3256
+ }
3257
+
3258
+ fn timestamp(index: usize) -> String {
3259
+ format!(
3260
+ "2026-05-01T00:{:02}:{:02}.000Z",
3261
+ (index / 60) % 60,
3262
+ index % 60
3263
+ )
3264
+ }
3265
+
3266
+ fn binary_payload(index: usize, len: usize) -> Vec<u8> {
3267
+ let mut payload = (0..len)
3268
+ .map(|offset| {
3269
+ ((index as u64)
3270
+ .wrapping_mul(31)
3271
+ .wrapping_add((offset as u64).wrapping_mul(17))
3272
+ & 0xff) as u8
3273
+ })
3274
+ .collect::<Vec<_>>();
3275
+ for (offset, byte) in (index as u64).to_le_bytes().into_iter().enumerate() {
3276
+ if offset < payload.len() {
3277
+ payload[offset] = byte;
3278
+ }
3279
+ }
3280
+ payload
3281
+ }
3282
+
3283
+ fn json_documents(shape: JsonStorePayloadShape, rows: usize) -> Vec<Vec<u8>> {
3284
+ (0..rows).map(|index| json_document(shape, index)).collect()
3285
+ }
3286
+
3287
+ fn json_document(shape: JsonStorePayloadShape, index: usize) -> Vec<u8> {
3288
+ match shape {
3289
+ JsonStorePayloadShape::SmallRaw1k => json_object_document(index, 1_024, 8),
3290
+ JsonStorePayloadShape::MediumStructured16k => json_object_document(index, 16 * 1024, 128),
3291
+ JsonStorePayloadShape::LargeStructured128k => {
3292
+ json_object_document(index, 128 * 1024, 1_000)
3293
+ }
3294
+ JsonStorePayloadShape::LargeArray128k => json_array_document(index, 128 * 1024, 1_000),
3295
+ }
3296
+ }
3297
+
3298
+ fn updated_json_document(shape: JsonStorePayloadShape, index: usize) -> Vec<u8> {
3299
+ let bytes = json_document(shape, index);
3300
+ let mut value: serde_json::Value =
3301
+ serde_json::from_slice(&bytes).expect("storage bench JSON document should parse");
3302
+ match shape {
3303
+ JsonStorePayloadShape::LargeArray128k => {
3304
+ value["items"][999]["value"] =
3305
+ serde_json::Value::String(format!("updated-array-value-{index}"));
3306
+ }
3307
+ JsonStorePayloadShape::SmallRaw1k
3308
+ | JsonStorePayloadShape::MediumStructured16k
3309
+ | JsonStorePayloadShape::LargeStructured128k => {
3310
+ value["field_999"] = serde_json::Value::String(format!("updated-object-value-{index}"));
3311
+ }
3312
+ }
3313
+ serde_json::to_vec(&value).expect("storage bench updated JSON should serialize")
3314
+ }
3315
+
3316
+ fn json_object_document(index: usize, target_bytes: usize, fields: usize) -> Vec<u8> {
3317
+ let mut object = serde_json::Map::new();
3318
+ object.insert(
3319
+ "id".to_string(),
3320
+ serde_json::Value::String(format!("json-{index}")),
3321
+ );
3322
+ object.insert(
3323
+ "target".to_string(),
3324
+ serde_json::Value::String(format!("target-{index}")),
3325
+ );
3326
+ object.insert(
3327
+ "status".to_string(),
3328
+ serde_json::Value::String(if index % 2 == 0 { "open" } else { "closed" }.to_string()),
3329
+ );
3330
+ object.insert(
3331
+ "nested".to_string(),
3332
+ serde_json::json!({
3333
+ "target": format!("nested-target-{index}"),
3334
+ "revision": index,
3335
+ }),
3336
+ );
3337
+ for field_index in 0..fields {
3338
+ object.insert(
3339
+ format!("field_{field_index}"),
3340
+ serde_json::Value::String(format!("value-{index}-{field_index}")),
3341
+ );
3342
+ }
3343
+ pad_json_object(&mut object, target_bytes);
3344
+ serde_json::to_vec(&serde_json::Value::Object(object))
3345
+ .expect("storage bench object JSON should serialize")
3346
+ }
3347
+
3348
+ fn json_array_document(index: usize, target_bytes: usize, items: usize) -> Vec<u8> {
3349
+ let mut object = serde_json::Map::new();
3350
+ object.insert(
3351
+ "id".to_string(),
3352
+ serde_json::Value::String(format!("json-array-{index}")),
3353
+ );
3354
+ object.insert(
3355
+ "target".to_string(),
3356
+ serde_json::Value::String(format!("target-{index}")),
3357
+ );
3358
+ object.insert(
3359
+ "status".to_string(),
3360
+ serde_json::Value::String(if index % 2 == 0 { "open" } else { "closed" }.to_string()),
3361
+ );
3362
+ object.insert(
3363
+ "items".to_string(),
3364
+ serde_json::Value::Array(
3365
+ (0..items)
3366
+ .map(|item_index| {
3367
+ serde_json::json!({
3368
+ "index": item_index,
3369
+ "status": if item_index % 2 == 0 { "ready" } else { "blocked" },
3370
+ "value": format!("item-{index}-{item_index}"),
3371
+ })
3372
+ })
3373
+ .collect(),
3374
+ ),
3375
+ );
3376
+ pad_json_object(&mut object, target_bytes);
3377
+ serde_json::to_vec(&serde_json::Value::Object(object))
3378
+ .expect("storage bench array JSON should serialize")
3379
+ }
3380
+
3381
+ fn pad_json_object(object: &mut serde_json::Map<String, serde_json::Value>, target_bytes: usize) {
3382
+ let current = serde_json::to_vec(&serde_json::Value::Object(object.clone()))
3383
+ .expect("storage bench JSON should serialize")
3384
+ .len();
3385
+ if target_bytes <= current {
3386
+ return;
3387
+ }
3388
+ object.insert(
3389
+ "padding".to_string(),
3390
+ serde_json::Value::String("x".repeat(target_bytes - current)),
3391
+ );
3392
+ }
3393
+
3394
+ fn json_projection_paths(projection: JsonStoreProjectionShape) -> Vec<JsonProjectionPath> {
3395
+ match projection {
3396
+ JsonStoreProjectionShape::TopLevelTarget => vec![JsonProjectionPath::new("/target")],
3397
+ JsonStoreProjectionShape::TopLevelTenProps => (0..10)
3398
+ .map(|index| JsonProjectionPath::new(format!("/field_{index}")))
3399
+ .collect(),
3400
+ JsonStoreProjectionShape::NestedTarget => vec![JsonProjectionPath::new("/nested/target")],
3401
+ JsonStoreProjectionShape::ArrayItem999 => {
3402
+ vec![JsonProjectionPath::new("/items/999/value")]
3403
+ }
3404
+ JsonStoreProjectionShape::Status => vec![JsonProjectionPath::new("/status")],
3405
+ }
3406
+ }