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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -1,17 +1,18 @@
1
1
  use crate::json_store::compression::{compress_json_payload, decode_json_zstd_payload};
2
2
  use crate::json_store::encoded::{EncodedJson, JsonCodec};
3
3
  use crate::json_store::types::{JsonReadScopeRef, JsonRef};
4
- use crate::storage::{KvGetGroup, KvGetRequest, StorageReader};
4
+ use crate::storage::{PointReadPlan, StorageRead, StorageSpace};
5
+ use crate::storage::{StorageGetOptions, StorageKey, StorageProjectedValue, StorageSpaceId};
5
6
  use crate::LixError;
7
+ use bytes::Bytes;
6
8
  use std::borrow::Cow;
7
9
  use std::collections::HashMap;
8
10
 
9
11
  pub(crate) const JSON_NAMESPACE: &str = "json_store.json";
10
- pub(crate) const JSON_PACK_NAMESPACE: &str = "json_store.pack";
12
+ pub(crate) const JSON_SPACE: StorageSpace =
13
+ StorageSpace::new(StorageSpaceId(0x0002_0001), JSON_NAMESPACE);
11
14
  const STORED_JSON_MAGIC: &[u8] = b"lix-json:v1";
12
15
  const STORED_JSON_HEADER_LEN: usize = STORED_JSON_MAGIC.len() + 1 + 8;
13
- const STORED_JSON_PACK_MAGIC: &[u8] = b"lix-json-pack:v2";
14
- const STORED_JSON_PACK_ENTRY_HEADER_LEN: usize = 32 + 1 + 4 + 4 + 4;
15
16
  const ZSTD_MIN_JSON_BYTES: usize = 16 * 1024;
16
17
  const MIN_ZSTD_SAVINGS_BYTES: usize = 128;
17
18
 
@@ -21,17 +22,6 @@ struct StoredJsonPayload<'a> {
21
22
  data: &'a [u8],
22
23
  }
23
24
 
