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

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 (165) hide show
  1. package/SKILL.md +4 -5
  2. package/dist/engine-wasm/wasm/lix_engine.js +1 -1
  3. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  4. package/dist/generated/builtin-schemas.d.ts +87 -162
  5. package/dist/generated/builtin-schemas.js +139 -236
  6. package/dist/open-lix.d.ts +1 -1
  7. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  8. package/dist-engine-src/src/catalog/context.rs +412 -0
  9. package/dist-engine-src/src/catalog/mod.rs +10 -0
  10. package/dist-engine-src/src/catalog/schema.rs +4 -0
  11. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  12. package/dist-engine-src/src/cel/mod.rs +1 -1
  13. package/dist-engine-src/src/cel/provider.rs +1 -1
  14. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  15. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  16. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  17. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  18. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  19. package/dist-engine-src/src/commit_store/context.rs +944 -0
  20. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  21. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  22. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  23. package/dist-engine-src/src/commit_store/types.rs +215 -0
  24. package/dist-engine-src/src/common/identity.rs +15 -5
  25. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  26. package/dist-engine-src/src/common/metadata.rs +17 -12
  27. package/dist-engine-src/src/common/mod.rs +5 -5
  28. package/dist-engine-src/src/domain.rs +324 -0
  29. package/dist-engine-src/src/engine.rs +29 -43
  30. package/dist-engine-src/src/entity_identity.rs +238 -118
  31. package/dist-engine-src/src/functions/context.rs +17 -52
  32. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  33. package/dist-engine-src/src/functions/mod.rs +1 -1
  34. package/dist-engine-src/src/functions/provider.rs +4 -4
  35. package/dist-engine-src/src/functions/state.rs +39 -66
  36. package/dist-engine-src/src/functions/types.rs +1 -1
  37. package/dist-engine-src/src/init.rs +204 -151
  38. package/dist-engine-src/src/json_store/context.rs +354 -60
  39. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  40. package/dist-engine-src/src/json_store/mod.rs +4 -1
  41. package/dist-engine-src/src/json_store/store.rs +884 -11
  42. package/dist-engine-src/src/json_store/types.rs +166 -1
  43. package/dist-engine-src/src/lib.rs +10 -9
  44. package/dist-engine-src/src/live_state/context.rs +608 -830
  45. package/dist-engine-src/src/live_state/mod.rs +3 -3
  46. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  47. package/dist-engine-src/src/live_state/reader.rs +5 -5
  48. package/dist-engine-src/src/live_state/types.rs +19 -36
  49. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  50. package/dist-engine-src/src/plugin/archive.rs +3 -6
  51. package/dist-engine-src/src/plugin/install.rs +0 -18
  52. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  53. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  54. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  55. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  56. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  57. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  58. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  60. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  61. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  63. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  64. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  65. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  66. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  67. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  68. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  69. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  70. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  71. package/dist-engine-src/src/schema/definition.json +47 -17
  72. package/dist-engine-src/src/schema/definition.rs +202 -96
  73. package/dist-engine-src/src/schema/key.rs +9 -77
  74. package/dist-engine-src/src/schema/mod.rs +4 -4
  75. package/dist-engine-src/src/schema/tests.rs +133 -92
  76. package/dist-engine-src/src/session/context.rs +40 -42
  77. package/dist-engine-src/src/session/create_version.rs +22 -14
  78. package/dist-engine-src/src/session/execute.rs +45 -14
  79. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  80. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  81. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  82. package/dist-engine-src/src/session/merge/version.rs +35 -45
  83. package/dist-engine-src/src/session/mod.rs +4 -2
  84. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  85. package/dist-engine-src/src/session/switch_version.rs +16 -28
  86. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  87. package/dist-engine-src/src/sql2/classify.rs +61 -26
  88. package/dist-engine-src/src/sql2/context.rs +22 -18
  89. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  90. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  91. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  92. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  93. package/dist-engine-src/src/sql2/error.rs +21 -1
  94. package/dist-engine-src/src/sql2/execute.rs +325 -264
  95. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  96. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  97. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  98. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  99. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  100. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  101. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  102. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  103. package/dist-engine-src/src/sql2/mod.rs +6 -3
  104. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  105. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  106. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  107. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  108. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  109. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  110. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  111. package/dist-engine-src/src/sql2/session.rs +7 -10
  112. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  113. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  114. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  115. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  116. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  117. package/dist-engine-src/src/storage_bench.rs +1782 -325
  118. package/dist-engine-src/src/test_support.rs +183 -36
  119. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  120. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  121. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  122. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  123. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  124. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  125. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  126. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  127. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  128. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  129. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  130. package/dist-engine-src/src/transaction/commit.rs +695 -435
  131. package/dist-engine-src/src/transaction/context.rs +551 -310
  132. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  133. package/dist-engine-src/src/transaction/mod.rs +2 -0
  134. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  135. package/dist-engine-src/src/transaction/prep.rs +37 -0
  136. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  137. package/dist-engine-src/src/transaction/staging.rs +701 -406
  138. package/dist-engine-src/src/transaction/types.rs +231 -122
  139. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  140. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  141. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  142. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  143. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  144. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  145. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  146. package/dist-engine-src/src/version/context.rs +1 -13
  147. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  148. package/dist-engine-src/src/version/mod.rs +3 -2
  149. package/dist-engine-src/src/version/refs.rs +12 -103
  150. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  151. package/package.json +1 -1
  152. package/dist-engine-src/src/changelog/codec.rs +0 -321
  153. package/dist-engine-src/src/changelog/context.rs +0 -92
  154. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  155. package/dist-engine-src/src/changelog/mod.rs +0 -13
  156. package/dist-engine-src/src/changelog/reader.rs +0 -20
  157. package/dist-engine-src/src/changelog/storage.rs +0 -220
  158. package/dist-engine-src/src/changelog/types.rs +0 -38
  159. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  160. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  161. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  162. package/dist-engine-src/src/schema_registry.rs +0 -294
  163. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  164. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  165. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,13 +1,17 @@
