@lix-js/sdk 0.6.0-preview.1 → 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 (205) hide show
  1. package/SKILL.md +304 -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/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +103 -14
  9. package/dist/open-lix.js +3 -0
  10. package/dist/sqlite/index.js +99 -22
  11. package/dist-engine-src/README.md +18 -0
  12. package/dist-engine-src/src/backend/kv.rs +358 -0
  13. package/dist-engine-src/src/backend/mod.rs +12 -0
  14. package/dist-engine-src/src/backend/testing.rs +658 -0
  15. package/dist-engine-src/src/backend/types.rs +96 -0
  16. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  17. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  18. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  19. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  20. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  21. package/dist-engine-src/src/binary_cas/types.rs +121 -0
  22. package/dist-engine-src/src/catalog/context.rs +412 -0
  23. package/dist-engine-src/src/catalog/mod.rs +10 -0
  24. package/dist-engine-src/src/catalog/schema.rs +4 -0
  25. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  26. package/dist-engine-src/src/cel/context.rs +86 -0
  27. package/dist-engine-src/src/cel/error.rs +19 -0
  28. package/dist-engine-src/src/cel/mod.rs +8 -0
  29. package/dist-engine-src/src/cel/provider.rs +9 -0
  30. package/dist-engine-src/src/cel/runtime.rs +167 -0
  31. package/dist-engine-src/src/cel/value.rs +50 -0
  32. package/dist-engine-src/src/commit_graph/context.rs +901 -0
  33. package/dist-engine-src/src/commit_graph/mod.rs +11 -0
  34. package/dist-engine-src/src/commit_graph/types.rs +109 -0
  35. package/dist-engine-src/src/commit_graph/walker.rs +756 -0
  36. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  37. package/dist-engine-src/src/commit_store/context.rs +944 -0
  38. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  39. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  40. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  41. package/dist-engine-src/src/commit_store/types.rs +215 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +145 -0
  46. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  47. package/dist-engine-src/src/common/metadata.rs +40 -0
  48. package/dist-engine-src/src/common/mod.rs +23 -0
  49. package/dist-engine-src/src/common/types.rs +105 -0
  50. package/dist-engine-src/src/common/wire.rs +222 -0
  51. package/dist-engine-src/src/domain.rs +324 -0
  52. package/dist-engine-src/src/engine.rs +225 -0
  53. package/dist-engine-src/src/entity_identity.rs +405 -0
  54. package/dist-engine-src/src/functions/context.rs +292 -0
  55. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  56. package/dist-engine-src/src/functions/mod.rs +18 -0
  57. package/dist-engine-src/src/functions/provider.rs +130 -0
  58. package/dist-engine-src/src/functions/state.rs +336 -0
  59. package/dist-engine-src/src/functions/types.rs +37 -0
  60. package/dist-engine-src/src/init.rs +558 -0
  61. package/dist-engine-src/src/json_store/compression.rs +77 -0
  62. package/dist-engine-src/src/json_store/context.rs +423 -0
  63. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  64. package/dist-engine-src/src/json_store/mod.rs +12 -0
  65. package/dist-engine-src/src/json_store/store.rs +1109 -0
  66. package/dist-engine-src/src/json_store/types.rs +217 -0
  67. package/dist-engine-src/src/lib.rs +62 -0
  68. package/dist-engine-src/src/live_state/context.rs +2019 -0
  69. package/dist-engine-src/src/live_state/mod.rs +15 -0
  70. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  71. package/dist-engine-src/src/live_state/reader.rs +23 -0
  72. package/dist-engine-src/src/live_state/types.rs +222 -0
  73. package/dist-engine-src/src/live_state/visibility.rs +223 -0
  74. package/dist-engine-src/src/plugin/archive.rs +438 -0
  75. package/dist-engine-src/src/plugin/component.rs +183 -0
  76. package/dist-engine-src/src/plugin/install.rs +619 -0
  77. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  78. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  79. package/dist-engine-src/src/plugin/mod.rs +33 -0
  80. package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
  81. package/dist-engine-src/src/plugin/storage.rs +74 -0
  82. package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
  83. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  84. package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
  85. package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
  86. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
  92. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
  93. package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
  94. package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
  100. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  101. package/dist-engine-src/src/schema/definition.json +187 -0
  102. package/dist-engine-src/src/schema/definition.rs +742 -0
  103. package/dist-engine-src/src/schema/key.rs +138 -0
  104. package/dist-engine-src/src/schema/mod.rs +20 -0
  105. package/dist-engine-src/src/schema/seed.rs +14 -0
  106. package/dist-engine-src/src/schema/tests.rs +780 -0
  107. package/dist-engine-src/src/session/context.rs +364 -0
  108. package/dist-engine-src/src/session/create_version.rs +88 -0
  109. package/dist-engine-src/src/session/execute.rs +478 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +427 -0
  116. package/dist-engine-src/src/session/mod.rs +27 -0
  117. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  118. package/dist-engine-src/src/session/switch_version.rs +109 -0
  119. package/dist-engine-src/src/sql2/change_provider.rs +331 -0
  120. package/dist-engine-src/src/sql2/classify.rs +182 -0
  121. package/dist-engine-src/src/sql2/context.rs +311 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
  127. package/dist-engine-src/src/sql2/error.rs +216 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3440 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +56 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +412 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +657 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
  138. package/dist-engine-src/src/sql2/mod.rs +46 -0
  139. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  140. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  141. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  142. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  143. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  144. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  145. package/dist-engine-src/src/sql2/read_only.rs +63 -0
  146. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  147. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  148. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  149. package/dist-engine-src/src/sql2/session.rs +132 -0
  150. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  153. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  154. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  155. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  156. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  157. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  158. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  159. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  160. package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
  161. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  162. package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
  163. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  164. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  165. package/dist-engine-src/src/storage/context.rs +356 -0
  166. package/dist-engine-src/src/storage/mod.rs +14 -0
  167. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  168. package/dist-engine-src/src/storage/types.rs +501 -0
  169. package/dist-engine-src/src/storage_bench.rs +4863 -0
  170. package/dist-engine-src/src/test_support.rs +228 -0
  171. package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
  172. package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
  173. package/dist-engine-src/src/tracked_state/context.rs +1867 -0
  174. package/dist-engine-src/src/tracked_state/diff.rs +686 -0
  175. package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
  176. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  177. package/dist-engine-src/src/tracked_state/merge.rs +492 -0
  178. package/dist-engine-src/src/tracked_state/mod.rs +32 -0
  179. package/dist-engine-src/src/tracked_state/storage.rs +375 -0
  180. package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
  181. package/dist-engine-src/src/tracked_state/types.rs +231 -0
  182. package/dist-engine-src/src/transaction/commit.rs +1484 -0
  183. package/dist-engine-src/src/transaction/context.rs +1548 -0
  184. package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
  185. package/dist-engine-src/src/transaction/mod.rs +13 -0
  186. package/dist-engine-src/src/transaction/normalization.rs +890 -0
  187. package/dist-engine-src/src/transaction/prep.rs +37 -0
  188. package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
  189. package/dist-engine-src/src/transaction/staging.rs +1731 -0
  190. package/dist-engine-src/src/transaction/types.rs +460 -0
  191. package/dist-engine-src/src/transaction/validation.rs +5830 -0
  192. package/dist-engine-src/src/untracked_state/codec.rs +307 -0
  193. package/dist-engine-src/src/untracked_state/context.rs +98 -0
  194. package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
  195. package/dist-engine-src/src/untracked_state/mod.rs +15 -0
  196. package/dist-engine-src/src/untracked_state/storage.rs +396 -0
  197. package/dist-engine-src/src/untracked_state/types.rs +146 -0
  198. package/dist-engine-src/src/version/context.rs +40 -0
  199. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  200. package/dist-engine-src/src/version/mod.rs +13 -0
  201. package/dist-engine-src/src/version/refs.rs +330 -0
  202. package/dist-engine-src/src/version/stage_rows.rs +67 -0
  203. package/dist-engine-src/src/version/types.rs +21 -0
  204. package/dist-engine-src/src/wasm/mod.rs +60 -0
  205. package/package.json +68 -64
