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

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 (223) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +39 -201
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +20 -50
  16. package/SKILL.md +0 -506
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -821
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -26
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -303
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/kv.rs +0 -358
  31. package/dist-engine-src/src/backend/mod.rs +0 -12
  32. package/dist-engine-src/src/backend/testing.rs +0 -658
  33. package/dist-engine-src/src/backend/types.rs +0 -96
  34. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  35. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  36. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  37. package/dist-engine-src/src/binary_cas/kv.rs +0 -1063
  38. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  39. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  40. package/dist-engine-src/src/catalog/context.rs +0 -412
  41. package/dist-engine-src/src/catalog/mod.rs +0 -10
  42. package/dist-engine-src/src/catalog/schema.rs +0 -4
  43. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  44. package/dist-engine-src/src/cel/context.rs +0 -86
  45. package/dist-engine-src/src/cel/error.rs +0 -19
  46. package/dist-engine-src/src/cel/mod.rs +0 -8
  47. package/dist-engine-src/src/cel/provider.rs +0 -9
  48. package/dist-engine-src/src/cel/runtime.rs +0 -167
  49. package/dist-engine-src/src/cel/value.rs +0 -50
  50. package/dist-engine-src/src/commit_graph/context.rs +0 -901
  51. package/dist-engine-src/src/commit_graph/mod.rs +0 -11
  52. package/dist-engine-src/src/commit_graph/types.rs +0 -109
  53. package/dist-engine-src/src/commit_graph/walker.rs +0 -756
  54. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  55. package/dist-engine-src/src/commit_store/context.rs +0 -944
  56. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  57. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  58. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  59. package/dist-engine-src/src/commit_store/types.rs +0 -215
  60. package/dist-engine-src/src/common/error.rs +0 -313
  61. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  62. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  63. package/dist-engine-src/src/common/identity.rs +0 -145
  64. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  65. package/dist-engine-src/src/common/metadata.rs +0 -40
  66. package/dist-engine-src/src/common/mod.rs +0 -23
  67. package/dist-engine-src/src/common/types.rs +0 -105
  68. package/dist-engine-src/src/common/wire.rs +0 -222
  69. package/dist-engine-src/src/domain.rs +0 -324
  70. package/dist-engine-src/src/engine.rs +0 -225
  71. package/dist-engine-src/src/entity_identity.rs +0 -405
  72. package/dist-engine-src/src/functions/context.rs +0 -292
  73. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  74. package/dist-engine-src/src/functions/mod.rs +0 -18
  75. package/dist-engine-src/src/functions/provider.rs +0 -130
  76. package/dist-engine-src/src/functions/state.rs +0 -336
  77. package/dist-engine-src/src/functions/types.rs +0 -37
  78. package/dist-engine-src/src/init.rs +0 -558
  79. package/dist-engine-src/src/json_store/compression.rs +0 -77
  80. package/dist-engine-src/src/json_store/context.rs +0 -423
  81. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  82. package/dist-engine-src/src/json_store/mod.rs +0 -12
  83. package/dist-engine-src/src/json_store/store.rs +0 -1109
  84. package/dist-engine-src/src/json_store/types.rs +0 -217
  85. package/dist-engine-src/src/lib.rs +0 -62
  86. package/dist-engine-src/src/live_state/context.rs +0 -2019
  87. package/dist-engine-src/src/live_state/mod.rs +0 -15
  88. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  89. package/dist-engine-src/src/live_state/reader.rs +0 -23
  90. package/dist-engine-src/src/live_state/types.rs +0 -222
  91. package/dist-engine-src/src/live_state/visibility.rs +0 -223
  92. package/dist-engine-src/src/plugin/archive.rs +0 -438
  93. package/dist-engine-src/src/plugin/component.rs +0 -183
  94. package/dist-engine-src/src/plugin/install.rs +0 -619
  95. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  96. package/dist-engine-src/src/plugin/materializer.rs +0 -477
  97. package/dist-engine-src/src/plugin/mod.rs +0 -33
  98. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -118
  99. package/dist-engine-src/src/plugin/storage.rs +0 -74
  100. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  101. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  102. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  103. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  104. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  105. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  106. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  107. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  108. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  109. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  110. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  111. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  112. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  113. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  114. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  115. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  116. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  117. package/dist-engine-src/src/schema/builtin/mod.rs +0 -222
  118. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  119. package/dist-engine-src/src/schema/definition.json +0 -187
  120. package/dist-engine-src/src/schema/definition.rs +0 -742
  121. package/dist-engine-src/src/schema/key.rs +0 -138
  122. package/dist-engine-src/src/schema/mod.rs +0 -20
  123. package/dist-engine-src/src/schema/seed.rs +0 -14
  124. package/dist-engine-src/src/schema/tests.rs +0 -780
  125. package/dist-engine-src/src/session/context.rs +0 -404
  126. package/dist-engine-src/src/session/create_version.rs +0 -88
  127. package/dist-engine-src/src/session/execute.rs +0 -541
  128. package/dist-engine-src/src/session/merge/analysis.rs +0 -102
  129. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  130. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  131. package/dist-engine-src/src/session/merge/mod.rs +0 -11
  132. package/dist-engine-src/src/session/merge/stats.rs +0 -65
  133. package/dist-engine-src/src/session/merge/version.rs +0 -427
  134. package/dist-engine-src/src/session/mod.rs +0 -27
  135. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  136. package/dist-engine-src/src/session/switch_version.rs +0 -110
  137. package/dist-engine-src/src/session/transaction.rs +0 -76
  138. package/dist-engine-src/src/sql2/change_provider.rs +0 -331
  139. package/dist-engine-src/src/sql2/classify.rs +0 -174
  140. package/dist-engine-src/src/sql2/context.rs +0 -311
  141. package/dist-engine-src/src/sql2/directory_history_provider.rs +0 -631
  142. package/dist-engine-src/src/sql2/directory_provider.rs +0 -2453
  143. package/dist-engine-src/src/sql2/dml.rs +0 -148
  144. package/dist-engine-src/src/sql2/entity_history_provider.rs +0 -440
  145. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  146. package/dist-engine-src/src/sql2/error.rs +0 -215
  147. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  148. package/dist-engine-src/src/sql2/file_history_provider.rs +0 -910
  149. package/dist-engine-src/src/sql2/file_provider.rs +0 -3679
  150. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1490
  151. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  152. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  153. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  154. package/dist-engine-src/src/sql2/history_provider.rs +0 -412
  155. package/dist-engine-src/src/sql2/history_route.rs +0 -657
  156. package/dist-engine-src/src/sql2/lix_state_provider.rs +0 -2512
  157. package/dist-engine-src/src/sql2/mod.rs +0 -47
  158. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -246
  159. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  160. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  161. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  162. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  163. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  164. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  165. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  166. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  167. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  168. package/dist-engine-src/src/sql2/session.rs +0 -132
  169. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  170. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +0 -53
  171. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  172. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  173. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  174. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  175. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  176. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  177. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  178. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  179. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -89
  180. package/dist-engine-src/src/sql2/udfs/public_call.rs +0 -238
  181. package/dist-engine-src/src/sql2/version_provider.rs +0 -1202
  182. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  183. package/dist-engine-src/src/sql2/write_normalization.rs +0 -345
  184. package/dist-engine-src/src/storage/context.rs +0 -356
  185. package/dist-engine-src/src/storage/mod.rs +0 -14
  186. package/dist-engine-src/src/storage/read_scope.rs +0 -88
  187. package/dist-engine-src/src/storage/types.rs +0 -501
  188. package/dist-engine-src/src/storage_bench.rs +0 -4863
  189. package/dist-engine-src/src/test_support.rs +0 -228
  190. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  191. package/dist-engine-src/src/tracked_state/codec.rs +0 -2085
  192. package/dist-engine-src/src/tracked_state/context.rs +0 -1867
  193. package/dist-engine-src/src/tracked_state/diff.rs +0 -686
  194. package/dist-engine-src/src/tracked_state/materialization.rs +0 -403
  195. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  196. package/dist-engine-src/src/tracked_state/merge.rs +0 -492
  197. package/dist-engine-src/src/tracked_state/mod.rs +0 -32
  198. package/dist-engine-src/src/tracked_state/storage.rs +0 -375
  199. package/dist-engine-src/src/tracked_state/tree.rs +0 -3187
  200. package/dist-engine-src/src/tracked_state/types.rs +0 -231
  201. package/dist-engine-src/src/transaction/commit.rs +0 -1484
  202. package/dist-engine-src/src/transaction/context.rs +0 -1548
  203. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  204. package/dist-engine-src/src/transaction/mod.rs +0 -13
  205. package/dist-engine-src/src/transaction/normalization.rs +0 -890
  206. package/dist-engine-src/src/transaction/prep.rs +0 -37
  207. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -149
  208. package/dist-engine-src/src/transaction/staging.rs +0 -1731
  209. package/dist-engine-src/src/transaction/types.rs +0 -460
  210. package/dist-engine-src/src/transaction/validation.rs +0 -5830
  211. package/dist-engine-src/src/untracked_state/codec.rs +0 -307
  212. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  213. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  214. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  215. package/dist-engine-src/src/untracked_state/storage.rs +0 -396
  216. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  217. package/dist-engine-src/src/version/context.rs +0 -40
  218. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  219. package/dist-engine-src/src/version/mod.rs +0 -13
  220. package/dist-engine-src/src/version/refs.rs +0 -330
  221. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  222. package/dist-engine-src/src/version/types.rs +0 -21
  223. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,1109 +0,0 @@
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
- }