1
1
  use crate::json_store::compression::{compress_json_payload, decode_json_zstd_payload};
2
2
  use crate::json_store::encoded::{EncodedJson, JsonCodec};
3
- use crate::json_store::types::JsonRef;
3
+ use crate::json_store::types::{JsonReadScopeRef, JsonRef};
4
4
  use crate::storage::{KvGetGroup, KvGetRequest, StorageReader};
5
5
  use crate::LixError;
6
6
  use std::borrow::Cow;
7
+ use std::collections::HashMap;
7
8
 
8
9
  pub(crate) const JSON_NAMESPACE: &str = "json_store.json";
10
+ pub(crate) const JSON_PACK_NAMESPACE: &str = "json_store.pack";
9
11
  const STORED_JSON_MAGIC: &[u8] = b"lix-json:v1";
10
12
  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;
11
15
  const ZSTD_MIN_JSON_BYTES: usize = 16 * 1024;
12
16
  const MIN_ZSTD_SAVINGS_BYTES: usize = 128;
13
17
 
@@ -17,10 +21,40 @@ struct StoredJsonPayload<'a> {
17
21
  data: &'a [u8],
18
22
  }
19
23
 
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
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36
+ enum JsonHashCheck {
37
+ /// Hot reads trust the local storage layer and pack directory. Content
38
+ /// hashes are computed at write time; exhaustive verification belongs in
39
+ /// explicit integrity-check/fsck callers rather than every row scan.
40
+ TrustedHotRead,
41
+ Verify,
42
+ }
43
+
44
+ enum OrderedSinglePackProbe {
45
+ Hit(Vec<Option<Vec<u8>>>),
46
+ MissPresent(Vec<u8>),
47
+ MissAbsent,
48
+ }
49
+
20
50
  fn raw_json_ref_for_content(json: &str) -> JsonRef {
21
51
  JsonRef::from_hash(blake3::hash(json.as_bytes()))
22
52
  }
23
53
 