@@ -0,0 +1,1109 @@
1
+ use crate::json_store::compression::{compress_json_payload, decode_json_zstd_payload};
2
+ use crate::json_store::encoded::{EncodedJson, JsonCodec};
3
+ use crate::json_store::types::{JsonReadScopeRef, JsonRef};
4
+ use crate::storage::{KvGetGroup, KvGetRequest, StorageReader};
5
+ use crate::LixError;
6
+ use std::borrow::Cow;
7
+ use std::collections::HashMap;
8
+
9
+ pub(crate) const JSON_NAMESPACE: &str = "json_store.json";
10
+ pub(crate) const JSON_PACK_NAMESPACE: &str = "json_store.pack";
11
+ const STORED_JSON_MAGIC: &[u8] = b"lix-json:v1";
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;
15
+ const ZSTD_MIN_JSON_BYTES: usize = 16 * 1024;
16
+ const MIN_ZSTD_SAVINGS_BYTES: usize = 128;
17
+
18
+ struct StoredJsonPayload<'a> {
19
+ codec: JsonCodec,
20
+ uncompressed_len: usize,
21
+ data: &'a [u8],
22
+ }
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
+
50
+ fn raw_json_ref_for_content(json: &str) -> JsonRef {
51
+ JsonRef::from_hash(blake3::hash(json.as_bytes()))
52
+ }
53
+
54
+ pub(crate) fn json_ref_for_content(bytes: &[u8]) -> JsonRef {
55
+ JsonRef::for_content(bytes)
56
+ }
57
+
58
+ #[cfg(test)]
59
+ fn encode_json(json: &str) -> Result<EncodedJson<'_>, LixError> {
60
+ encode_json_for_storage(json)
61
+ }
62
+
63
+ fn encode_json_for_storage(json: &str) -> Result<EncodedJson<'_>, LixError> {
64
+ let raw_ref = raw_json_ref_for_content(json);
65
+ encode_json_for_storage_with_ref(json, raw_ref)
66
+ }
67
+
68
+ fn encode_json_for_storage_with_ref(
69
+ json: &str,
70
+ raw_ref: JsonRef,
71
+ ) -> Result<EncodedJson<'_>, LixError> {
72
+ let raw_data = json.as_bytes();
73
+
74
+ if raw_data.len() >= ZSTD_MIN_JSON_BYTES {
75
+ let compressed = compress_json_payload(raw_data)?;
76
+ if raw_data.len().saturating_sub(compressed.len()) >= MIN_ZSTD_SAVINGS_BYTES {
77
+ return Ok(EncodedJson {
78
+ json_ref: raw_ref,
79
+ codec: JsonCodec::Zstd,
80
+ uncompressed_len: json.len(),
81
+ data: Cow::Owned(compressed),
82
+ });
83
+ }
84
+ }
85
+
86
+ Ok(EncodedJson {
87
+ json_ref: raw_ref,
88
+ codec: JsonCodec::Raw,
89
+ uncompressed_len: json.len(),
90
+ data: Cow::Borrowed(raw_data),
91
+ })
92
+ }
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
+
180
+ pub(crate) fn encode_json_bytes_for_storage(bytes: &[u8]) -> Result<(JsonRef, Vec<u8>), LixError> {
181
+ let json = std::str::from_utf8(bytes).map_err(|error| {
182
+ LixError::new(
183
+ "LIX_ERROR_UNKNOWN",
184
+ format!("json bytes are invalid UTF-8: {error}"),
185
+ )
186
+ })?;
187
+ let json_ref = JsonRef::from_hash(blake3::hash(bytes));
188
+ encode_json_str_for_storage_with_ref(json, json_ref)
189
+ }
190
+
191
+ pub(crate) fn encode_json_str_for_storage_with_ref(
192
+ json: &str,
193
+ json_ref: JsonRef,
194
+ ) -> Result<(JsonRef, Vec<u8>), LixError> {
195
+ let encoded_json = encode_json_for_storage_with_ref(json, json_ref)?;
196
+ let json_ref = encoded_json.json_ref.clone();
197
+ Ok((json_ref, encode_stored_json_payload(&encoded_json)))
198
+ }
199
+
200
+ async fn load_json_bytes_direct(
201
+ store: &mut impl StorageReader,
202
+ json_ref: &JsonRef,
203
+ ) -> 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
213
+ .into_iter()
214
+ .next()
215
+ .and_then(|group| group.single_value_owned());
216
+ let Some(bytes) = result else {
217
+ return Ok(None);
218
+ };
219
+ let stored_payload = decode_stored_json_payload(&bytes)?;
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)
244
+ .await
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)
509
+ }
510
+
511
+ fn encode_stored_json_payload(encoded_json: &EncodedJson<'_>) -> Vec<u8> {
512
+ let mut out = Vec::with_capacity(STORED_JSON_HEADER_LEN + encoded_json.data.as_ref().len());
513
+ out.extend_from_slice(STORED_JSON_MAGIC);
514
+ out.push(json_codec_byte(encoded_json.codec));
515
+ out.extend_from_slice(&(encoded_json.uncompressed_len as u64).to_be_bytes());
516
+ out.extend_from_slice(encoded_json.data.as_ref());
517
+ out
518
+ }
519
+
520
+ fn decode_stored_json_payload(bytes: &[u8]) -> Result<StoredJsonPayload<'_>, LixError> {
521
+ if bytes.len() < STORED_JSON_HEADER_LEN {
522
+ return Err(LixError::new(
523
+ "LIX_ERROR_UNKNOWN",
524
+ "stored JSON payload is truncated",
525
+ ));
526
+ }
527
+ if &bytes[..STORED_JSON_MAGIC.len()] != STORED_JSON_MAGIC {
528
+ return Err(LixError::new(
529
+ "LIX_ERROR_UNKNOWN",
530
+ "stored JSON payload has invalid header",
531
+ ));
532
+ }
533
+ let codec = read_json_codec(bytes[STORED_JSON_MAGIC.len()])?;
534
+ let len_start = STORED_JSON_MAGIC.len() + 1;
535
+ let len_end = len_start + 8;
536
+ let uncompressed_len = u64::from_be_bytes(
537
+ bytes[len_start..len_end]
538
+ .try_into()
539
+ .expect("stored JSON length header is fixed size"),
540
+ ) as usize;
541
+ Ok(StoredJsonPayload {
542
+ codec,
543
+ uncompressed_len,
544
+ data: &bytes[len_end..],
545
+ })
546
+ }
547
+
548
+ fn json_codec_byte(codec: JsonCodec) -> u8 {
549
+ match codec {
550
+ JsonCodec::Raw => 0,
551
+ JsonCodec::Zstd => 1,
552
+ }
553
+ }
554
+
555
+ fn read_json_codec(byte: u8) -> Result<JsonCodec, LixError> {
556
+ match byte {
557
+ 0 => Ok(JsonCodec::Raw),
558
+ 1 => Ok(JsonCodec::Zstd),
559
+ _ => Err(LixError::new(
560
+ "LIX_ERROR_UNKNOWN",
561
+ format!("stored JSON payload has unknown codec byte {byte}"),
562
+ )),
563
+ }
564
+ }
565
+
566
+ fn decode_json_payload(
567
+ json_ref: &JsonRef,
568
+ stored_payload: StoredJsonPayload<'_>,
569
+ hash_check: JsonHashCheck,
570
+ ) -> Result<Vec<u8>, LixError> {
571
+ let data = match stored_payload.codec {
572
+ JsonCodec::Raw => Ok(stored_payload.data.to_vec()),
573
+ JsonCodec::Zstd => decode_json_zstd_payload(
574
+ stored_payload.data,
575
+ stored_payload.uncompressed_len,
576
+ &json_ref.to_hex(),
577
+ ),
578
+ }?;
579
+ if data.len() != stored_payload.uncompressed_len {
580
+ return Err(LixError::new(
581
+ "LIX_ERROR_UNKNOWN",
582
+ format!(
583
+ "json ref '{}' decoded to {} bytes, expected {}",
584
+ json_ref.to_hex(),
585
+ data.len(),
586
+ stored_payload.uncompressed_len
587
+ ),
588
+ ));
589
+ }
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 {
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
+ #[cfg(test)]
762
+ mod tests {
763
+ use std::sync::Arc;
764
+
765
+ use super::*;
766
+ use crate::backend::testing::UnitTestBackend;
767
+ use crate::storage::{StorageContext, StorageWriteSet};
768
+
769
+ #[tokio::test]
770
+ async fn json_roundtrips_raw_payload() {
771
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
772
+ let json = "{\"value\":\"small\"}";
773
+ let encoded = encode_json(json).expect("json should encode");
774
+ assert_eq!(encoded.codec, JsonCodec::Raw);
775
+
776
+ let mut transaction = storage
777
+ .begin_write_transaction()
778
+ .await
779
+ .expect("transaction should open");
780
+ let mut writes = StorageWriteSet::new();
781
+ writes.put(
782
+ JSON_NAMESPACE,
783
+ encoded.json_ref.as_hash_bytes().to_vec(),
784
+ encode_stored_json_payload(&encoded),
785
+ );
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");
794
+
795
+ let mut store = storage.clone();
796
+ assert_eq!(
797
+ load_json_bytes_direct(&mut store, &encoded.json_ref)
798
+ .await
799
+ .expect("json should load"),
800
+ Some(json.as_bytes().to_vec())
801
+ );
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
+ }
1109
+ }