@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.
- package/SKILL.md +304 -320
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
- package/dist/engine-wasm/wasm/lix_engine.js +9 -13
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +103 -14
- package/dist/open-lix.js +3 -0
- package/dist/sqlite/index.js +99 -22
- package/dist-engine-src/README.md +18 -0
- package/dist-engine-src/src/backend/kv.rs +358 -0
- package/dist-engine-src/src/backend/mod.rs +12 -0
- package/dist-engine-src/src/backend/testing.rs +658 -0
- package/dist-engine-src/src/backend/types.rs +96 -0
- package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
- package/dist-engine-src/src/binary_cas/codec.rs +346 -0
- package/dist-engine-src/src/binary_cas/context.rs +139 -0
- package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
- package/dist-engine-src/src/binary_cas/mod.rs +11 -0
- package/dist-engine-src/src/binary_cas/types.rs +121 -0
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/context.rs +86 -0
- package/dist-engine-src/src/cel/error.rs +19 -0
- package/dist-engine-src/src/cel/mod.rs +8 -0
- package/dist-engine-src/src/cel/provider.rs +9 -0
- package/dist-engine-src/src/cel/runtime.rs +167 -0
- package/dist-engine-src/src/cel/value.rs +50 -0
- package/dist-engine-src/src/commit_graph/context.rs +901 -0
- package/dist-engine-src/src/commit_graph/mod.rs +11 -0
- package/dist-engine-src/src/commit_graph/types.rs +109 -0
- package/dist-engine-src/src/commit_graph/walker.rs +756 -0
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/error.rs +313 -0
- package/dist-engine-src/src/common/fingerprint.rs +3 -0
- package/dist-engine-src/src/common/fs_path.rs +1336 -0
- package/dist-engine-src/src/common/identity.rs +145 -0
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +40 -0
- package/dist-engine-src/src/common/mod.rs +23 -0
- package/dist-engine-src/src/common/types.rs +105 -0
- package/dist-engine-src/src/common/wire.rs +222 -0
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +225 -0
- package/dist-engine-src/src/entity_identity.rs +405 -0
- package/dist-engine-src/src/functions/context.rs +292 -0
- package/dist-engine-src/src/functions/deterministic.rs +113 -0
- package/dist-engine-src/src/functions/mod.rs +18 -0
- package/dist-engine-src/src/functions/provider.rs +130 -0
- package/dist-engine-src/src/functions/state.rs +336 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +558 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +423 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +12 -0
- package/dist-engine-src/src/json_store/store.rs +1109 -0
- package/dist-engine-src/src/json_store/types.rs +217 -0
- package/dist-engine-src/src/lib.rs +62 -0
- package/dist-engine-src/src/live_state/context.rs +2019 -0
- package/dist-engine-src/src/live_state/mod.rs +15 -0
- package/dist-engine-src/src/live_state/overlay.rs +75 -0
- package/dist-engine-src/src/live_state/reader.rs +23 -0
- package/dist-engine-src/src/live_state/types.rs +222 -0
- package/dist-engine-src/src/live_state/visibility.rs +223 -0
- package/dist-engine-src/src/plugin/archive.rs +438 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +619 -0
- package/dist-engine-src/src/plugin/manifest.rs +516 -0
- package/dist-engine-src/src/plugin/materializer.rs +477 -0
- package/dist-engine-src/src/plugin/mod.rs +33 -0
- package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +187 -0
- package/dist-engine-src/src/schema/definition.rs +742 -0
- package/dist-engine-src/src/schema/key.rs +138 -0
- package/dist-engine-src/src/schema/mod.rs +20 -0
- package/dist-engine-src/src/schema/seed.rs +14 -0
- package/dist-engine-src/src/schema/tests.rs +780 -0
- package/dist-engine-src/src/session/context.rs +364 -0
- package/dist-engine-src/src/session/create_version.rs +88 -0
- package/dist-engine-src/src/session/execute.rs +478 -0
- package/dist-engine-src/src/session/merge/analysis.rs +102 -0
- package/dist-engine-src/src/session/merge/apply.rs +23 -0
- package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
- package/dist-engine-src/src/session/merge/mod.rs +11 -0
- package/dist-engine-src/src/session/merge/stats.rs +65 -0
- package/dist-engine-src/src/session/merge/version.rs +427 -0
- package/dist-engine-src/src/session/mod.rs +27 -0
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +109 -0
- package/dist-engine-src/src/sql2/change_provider.rs +331 -0
- package/dist-engine-src/src/sql2/classify.rs +182 -0
- package/dist-engine-src/src/sql2/context.rs +311 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
- package/dist-engine-src/src/sql2/error.rs +216 -0
- package/dist-engine-src/src/sql2/execute.rs +3440 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
- package/dist-engine-src/src/sql2/history_projection.rs +56 -0
- package/dist-engine-src/src/sql2/history_provider.rs +412 -0
- package/dist-engine-src/src/sql2/history_route.rs +657 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
- package/dist-engine-src/src/sql2/mod.rs +46 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +63 -0
- package/dist-engine-src/src/sql2/record_batch.rs +17 -0
- package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
- package/dist-engine-src/src/sql2/runtime.rs +60 -0
- package/dist-engine-src/src/sql2/session.rs +132 -0
- package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
- package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
- package/dist-engine-src/src/sql2/version_scope.rs +394 -0
- package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
- package/dist-engine-src/src/storage/context.rs +356 -0
- package/dist-engine-src/src/storage/mod.rs +14 -0
- package/dist-engine-src/src/storage/read_scope.rs +88 -0
- package/dist-engine-src/src/storage/types.rs +501 -0
- package/dist-engine-src/src/storage_bench.rs +4863 -0
- package/dist-engine-src/src/test_support.rs +228 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
- package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
- package/dist-engine-src/src/tracked_state/context.rs +1867 -0
- package/dist-engine-src/src/tracked_state/diff.rs +686 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +492 -0
- package/dist-engine-src/src/tracked_state/mod.rs +32 -0
- package/dist-engine-src/src/tracked_state/storage.rs +375 -0
- package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
- package/dist-engine-src/src/tracked_state/types.rs +231 -0
- package/dist-engine-src/src/transaction/commit.rs +1484 -0
- package/dist-engine-src/src/transaction/context.rs +1548 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
- package/dist-engine-src/src/transaction/mod.rs +13 -0
- package/dist-engine-src/src/transaction/normalization.rs +890 -0
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
- package/dist-engine-src/src/transaction/staging.rs +1731 -0
- package/dist-engine-src/src/transaction/types.rs +460 -0
- package/dist-engine-src/src/transaction/validation.rs +5830 -0
- package/dist-engine-src/src/untracked_state/codec.rs +307 -0
- package/dist-engine-src/src/untracked_state/context.rs +98 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
- package/dist-engine-src/src/untracked_state/mod.rs +15 -0
- package/dist-engine-src/src/untracked_state/storage.rs +396 -0
- package/dist-engine-src/src/untracked_state/types.rs +146 -0
- package/dist-engine-src/src/version/context.rs +40 -0
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +13 -0
- package/dist-engine-src/src/version/refs.rs +330 -0
- package/dist-engine-src/src/version/stage_rows.rs +67 -0
- package/dist-engine-src/src/version/types.rs +21 -0
- package/dist-engine-src/src/wasm/mod.rs +60 -0
- 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
|
+
}
|