54
+ pub(crate) fn json_ref_for_content(bytes: &[u8]) -> JsonRef {
55
+ JsonRef::for_content(bytes)
56
+ }
57
+
24
58
  #[cfg(test)]
25
59
  fn encode_json(json: &str) -> Result<EncodedJson<'_>, LixError> {
26
60
  encode_json_for_storage(json)
@@ -57,6 +91,92 @@ fn encode_json_for_storage_with_ref(
57
91
  })
58
92
  }
59
93
 
94
+ pub(crate) fn encode_json_str(json: &str) -> Result<EncodedJson<'_>, LixError> {
95
+ encode_json_for_storage(json)
96
+ }
97
+
98
+ pub(crate) fn encode_json_str_with_ref(
99
+ json: &str,
100
+ json_ref: JsonRef,
101
+ ) -> Result<EncodedJson<'_>, LixError> {
102
+ debug_assert_eq!(JsonRef::for_content(json.as_bytes()), json_ref);
103
+ encode_json_for_storage_with_ref(json, json_ref)
104
+ }
105
+
106
+ pub(crate) fn encode_direct_json_payload(encoded_json: &EncodedJson<'_>) -> Vec<u8> {
107
+ encode_stored_json_payload(encoded_json)
108
+ }
109
+
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
+
60
180
  pub(crate) fn encode_json_bytes_for_storage(bytes: &[u8]) -> Result<(JsonRef, Vec<u8>), LixError> {
61
181
  let json = std::str::from_utf8(bytes).map_err(|error| {
62
182
  LixError::new(
@@ -77,7 +197,7 @@ pub(crate) fn encode_json_str_for_storage_with_ref(
77
197
  Ok((json_ref, encode_stored_json_payload(&encoded_json)))
78
198
  }
79
199
 
80
- pub(crate) async fn load_json_bytes(
200
+ async fn load_json_bytes_direct(
81
201
  store: &mut impl StorageReader,
82
202
  json_ref: &JsonRef,
83
203
  ) -> Result<Option<Vec<u8>>, LixError> {
@@ -97,9 +217,295 @@ pub(crate) async fn load_json_bytes(
97
217
  return Ok(None);
98
218
  };
99
219
  let stored_payload = decode_stored_json_payload(&bytes)?;
100
- decode_json_payload(store, json_ref, stored_payload)
220
+ let _ = store;
221
+ decode_json_payload(json_ref, stored_payload, JsonHashCheck::TrustedHotRead).map(Some)
222
+ }
223
+
224
+ pub(crate) async fn load_json_bytes_many_in_scope(
225
+ store: &mut impl StorageReader,
226
+ json_refs: &[JsonRef],
227
+ scope: JsonReadScopeRef<'_>,
228
+ ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
229
+ load_json_bytes_many_in_scope_with_hash_check(
230
+ store,
231
+ json_refs,
232
+ scope,
233
+ JsonHashCheck::TrustedHotRead,
234
+ )
235
+ .await
236
+ }
237
+
238
+ pub(crate) async fn verify_json_bytes_many_in_scope(
239
+ store: &mut impl StorageReader,
240
+ json_refs: &[JsonRef],
241
+ scope: JsonReadScopeRef<'_>,
242
+ ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
243
+ load_json_bytes_many_in_scope_with_hash_check(store, json_refs, scope, JsonHashCheck::Verify)
101
244
  .await
102
- .map(Some)
245
+ }
246
+
247
+ async fn load_json_bytes_many_in_scope_with_hash_check(
248
+ store: &mut impl StorageReader,
249
+ json_refs: &[JsonRef],
250
+ scope: JsonReadScopeRef<'_>,
251
+ hash_check: JsonHashCheck,
252
+ ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
253
+ if json_refs.is_empty() {
254
+ return Ok(Vec::new());
255
+ }
256
+
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
+ let mut unique_keys = Vec::new();
273
+ let mut unique_refs = Vec::new();
274
+ let mut key_indexes = HashMap::<[u8; 32], usize>::new();
275
+ let mut requested_indexes = Vec::with_capacity(json_refs.len());
276
+ let mut has_duplicate_refs = false;
277
+ for json_ref in json_refs {
278
+ let hash = *json_ref.as_hash_array();
279
+ let index = match key_indexes.get(&hash) {
280
+ Some(index) => {
281
+ has_duplicate_refs = true;
282
+ *index
283
+ }
284
+ None => {
285
+ let index = unique_keys.len();
286
+ key_indexes.insert(hash, index);
287
+ unique_keys.push(hash.to_vec());
288
+ unique_refs.push(*json_ref);
289
+ index
290
+ }
291
+ };
292
+ requested_indexes.push(index);
293
+ }
294
+
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
+ };
315
+
316
+ let missing = unique_values
317
+ .iter()
318
+ .enumerate()
319
+ .filter_map(|(index, value)| value.is_none().then_some(index))
320
+ .collect::<Vec<_>>();
321
+ if missing.is_empty() {
322
+ return Ok(json_values_in_request_order(
323
+ unique_values,
324
+ requested_indexes,
325
+ has_duplicate_refs,
326
+ ));
327
+ }
328
+
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() {
347
+ return Err(LixError::new(
348
+ LixError::CODE_INTERNAL_ERROR,
349
+ format!(
350
+ "json_store batch load returned {} values for {} requested refs",
351
+ group.len(),
352
+ missing.len()
353
+ ),
354
+ ));
355
+ }
356
+
357
+ for (index, stored_bytes) in group.values_iter().enumerate() {
358
+ let unique_index = missing[index];
359
+ let Some(stored_bytes) = stored_bytes else {
360
+ continue;
361
+ };
362
+ let stored_payload = decode_stored_json_payload(stored_bytes)?;
363
+ let _ = store;
364
+ unique_values[unique_index] = Some(decode_json_payload(
365
+ &unique_refs[unique_index],
366
+ stored_payload,
367
+ hash_check,
368
+ )?);
369
+ }
370
+
371
+ Ok(json_values_in_request_order(
372
+ unique_values,
373
+ requested_indexes,
374
+ has_duplicate_refs,
375
+ ))
376
+ }
377
+
378
+ fn json_values_in_request_order(
379
+ unique_values: Vec<Option<Vec<u8>>>,
380
+ requested_indexes: Vec<usize>,
381
+ has_duplicate_refs: bool,
382
+ ) -> Vec<Option<Vec<u8>>> {
383
+ if !has_duplicate_refs {
384
+ debug_assert_eq!(requested_indexes.len(), unique_values.len());
385
+ debug_assert!(requested_indexes
386
+ .iter()
387
+ .copied()
388
+ .enumerate()
389
+ .all(|(request_index, unique_index)| request_index == unique_index));
390
+ return unique_values;
391
+ }
392
+ requested_indexes
393
+ .into_iter()
394
+ .map(|index| unique_values[index].clone())
395
+ .collect()
396
+ }
397
+
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,
444
+ ) -> 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))
472
+ .collect::<Vec<_>>();
473
+ let result = store
474
+ .get_values(KvGetRequest {
475
+ groups: vec![KvGetGroup {
476
+ namespace: JSON_PACK_NAMESPACE.to_string(),
477
+ keys,
478
+ }],
479
+ })
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)
103
509
  }