24
- struct JsonPackLayout {
25
- directory_start: usize,
26
- payload_start: usize,
27
- count: usize,
28
- }
29
-
30
- struct JsonPackEntry<'a> {
31
- hash: [u8; 32],
32
- payload: StoredJsonPayload<'a>,
33
- }
34
-
35
25
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36
26
  enum JsonHashCheck {
37
27
  /// Hot reads trust the local storage layer and pack directory. Content
@@ -41,12 +31,6 @@ enum JsonHashCheck {
41
31
  Verify,
42
32
  }
43
33
 
44
- enum OrderedSinglePackProbe {
45
- Hit(Vec<Option<Vec<u8>>>),
46
- MissPresent(Vec<u8>),
47
- MissAbsent,
48
- }
49
-
50
34
  fn raw_json_ref_for_content(json: &str) -> JsonRef {
51
35
  JsonRef::from_hash(blake3::hash(json.as_bytes()))
52
36
  }
@@ -107,76 +91,6 @@ pub(crate) fn encode_direct_json_payload(encoded_json: &EncodedJson<'_>) -> Vec<
107
91
  encode_stored_json_payload(encoded_json)
108
92
  }
109
93
 
110
- pub(crate) fn pack_key(commit_id: &str, pack_id: u32) -> Vec<u8> {
111
- let commit_id = commit_id.as_bytes();
112
- let mut key = Vec::with_capacity(4 + commit_id.len() + 4);
113
- key.extend_from_slice(&(commit_id.len() as u32).to_be_bytes());
114
- key.extend_from_slice(commit_id);
115
- key.extend_from_slice(&pack_id.to_be_bytes());
116
- key
117
- }
118
-
119
- pub(crate) fn decode_json_pack_refs(bytes: &[u8]) -> Result<Vec<JsonRef>, LixError> {
120
- let layout = json_pack_layout(bytes)?;
121
- let mut refs = Vec::with_capacity(layout.count);
122
- for index in 0..layout.count {
123
- refs.push(JsonRef::from_hash_bytes(
124
- json_pack_entry(bytes, &layout, index)?.hash,
125
- ));
126
- }
127
- Ok(refs)
128
- }
129
-
130
- pub(crate) fn encode_json_pack(entries: &[&EncodedJson<'_>]) -> Result<Vec<u8>, LixError> {
131
- let mut directory_len =
132
- STORED_JSON_PACK_MAGIC.len() + 4 + entries.len() * STORED_JSON_PACK_ENTRY_HEADER_LEN;
133
- let payload_len = entries
134
- .iter()
135
- .map(|entry| entry.data.as_ref().len())
136
- .sum::<usize>();
137
- let mut out = Vec::with_capacity(directory_len + payload_len);
138
- out.extend_from_slice(STORED_JSON_PACK_MAGIC);
139
- out.extend_from_slice(&(entries.len() as u32).to_be_bytes());
140
-
141
- let mut offset = 0usize;
142
- for entry in entries {
143
- let data = entry.data.as_ref();
144
- out.extend_from_slice(entry.json_ref.as_hash_bytes());
145
- out.push(json_codec_byte(entry.codec));
146
- out.extend_from_slice(&json_pack_u32(
147
- entry.uncompressed_len,
148
- "uncompressed length",
149
- )?);
150
- out.extend_from_slice(&json_pack_u32(offset, "payload offset")?);
151
- out.extend_from_slice(&json_pack_u32(data.len(), "payload length")?);
152
- offset = offset.checked_add(data.len()).ok_or_else(|| {
153
- LixError::new(
154
- LixError::CODE_INTERNAL_ERROR,
155
- "json_store pack payload offset overflow",
156
- )
157
- })?;
158
- }
159
- for entry in entries {
160
- out.extend_from_slice(entry.data.as_ref());
161
- }
162
- directory_len = out.len() - payload_len;
163
- debug_assert_eq!(
164
- directory_len,
165
- STORED_JSON_PACK_MAGIC.len() + 4 + entries.len() * STORED_JSON_PACK_ENTRY_HEADER_LEN
166
- );
167
- Ok(out)
168
- }
169
-
170
- fn json_pack_u32(value: usize, field: &str) -> Result<[u8; 4], LixError> {
171
- let value = u32::try_from(value).map_err(|_| {
172
- LixError::new(
173
- LixError::CODE_INTERNAL_ERROR,
174
- format!("json_store pack {field} exceeds u32"),
175
- )
176
- })?;
177
- Ok(value.to_be_bytes())
178
- }
179
-
180
94
  pub(crate) fn encode_json_bytes_for_storage(bytes: &[u8]) -> Result<(JsonRef, Vec<u8>), LixError> {
181
95
  let json = std::str::from_utf8(bytes).map_err(|error| {
182
96
  LixError::new(
@@ -198,21 +112,13 @@ pub(crate) fn encode_json_str_for_storage_with_ref(
198
112
  }
199
113
 
200
114
  async fn load_json_bytes_direct(
201
- store: &mut impl StorageReader,
115
+ store: &(impl StorageRead + ?Sized),
202
116
  json_ref: &JsonRef,
203
117
  ) -> Result<Option<Vec<u8>>, LixError> {
204
- let result = store
205
- .get_values(KvGetRequest {
206
- groups: vec![KvGetGroup {
207
- namespace: JSON_NAMESPACE.to_string(),
208
- keys: vec![json_ref.as_hash_bytes().to_vec()],
209
- }],
210
- })
211
- .await?
212
- .groups
118
+ let result = load_values(store, JSON_SPACE, vec![json_ref.as_hash_bytes().to_vec()])?
213
119
  .into_iter()
214
120
  .next()
215
- .and_then(|group| group.single_value_owned());
121
+ .flatten();
216
122
  let Some(bytes) = result else {
217
123
  return Ok(None);
218
124
  };
@@ -222,9 +128,9 @@ async fn load_json_bytes_direct(
222
128
  }
223
129
 
224
130
  pub(crate) async fn load_json_bytes_many_in_scope(
225
- store: &mut impl StorageReader,
131
+ store: &(impl StorageRead + ?Sized),
226
132
  json_refs: &[JsonRef],
227
- scope: JsonReadScopeRef<'_>,
133
+ scope: JsonReadScopeRef,
228
134
  ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
229
135
  load_json_bytes_many_in_scope_with_hash_check(
230
136
  store,
@@ -236,39 +142,24 @@ pub(crate) async fn load_json_bytes_many_in_scope(
236
142
  }
237
143
 
238
144
  pub(crate) async fn verify_json_bytes_many_in_scope(
239
- store: &mut impl StorageReader,
145
+ store: &impl StorageRead,
240
146
  json_refs: &[JsonRef],
241
- scope: JsonReadScopeRef<'_>,
147
+ scope: JsonReadScopeRef,
242
148
  ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
243
149
  load_json_bytes_many_in_scope_with_hash_check(store, json_refs, scope, JsonHashCheck::Verify)
244
150
  .await
245
151
  }
246
152
 
247
153
  async fn load_json_bytes_many_in_scope_with_hash_check(
248
- store: &mut impl StorageReader,
154
+ store: &(impl StorageRead + ?Sized),
249
155
  json_refs: &[JsonRef],
250
- scope: JsonReadScopeRef<'_>,
156
+ scope: JsonReadScopeRef,
251
157
  hash_check: JsonHashCheck,
252
158
  ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
253
159
  if json_refs.is_empty() {
254
160
  return Ok(Vec::new());
255
161
  }
256
162
 
257
- let ordered_single_pack_probe = if let JsonReadScopeRef::CommitPacks {
258
- commit_id,
259
- pack_ids: [pack_id],
260
- } = scope
261
- {
262
- let probe =
263
- load_ordered_single_pack(store, json_refs, commit_id, *pack_id, hash_check).await?;
264
- if let OrderedSinglePackProbe::Hit(values) = probe {
265
- return Ok(values);
266
- }
267
- Some(probe)
268
- } else {
269
- None
270
- };
271
-
272
163
  let mut unique_keys = Vec::new();
273
164
  let mut unique_refs = Vec::new();
274
165
  let mut key_indexes = HashMap::<[u8; 32], usize>::new();
@@ -292,26 +183,8 @@ async fn load_json_bytes_many_in_scope_with_hash_check(
292
183
  requested_indexes.push(index);
293
184
  }
294
185
 
295
- let mut unique_values = match scope {
296
- JsonReadScopeRef::OutOfBand => vec![None; unique_refs.len()],
297
- JsonReadScopeRef::CommitPacks {
298
- commit_id,
299
- pack_ids: [pack_id],
300
- } => match &ordered_single_pack_probe {
301
- Some(OrderedSinglePackProbe::MissPresent(stored_pack)) => {
302
- load_from_single_pack_bytes(stored_pack, &unique_refs, hash_check)?
303
- }
304
- Some(OrderedSinglePackProbe::MissAbsent) => vec![None; unique_refs.len()],
305
- _ => {
306
- let pack_ids = [*pack_id];
307
- load_from_packs(store, &unique_refs, commit_id, &pack_ids, hash_check).await?
308
- }
309
- },
310
- JsonReadScopeRef::CommitPacks {
311
- commit_id,
312
- pack_ids,
313
- } => load_from_packs(store, &unique_refs, commit_id, pack_ids, hash_check).await?,
314
- };
186
+ let JsonReadScopeRef::OutOfBand = scope;
187
+ let mut unique_values = vec![None; unique_refs.len()];
315
188
 
316
189
  let missing = unique_values
317
190
  .iter()
@@ -326,40 +199,31 @@ async fn load_json_bytes_many_in_scope_with_hash_check(
326
199
  ));
327
200
  }
328
201
 
329
- let result = store
330
- .get_values(KvGetRequest {
331
- groups: vec![KvGetGroup {
332
- namespace: JSON_NAMESPACE.to_string(),
333
- keys: missing
334
- .iter()
335
- .map(|&index| unique_keys[index].clone())
336
- .collect(),
337
- }],
338
- })
339
- .await?;
340
- let group = result.groups.into_iter().next().ok_or_else(|| {
341
- LixError::new(
342
- LixError::CODE_INTERNAL_ERROR,
343
- "json_store batch load returned no result group",
344
- )
345
- })?;
346
- if group.len() != missing.len() {
202
+ let loaded = load_values(
203
+ store,
204
+ JSON_SPACE,
205
+ missing
206
+ .iter()
207
+ .map(|&index| unique_keys[index].clone())
208
+ .collect(),
209
+ )?;
210
+ if loaded.len() != missing.len() {
347
211
  return Err(LixError::new(
348
212
  LixError::CODE_INTERNAL_ERROR,
349
213
  format!(
350
214
  "json_store batch load returned {} values for {} requested refs",
351
- group.len(),
215
+ loaded.len(),
352
216
  missing.len()
353
217
  ),
354
218
  ));
355
219
  }
356
220
 
357
- for (index, stored_bytes) in group.values_iter().enumerate() {
221
+ for (index, stored_bytes) in loaded.into_iter().enumerate() {
358
222
  let unique_index = missing[index];
359
223
  let Some(stored_bytes) = stored_bytes else {
360
224
  continue;
361
225
  };
362
- let stored_payload = decode_stored_json_payload(stored_bytes)?;
226
+ let stored_payload = decode_stored_json_payload(&stored_bytes)?;
363
227
  let _ = store;
364
228
  unique_values[unique_index] = Some(decode_json_payload(
365
229
  &unique_refs[unique_index],
@@ -395,117 +259,25 @@ fn json_values_in_request_order(
395
259
  .collect()
396
260
  }
397
261
 
398
- async fn load_ordered_single_pack(
399
- store: &mut impl StorageReader,
400
- requested_refs: &[JsonRef],
401
- commit_id: &str,
402
- pack_id: u32,
403
- hash_check: JsonHashCheck,
404
- ) -> Result<OrderedSinglePackProbe, LixError> {
405
- let result = store
406
- .get_values(KvGetRequest {
407
- groups: vec![KvGetGroup {
408
- namespace: JSON_PACK_NAMESPACE.to_string(),
409
- keys: vec![pack_key(commit_id, pack_id)],
410
- }],
411
- })
412
- .await?;
413
- let group = result.groups.into_iter().next().ok_or_else(|| {
414
- LixError::new(
415
- LixError::CODE_INTERNAL_ERROR,
416
- "json_store ordered pack load returned no result group",
417
- )
418
- })?;
419
- if group.len() != 1 {
420
- return Err(LixError::new(
421
- LixError::CODE_INTERNAL_ERROR,
422
- format!(
423
- "json_store ordered pack load returned {} values for 1 requested pack",
424
- group.len()
425
- ),
426
- ));
427
- }
428
- let Some(stored_pack) = group.value(0).flatten() else {
429
- return Ok(OrderedSinglePackProbe::MissAbsent);
430
- };
431
- let mut values = vec![None; requested_refs.len()];
432
- if load_json_pack_values_in_request_order(stored_pack, hash_check, requested_refs, &mut values)?
433
- {
434
- Ok(OrderedSinglePackProbe::Hit(values))
435
- } else {
436
- Ok(OrderedSinglePackProbe::MissPresent(stored_pack.to_vec()))
437
- }
438
- }
439
-
440
- fn load_from_single_pack_bytes(
441
- stored_pack: &[u8],
442
- unique_refs: &[JsonRef],
443
- hash_check: JsonHashCheck,
262
+ fn load_values(
263
+ store: &(impl StorageRead + ?Sized),
264
+ space: StorageSpace,
265
+ keys: Vec<Vec<u8>>,
444
266
  ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
445
- let mut values = vec![None; unique_refs.len()];
446
- if load_json_pack_values_in_request_order(stored_pack, hash_check, unique_refs, &mut values)? {
447
- return Ok(values);
448
- }
449
- let wanted = unique_refs
450
- .iter()
451
- .enumerate()
452
- .map(|(index, json_ref)| (*json_ref.as_hash_array(), index))
453
- .collect::<HashMap<_, _>>();
454
- load_json_pack_values(stored_pack, hash_check, &wanted, &mut values)?;
455
- Ok(values)
456
- }
457
-
458
- async fn load_from_packs(
459
- store: &mut impl StorageReader,
460
- unique_refs: &[JsonRef],
461
- commit_id: &str,
462
- pack_ids: &[u32],
463
- hash_check: JsonHashCheck,
464
- ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
465
- let mut values = vec![None; unique_refs.len()];
466
- if pack_ids.is_empty() || unique_refs.is_empty() {
467
- return Ok(values);
468
- }
469
- let keys = pack_ids
470
- .iter()
471
- .map(|&pack_id| pack_key(commit_id, pack_id))
267
+ let keys = keys
268
+ .into_iter()
269
+ .map(|key| StorageKey(Bytes::from(key)))
472
270
  .collect::<Vec<_>>();
473
- let result = store
474
- .get_values(KvGetRequest {
475
- groups: vec![KvGetGroup {
476
- namespace: JSON_PACK_NAMESPACE.to_string(),
477
- keys,
478
- }],
271
+ let result =
272
+ PointReadPlan::new(space, &keys).materialize(store, StorageGetOptions::default())?;
273
+ Ok(result
274
+ .value
275
+ .into_iter()
276
+ .map(|value| match value {
277
+ Some(StorageProjectedValue::FullValue(bytes)) => Some(bytes.to_vec()),
278
+ Some(StorageProjectedValue::KeyOnly) | None => None,
479
279
  })
480
- .await?;
481
- let group = result.groups.into_iter().next().ok_or_else(|| {
482
- LixError::new(
483
- LixError::CODE_INTERNAL_ERROR,
484
- "json_store pack load returned no result group",
485
- )
486
- })?;
487
- if pack_ids.len() == 1 && group.len() == 1 {
488
- if let Some(stored_pack) = group.value(0).flatten() {
489
- if load_json_pack_values_in_request_order(
490
- stored_pack,
491
- hash_check,
492
- unique_refs,
493
- &mut values,
494
- )? {
495
- return Ok(values);
496
- }
497
- }
498
- }
499
-
500
- let wanted = unique_refs
501
- .iter()
502
- .enumerate()
503
- .map(|(index, json_ref)| (*json_ref.as_hash_array(), index))
504
- .collect::<HashMap<_, _>>();
505
- for stored_pack in group.values_iter().flatten() {
506
- load_json_pack_values(stored_pack, hash_check, &wanted, &mut values)?;
507
- }
508
- Ok(values)
280
+ .collect())
509
281
  }
510
282
 
511
283
  fn encode_stored_json_payload(encoded_json: &EncodedJson<'_>) -> Vec<u8> {
@@ -599,202 +371,38 @@ fn decode_json_payload(
599
371
  Ok(data)
600
372
  }
601
373
 
602
- fn load_json_pack_values_in_request_order(
603
- bytes: &[u8],
604
- hash_check: JsonHashCheck,
605
- requested_refs: &[JsonRef],
606
- values: &mut [Option<Vec<u8>>],
607
- ) -> Result<bool, LixError> {
608
- if values.len() < requested_refs.len() {
609
- return Err(LixError::new(
610
- LixError::CODE_INTERNAL_ERROR,
611
- "json_store ordered pack load has fewer result slots than refs",
612
- ));
613
- }
614
- let layout = json_pack_layout(bytes)?;
615
- if layout.count != requested_refs.len() {
616
- return Ok(false);
617
- }
618
-
619
- for (index, json_ref) in requested_refs.iter().enumerate() {
620
- let entry = json_pack_entry(bytes, &layout, index)?;
621
- if &entry.hash != json_ref.as_hash_array() {
622
- for value in &mut values[..index] {
623
- *value = None;
624
- }
625
- return Ok(false);
626
- }
627
- values[index] = Some(decode_json_payload(json_ref, entry.payload, hash_check)?);
628
- }
629
- Ok(true)
630
- }
631
-
632
- fn load_json_pack_values(
633
- bytes: &[u8],
634
- hash_check: JsonHashCheck,
635
- wanted: &HashMap<[u8; 32], usize>,
636
- values: &mut [Option<Vec<u8>>],
637
- ) -> Result<(), LixError> {
638
- let layout = json_pack_layout(bytes)?;
639
- for index in 0..layout.count {
640
- let entry = json_pack_entry(bytes, &layout, index)?;
641
- let Some(&value_index) = wanted.get(&entry.hash) else {
642
- continue;
643
- };
644
- let json_ref = JsonRef::from_hash_bytes(entry.hash);
645
- values[value_index] = Some(decode_json_payload(&json_ref, entry.payload, hash_check)?);
646
- }
647
- Ok(())
648
- }
649
-
650
- fn json_pack_layout(bytes: &[u8]) -> Result<JsonPackLayout, LixError> {
651
- if bytes.len() < STORED_JSON_PACK_MAGIC.len() + 4 {
652
- return Err(LixError::new(
653
- "LIX_ERROR_UNKNOWN",
654
- "stored JSON pack is truncated",
655
- ));
656
- }
657
- if &bytes[..STORED_JSON_PACK_MAGIC.len()] != STORED_JSON_PACK_MAGIC {
658
- return Err(LixError::new(
659
- "LIX_ERROR_UNKNOWN",
660
- "stored JSON pack has invalid header",
661
- ));
662
- }
663
- let count_start = STORED_JSON_PACK_MAGIC.len();
664
- let count_end = count_start + 4;
665
- let count = u32::from_be_bytes(
666
- bytes[count_start..count_end]
667
- .try_into()
668
- .expect("json pack count header is fixed size"),
669
- ) as usize;
670
- let directory_start = count_end;
671
- let directory_len = count
672
- .checked_mul(STORED_JSON_PACK_ENTRY_HEADER_LEN)
673
- .ok_or_else(|| {
674
- LixError::new(
675
- LixError::CODE_INTERNAL_ERROR,
676
- "json pack directory overflow",
677
- )
678
- })?;
679
- let payload_start = directory_start.checked_add(directory_len).ok_or_else(|| {
680
- LixError::new(
681
- LixError::CODE_INTERNAL_ERROR,
682
- "json pack payload offset overflow",
683
- )
684
- })?;
685
- if bytes.len() < payload_start {
686
- return Err(LixError::new(
687
- "LIX_ERROR_UNKNOWN",
688
- "stored JSON pack directory is truncated",
689
- ));
690
- }
691
- Ok(JsonPackLayout {
692
- directory_start,
693
- payload_start,
694
- count,
695
- })
696
- }
697
-
698
- fn json_pack_entry<'a>(
699
- bytes: &'a [u8],
700
- layout: &JsonPackLayout,
701
- index: usize,
702
- ) -> Result<JsonPackEntry<'a>, LixError> {
703
- if index >= layout.count {
704
- return Err(LixError::new(
705
- LixError::CODE_INTERNAL_ERROR,
706
- "json pack entry index exceeds directory count",
707
- ));
708
- }
709
- let mut cursor = layout.directory_start + index * STORED_JSON_PACK_ENTRY_HEADER_LEN;
710
- let hash: [u8; 32] = bytes[cursor..cursor + 32]
711
- .try_into()
712
- .expect("json pack hash header is fixed size");
713
- cursor += 32;
714
- let codec = read_json_codec(bytes[cursor])?;
715
- cursor += 1;
716
- let uncompressed_len = u32::from_be_bytes(
717
- bytes[cursor..cursor + 4]
718
- .try_into()
719
- .expect("json pack uncompressed length is fixed size"),
720
- ) as usize;
721
- cursor += 4;
722
- let offset = u32::from_be_bytes(
723
- bytes[cursor..cursor + 4]
724
- .try_into()
725
- .expect("json pack payload offset is fixed size"),
726
- ) as usize;
727
- cursor += 4;
728
- let len = u32::from_be_bytes(
729
- bytes[cursor..cursor + 4]
730
- .try_into()
731
- .expect("json pack payload length is fixed size"),
732
- ) as usize;
733
- let data_start = layout.payload_start.checked_add(offset).ok_or_else(|| {
734
- LixError::new(
735
- LixError::CODE_INTERNAL_ERROR,
736
- "json pack entry offset overflow",
737
- )
738
- })?;
739
- let data_end = data_start.checked_add(len).ok_or_else(|| {
740
- LixError::new(
741
- LixError::CODE_INTERNAL_ERROR,
742
- "json pack entry length overflow",
743
- )
744
- })?;
745
- if data_end > bytes.len() {
746
- return Err(LixError::new(
747
- "LIX_ERROR_UNKNOWN",
748
- "stored JSON pack entry payload is truncated",
749
- ));
750
- }
751
- Ok(JsonPackEntry {
752
- hash,
753
- payload: StoredJsonPayload {
754
- codec,
755
- uncompressed_len,
756
- data: &bytes[data_start..data_end],
757
- },
758
- })
759
- }
760
-
761
374
  #[cfg(test)]
762
375
  mod tests {
763
- use std::sync::Arc;
764
-
765
376
  use super::*;
766
- use crate::backend::testing::UnitTestBackend;
767
- use crate::storage::{StorageContext, StorageWriteSet};
377
+ use crate::storage::StorageContext;
378
+ use crate::storage::{
379
+ InMemoryStorageBackend, StorageKey, StorageReadOptions, StorageValue, StorageWriteOptions,
380
+ };
768
381
 
769
382
  #[tokio::test]
770
383
  async fn json_roundtrips_raw_payload() {
771
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
384
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
772
385
  let json = "{\"value\":\"small\"}";
773
386
  let encoded = encode_json(json).expect("json should encode");
774
387
  assert_eq!(encoded.codec, JsonCodec::Raw);
775
388
 
776
- let mut transaction = storage
777
- .begin_write_transaction()
778
- .await
779
- .expect("transaction should open");
780
- let mut writes = StorageWriteSet::new();
389
+ let mut writes = storage.new_write_set();
781
390
  writes.put(
782
- JSON_NAMESPACE,
783
- encoded.json_ref.as_hash_bytes().to_vec(),
784
- encode_stored_json_payload(&encoded),
391
+ JSON_SPACE,
392
+ StorageKey(Bytes::copy_from_slice(encoded.json_ref.as_hash_bytes())),
393
+ StorageValue {
394
+ bytes: Bytes::from(encode_stored_json_payload(&encoded)),
395
+ },
785
396
  );
786
- writes
787
- .apply(&mut transaction.as_mut())
788
- .await
789
- .expect("json should store");
790
- transaction
791
- .commit()
792
- .await
793
- .expect("transaction should commit");
397
+ storage
398
+ .commit_write_set(writes, StorageWriteOptions::default())
399
+ .expect("writes should commit");
794
400
 
795
- let mut store = storage.clone();
401
+ let store = storage
402
+ .begin_read(StorageReadOptions::default())
403
+ .expect("read should open");
796
404
  assert_eq!(
797
- load_json_bytes_direct(&mut store, &encoded.json_ref)
405
+ load_json_bytes_direct(&store, &encoded.json_ref)
798
406
  .await
799
407
  .expect("json should load"),
800
408
  Some(json.as_bytes().to_vec())
@@ -803,37 +411,34 @@ mod tests {
803
411
 
804
412
  #[tokio::test]
805
413
  async fn json_batch_load_roundtrips_in_request_order() {
806
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
414
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
807
415
  let first = encode_json("{\"value\":\"first\"}").expect("first json should encode");
808
416
  let second = encode_json("{\"value\":\"second\"}").expect("second json should encode");
809
417
 
810
- let mut transaction = storage
811
- .begin_write_transaction()
812
- .await
813
- .expect("transaction should open");
814
- let mut writes = StorageWriteSet::new();
418
+ let mut writes = storage.new_write_set();
815
419
  writes.put(
816
- JSON_NAMESPACE,
817
- first.json_ref.as_hash_bytes().to_vec(),
818
- encode_stored_json_payload(&first),
420
+ JSON_SPACE,
421
+ StorageKey(Bytes::copy_from_slice(first.json_ref.as_hash_bytes())),
422
+ StorageValue {
423
+ bytes: Bytes::from(encode_stored_json_payload(&first)),
424
+ },
819
425
  );
820
426
  writes.put(
821
- JSON_NAMESPACE,
822
- second.json_ref.as_hash_bytes().to_vec(),
823
- encode_stored_json_payload(&second),
427
+ JSON_SPACE,
428
+ StorageKey(Bytes::copy_from_slice(second.json_ref.as_hash_bytes())),
429
+ StorageValue {
430
+ bytes: Bytes::from(encode_stored_json_payload(&second)),
431
+ },
824
432
  );
825
- writes
826
- .apply(&mut transaction.as_mut())
827
- .await
828
- .expect("json should store");
829
- transaction
830
- .commit()
831
- .await
832
- .expect("transaction should commit");
433
+ storage
434
+ .commit_write_set(writes, StorageWriteOptions::default())
435
+ .expect("writes should commit");
833
436
 
834
- let mut store = storage.clone();
437
+ let store = storage
438
+ .begin_read(StorageReadOptions::default())
439
+ .expect("read should open");
835
440
  let values = load_json_bytes_many_in_scope(
836
- &mut store,
441
+ &store,
837
442
  &[second.json_ref, first.json_ref, second.json_ref],
838
443
  JsonReadScopeRef::OutOfBand,
839
444
  )
@@ -852,258 +457,38 @@ mod tests {
852
457
 
853
458
  #[tokio::test]
854
459
  async fn verified_batch_load_rejects_hash_mismatch() {
855
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
460
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
856
461
  let requested_ref = JsonRef::for_content(br#"{"value":"requested"}"#);
857
462
  let stored = encode_json("{\"value\":\"different\"}").expect("stored json should encode");
858
463
 
859
- let mut transaction = storage
860
- .begin_write_transaction()
861
- .await
862
- .expect("transaction should open");
863
- let mut writes = StorageWriteSet::new();
464
+ let mut writes = storage.new_write_set();
864
465
  writes.put(
865
- JSON_NAMESPACE,
866
- requested_ref.as_hash_bytes().to_vec(),
867
- encode_stored_json_payload(&stored),
466
+ JSON_SPACE,
467
+ StorageKey(Bytes::copy_from_slice(requested_ref.as_hash_bytes())),
468
+ StorageValue {
469
+ bytes: Bytes::from(encode_stored_json_payload(&stored)),
470
+ },
868
471
  );
869
- writes
870
- .apply(&mut transaction.as_mut())
871
- .await
872
- .expect("json should store");
873
- transaction
874
- .commit()
875
- .await
876
- .expect("transaction should commit");
877
-
878
- let mut store = storage.clone();
879
- let trusted = load_json_bytes_many_in_scope(
880
- &mut store,
881
- &[requested_ref],
882
- JsonReadScopeRef::OutOfBand,
883
- )
884
- .await
885
- .expect("trusted hot read should not hash-check");
472
+ storage
473
+ .commit_write_set(writes, StorageWriteOptions::default())
474
+ .expect("writes should commit");
475
+
476
+ let store = storage
477
+ .begin_read(StorageReadOptions::default())
478
+ .expect("read should open");
479
+ let trusted =
480
+ load_json_bytes_many_in_scope(&store, &[requested_ref], JsonReadScopeRef::OutOfBand)
481
+ .await
482
+ .expect("trusted hot read should not hash-check");
886
483
  assert_eq!(trusted, vec![Some(stored.data.as_ref().to_vec())]);
887
484
 
888
- let mut store = storage.clone();
889
- let error = verify_json_bytes_many_in_scope(
890
- &mut store,
891
- &[requested_ref],
892
- JsonReadScopeRef::OutOfBand,
893
- )
894
- .await
895
- .expect_err("verified read should reject mismatched content address");
896
- assert!(
897
- error.to_string().contains("hash mismatch"),
898
- "error should mention hash mismatch: {error}"
899
- );
900
- }
901
-
902
- #[tokio::test]
903
- async fn verified_pack_load_checks_only_requested_entries() {
904
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
905
- let good = encode_json("{\"value\":\"good\"}").expect("good json should encode");
906
- let bad_ref = JsonRef::for_content(br#"{"value":"expected"}"#);
907
- let bad = encode_json_for_storage_with_ref("{\"value\":\"wrong\"}", bad_ref)
908
- .expect("bad json should encode with mismatched ref");
909
-
910
- let mut transaction = storage
911
- .begin_write_transaction()
912
- .await
913
- .expect("transaction should open");
914
- let mut writes = StorageWriteSet::new();
915
- writes.put(
916
- JSON_PACK_NAMESPACE,
917
- pack_key("commit-a", 0),
918
- encode_json_pack(&[&good, &bad]).expect("pack should encode"),
919
- );
920
- writes
921
- .apply(&mut transaction.as_mut())
922
- .await
923
- .expect("json pack should store");
924
- transaction
925
- .commit()
926
- .await
927
- .expect("transaction should commit");
928
-
929
- let pack_ids = [0];
930
- let mut store = storage.clone();
931
- let good_values = verify_json_bytes_many_in_scope(
932
- &mut store,
933
- &[good.json_ref],
934
- JsonReadScopeRef::CommitPacks {
935
- commit_id: "commit-a",
936
- pack_ids: &pack_ids,
937
- },
938
- )
939
- .await
940
- .expect("unrequested bad pack entry should not be decoded");
941
- assert_eq!(good_values, vec![Some(good.data.as_ref().to_vec())]);
942
-
943
- let mut store = storage.clone();
944
- let error = verify_json_bytes_many_in_scope(
945
- &mut store,
946
- &[bad_ref],
947
- JsonReadScopeRef::CommitPacks {
948
- commit_id: "commit-a",
949
- pack_ids: &pack_ids,
950
- },
951
- )
952
- .await
953
- .expect_err("requested bad pack entry should be verified");
485
+ let error =
486
+ verify_json_bytes_many_in_scope(&store, &[requested_ref], JsonReadScopeRef::OutOfBand)
487
+ .await
488
+ .expect_err("verified read should reject mismatched content address");
954
489
  assert!(
955
490
  error.to_string().contains("hash mismatch"),
956
491
  "error should mention hash mismatch: {error}"
957
492
  );
958
493
  }
959
-
960
- #[test]
961
- fn json_pack_directory_uses_compact_u32_fields() {
962
- let first = encode_json("{\"value\":\"first\"}").expect("first json should encode");
963
- let second = encode_json("{\"value\":\"second\"}").expect("second json should encode");
964
- let pack = encode_json_pack(&[&first, &second]).expect("pack should encode");
965
- let payload_len = first.data.as_ref().len() + second.data.as_ref().len();
966
-
967
- assert_eq!(STORED_JSON_PACK_ENTRY_HEADER_LEN, 32 + 1 + 4 + 4 + 4);
968
- assert_eq!(
969
- pack.len(),
970
- STORED_JSON_PACK_MAGIC.len() + 4 + 2 * STORED_JSON_PACK_ENTRY_HEADER_LEN + payload_len
971
- );
972
- }
973
-
974
- #[test]
975
- fn json_pack_u32_rejects_oversized_directory_fields() {
976
- let error = json_pack_u32((u32::MAX as usize) + 1, "payload offset")
977
- .expect_err("oversized pack directory field should reject");
978
- assert!(
979
- error.to_string().contains("payload offset exceeds u32"),
980
- "error should identify oversized field: {error}"
981
- );
982
- }
983
-
984
- #[test]
985
- fn ordered_pack_load_fast_path_requires_exact_pack_order() {
986
- let first = encode_json("{\"value\":\"first\"}").expect("first json should encode");
987
- let second = encode_json("{\"value\":\"second\"}").expect("second json should encode");
988
- let pack = encode_json_pack(&[&first, &second]).expect("pack should encode");
989
-
990
- let mut values = vec![None, None];
991
- let loaded = load_json_pack_values_in_request_order(
992
- &pack,
993
- JsonHashCheck::Verify,
994
- &[first.json_ref, second.json_ref],
995
- &mut values,
996
- )
997
- .expect("ordered pack load should parse");
998
- assert!(loaded);
999
- assert_eq!(
1000
- values,
1001
- vec![
1002
- Some(first.data.as_ref().to_vec()),
1003
- Some(second.data.as_ref().to_vec()),
1004
- ]
1005
- );
1006
-
1007
- let mut values = vec![None, None];
1008
- let loaded = load_json_pack_values_in_request_order(
1009
- &pack,
1010
- JsonHashCheck::Verify,
1011
- &[second.json_ref, first.json_ref],
1012
- &mut values,
1013
- )
1014
- .expect("unordered refs should fall back without error");
1015
- assert!(!loaded);
1016
- assert_eq!(values, vec![None, None]);
1017
- }
1018
-
1019
- #[tokio::test]
1020
- async fn pack_batch_load_falls_back_for_unordered_refs() {
1021
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
1022
- let first = encode_json("{\"value\":\"first\"}").expect("first json should encode");
1023
- let second = encode_json("{\"value\":\"second\"}").expect("second json should encode");
1024
-
1025
- let mut transaction = storage
1026
- .begin_write_transaction()
1027
- .await
1028
- .expect("transaction should open");
1029
- let mut writes = StorageWriteSet::new();
1030
- writes.put(
1031
- JSON_PACK_NAMESPACE,
1032
- pack_key("commit-a", 0),
1033
- encode_json_pack(&[&first, &second]).expect("pack should encode"),
1034
- );
1035
- writes
1036
- .apply(&mut transaction.as_mut())
1037
- .await
1038
- .expect("json pack should store");
1039
- transaction
1040
- .commit()
1041
- .await
1042
- .expect("transaction should commit");
1043
-
1044
- let pack_ids = [0];
1045
- let mut store = storage.clone();
1046
- let values = load_json_bytes_many_in_scope(
1047
- &mut store,
1048
- &[second.json_ref, first.json_ref],
1049
- JsonReadScopeRef::CommitPacks {
1050
- commit_id: "commit-a",
1051
- pack_ids: &pack_ids,
1052
- },
1053
- )
1054
- .await
1055
- .expect("unordered refs should load through fallback");
1056
- assert_eq!(
1057
- values,
1058
- vec![
1059
- Some(second.data.as_ref().to_vec()),
1060
- Some(first.data.as_ref().to_vec()),
1061
- ]
1062
- );
1063
- }
1064
-
1065
- #[tokio::test]
1066
- async fn ordered_pack_probe_falls_back_to_direct_rows() {
1067
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
1068
- let packed = encode_json("{\"value\":\"packed\"}").expect("packed json should encode");
1069
- let direct = encode_json("{\"value\":\"direct\"}").expect("direct json should encode");
1070
-
1071
- let mut transaction = storage
1072
- .begin_write_transaction()
1073
- .await
1074
- .expect("transaction should open");
1075
- let mut writes = StorageWriteSet::new();
1076
- writes.put(
1077
- JSON_PACK_NAMESPACE,
1078
- pack_key("commit-a", 0),
1079
- encode_json_pack(&[&packed]).expect("pack should encode"),
1080
- );
1081
- writes.put(
1082
- JSON_NAMESPACE,
1083
- direct.json_ref.as_hash_bytes().to_vec(),
1084
- encode_stored_json_payload(&direct),
1085
- );
1086
- writes
1087
- .apply(&mut transaction.as_mut())
1088
- .await
1089
- .expect("json rows should store");
1090
- transaction
1091
- .commit()
1092
- .await
1093
- .expect("transaction should commit");
1094
-
1095
- let pack_ids = [0];
1096
- let mut store = storage.clone();
1097
- let values = load_json_bytes_many_in_scope(
1098
- &mut store,
1099
- &[direct.json_ref],
1100
- JsonReadScopeRef::CommitPacks {
1101
- commit_id: "commit-a",
1102
- pack_ids: &pack_ids,
1103
- },
1104
- )
1105
- .await
1106
- .expect("mismatched ordered pack probe should fall back to direct rows");
1107
- assert_eq!(values, vec![Some(direct.data.as_ref().to_vec())]);
1108
- }
1109
494
  }