104
510
 
105
511
  fn encode_stored_json_payload(encoded_json: &EncodedJson<'_>) -> Vec<u8> {
@@ -157,10 +563,10 @@ fn read_json_codec(byte: u8) -> Result<JsonCodec, LixError> {
157
563
  }
158
564
  }
159
565
 
160
- async fn decode_json_payload(
161
- _store: &mut impl StorageReader,
566
+ fn decode_json_payload(
162
567
  json_ref: &JsonRef,
163
568
  stored_payload: StoredJsonPayload<'_>,
569
+ hash_check: JsonHashCheck,
164
570
  ) -> Result<Vec<u8>, LixError> {
165
571
  let data = match stored_payload.codec {
166
572
  JsonCodec::Raw => Ok(stored_payload.data.to_vec()),
@@ -181,14 +587,175 @@ async fn decode_json_payload(
181
587
  ),
182
588
  ));
183
589
  }
184
- let actual_hash = blake3::hash(&data);
185
- if actual_hash.as_bytes() != json_ref.as_hash_bytes() {
590
+ if hash_check == JsonHashCheck::Verify {
591
+ let actual_hash = blake3::hash(&data);
592
+ if actual_hash.as_bytes() != json_ref.as_hash_bytes() {
593
+ return Err(LixError::new(
594
+ "LIX_ERROR_UNKNOWN",
595
+ format!("json ref '{}' hash mismatch", json_ref.to_hex()),
596
+ ));
597
+ }
598
+ }
599
+ Ok(data)
600
+ }
601
+
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 {
186
652
  return Err(LixError::new(
187
653
  "LIX_ERROR_UNKNOWN",
188
- format!("json ref '{}' hash mismatch", json_ref.to_hex()),
654
+ "stored JSON pack is truncated",
189
655
  ));
190
656
  }
191
- Ok(data)
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
+ })
192
759
  }
193
760
 
194
761
  #[cfg(test)]
@@ -227,10 +794,316 @@ mod tests {
227
794
 
228
795
  let mut store = storage.clone();
229
796
  assert_eq!(
230
- load_json_bytes(&mut store, &encoded.json_ref)
797
+ load_json_bytes_direct(&mut store, &encoded.json_ref)
231
798
  .await
232
799
  .expect("json should load"),
233
800
  Some(json.as_bytes().to_vec())
234
801
  );
235
802
  }
803
+
804
+ #[tokio::test]
805
+ async fn json_batch_load_roundtrips_in_request_order() {
806
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
807
+ let first = encode_json("{\"value\":\"first\"}").expect("first json should encode");
808
+ let second = encode_json("{\"value\":\"second\"}").expect("second json should encode");
809
+
810
+ let mut transaction = storage
811
+ .begin_write_transaction()
812
+ .await
813
+ .expect("transaction should open");
814
+ let mut writes = StorageWriteSet::new();
815
+ writes.put(
816
+ JSON_NAMESPACE,
817
+ first.json_ref.as_hash_bytes().to_vec(),
818
+ encode_stored_json_payload(&first),
819
+ );
820
+ writes.put(
821
+ JSON_NAMESPACE,
822
+ second.json_ref.as_hash_bytes().to_vec(),
823
+ encode_stored_json_payload(&second),
824
+ );
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");
833
+
834
+ let mut store = storage.clone();
835
+ let values = load_json_bytes_many_in_scope(
836
+ &mut store,
837
+ &[second.json_ref, first.json_ref, second.json_ref],
838
+ JsonReadScopeRef::OutOfBand,
839
+ )
840
+ .await
841
+ .expect("json batch should load");
842
+
843
+ assert_eq!(
844
+ values,
845
+ vec![
846
+ Some(second.data.as_ref().to_vec()),
847
+ Some(first.data.as_ref().to_vec()),
848
+ Some(second.data.as_ref().to_vec()),
849
+ ]
850
+ );
851
+ }
852
+
853
+ #[tokio::test]
854
+ async fn verified_batch_load_rejects_hash_mismatch() {
855
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
856
+ let requested_ref = JsonRef::for_content(br#"{"value":"requested"}"#);
857
+ let stored = encode_json("{\"value\":\"different\"}").expect("stored json should encode");
858
+
859
+ let mut transaction = storage
860
+ .begin_write_transaction()
861
+ .await
862
+ .expect("transaction should open");
863
+ let mut writes = StorageWriteSet::new();
864
+ writes.put(
865
+ JSON_NAMESPACE,
866
+ requested_ref.as_hash_bytes().to_vec(),
867
+ encode_stored_json_payload(&stored),
868
+ );
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");
886
+ assert_eq!(trusted, vec![Some(stored.data.as_ref().to_vec())]);
887
+
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");
954
+ assert!(
955
+ error.to_string().contains("hash mismatch"),
956
+ "error should mention hash mismatch: {error}"
957
+ );
958
+ }
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
+ }
236
1109
  }