@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +4 -5
- package/dist/engine-wasm/wasm/lix_engine.js +1 -1
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +1 -1
- package/dist-engine-src/src/binary_cas/types.rs +0 -6
- 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/mod.rs +1 -1
- package/dist-engine-src/src/cel/provider.rs +1 -1
- package/dist-engine-src/src/commit_graph/context.rs +328 -1015
- package/dist-engine-src/src/commit_graph/mod.rs +2 -3
- package/dist-engine-src/src/commit_graph/types.rs +7 -43
- package/dist-engine-src/src/commit_graph/walker.rs +57 -81
- 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/identity.rs +15 -5
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +17 -12
- package/dist-engine-src/src/common/mod.rs +5 -5
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +29 -43
- package/dist-engine-src/src/entity_identity.rs +238 -118
- package/dist-engine-src/src/functions/context.rs +17 -52
- package/dist-engine-src/src/functions/deterministic.rs +1 -1
- package/dist-engine-src/src/functions/mod.rs +1 -1
- package/dist-engine-src/src/functions/provider.rs +4 -4
- package/dist-engine-src/src/functions/state.rs +39 -66
- package/dist-engine-src/src/functions/types.rs +1 -1
- package/dist-engine-src/src/init.rs +204 -151
- package/dist-engine-src/src/json_store/context.rs +354 -60
- package/dist-engine-src/src/json_store/encoded.rs +6 -6
- package/dist-engine-src/src/json_store/mod.rs +4 -1
- package/dist-engine-src/src/json_store/store.rs +884 -11
- package/dist-engine-src/src/json_store/types.rs +166 -1
- package/dist-engine-src/src/lib.rs +10 -9
- package/dist-engine-src/src/live_state/context.rs +608 -830
- package/dist-engine-src/src/live_state/mod.rs +3 -3
- package/dist-engine-src/src/live_state/overlay.rs +7 -7
- package/dist-engine-src/src/live_state/reader.rs +5 -5
- package/dist-engine-src/src/live_state/types.rs +19 -36
- package/dist-engine-src/src/live_state/visibility.rs +19 -14
- package/dist-engine-src/src/plugin/archive.rs +3 -6
- package/dist-engine-src/src/plugin/install.rs +0 -18
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
- package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +47 -17
- package/dist-engine-src/src/schema/definition.rs +202 -96
- package/dist-engine-src/src/schema/key.rs +9 -77
- package/dist-engine-src/src/schema/mod.rs +4 -4
- package/dist-engine-src/src/schema/tests.rs +133 -92
- package/dist-engine-src/src/session/context.rs +40 -42
- package/dist-engine-src/src/session/create_version.rs +22 -14
- package/dist-engine-src/src/session/execute.rs +45 -14
- package/dist-engine-src/src/session/merge/apply.rs +4 -4
- package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
- package/dist-engine-src/src/session/merge/stats.rs +1 -1
- package/dist-engine-src/src/session/merge/version.rs +35 -45
- package/dist-engine-src/src/session/mod.rs +4 -2
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +16 -28
- package/dist-engine-src/src/sql2/change_provider.rs +14 -20
- package/dist-engine-src/src/sql2/classify.rs +61 -26
- package/dist-engine-src/src/sql2/context.rs +22 -18
- package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
- package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
- package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
- package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
- package/dist-engine-src/src/sql2/error.rs +21 -1
- package/dist-engine-src/src/sql2/execute.rs +325 -264
- package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
- package/dist-engine-src/src/sql2/file_provider.rs +533 -108
- package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
- package/dist-engine-src/src/sql2/history_projection.rs +3 -27
- package/dist-engine-src/src/sql2/history_provider.rs +11 -17
- package/dist-engine-src/src/sql2/history_route.rs +22 -8
- package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
- package/dist-engine-src/src/sql2/mod.rs +6 -3
- 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 +10 -12
- package/dist-engine-src/src/sql2/session.rs +7 -10
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +46 -31
- package/dist-engine-src/src/sql2/version_scope.rs +4 -4
- package/dist-engine-src/src/storage_bench.rs +1782 -325
- package/dist-engine-src/src/test_support.rs +183 -36
- package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
- package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
- package/dist-engine-src/src/tracked_state/context.rs +1155 -271
- package/dist-engine-src/src/tracked_state/diff.rs +249 -57
- package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +37 -19
- package/dist-engine-src/src/tracked_state/mod.rs +8 -7
- package/dist-engine-src/src/tracked_state/storage.rs +138 -6
- package/dist-engine-src/src/tracked_state/tree.rs +695 -252
- package/dist-engine-src/src/tracked_state/types.rs +176 -6
- package/dist-engine-src/src/transaction/commit.rs +695 -435
- package/dist-engine-src/src/transaction/context.rs +551 -310
- package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
- package/dist-engine-src/src/transaction/mod.rs +2 -0
- package/dist-engine-src/src/transaction/normalization.rs +311 -447
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
- package/dist-engine-src/src/transaction/staging.rs +701 -406
- package/dist-engine-src/src/transaction/types.rs +231 -122
- package/dist-engine-src/src/transaction/validation.rs +2717 -1698
- package/dist-engine-src/src/untracked_state/codec.rs +40 -96
- package/dist-engine-src/src/untracked_state/context.rs +21 -5
- package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
- package/dist-engine-src/src/untracked_state/mod.rs +3 -5
- package/dist-engine-src/src/untracked_state/storage.rs +105 -57
- package/dist-engine-src/src/untracked_state/types.rs +63 -13
- package/dist-engine-src/src/version/context.rs +1 -13
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +3 -2
- package/dist-engine-src/src/version/refs.rs +12 -103
- package/dist-engine-src/src/version/stage_rows.rs +15 -19
- package/package.json +1 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -321
- package/dist-engine-src/src/changelog/context.rs +0 -92
- package/dist-engine-src/src/changelog/materialization.rs +0 -121
- package/dist-engine-src/src/changelog/mod.rs +0 -13
- package/dist-engine-src/src/changelog/reader.rs +0 -20
- package/dist-engine-src/src/changelog/storage.rs +0 -220
- package/dist-engine-src/src/changelog/types.rs +0 -38
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
- package/dist-engine-src/src/schema_registry.rs +0 -294
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
- package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
- package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
|
@@ -1,21 +1,49 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
1
3
|
use xxhash_rust::xxh3::xxh3_64_with_seed;
|
|
2
4
|
|
|
3
|
-
use crate::
|
|
5
|
+
use crate::commit_store::ChangeLocator;
|
|
6
|
+
use crate::entity_identity::EntityIdentity;
|
|
4
7
|
use crate::json_store::JsonRef;
|
|
5
|
-
use crate::tracked_state::
|
|
6
|
-
|
|
8
|
+
use crate::tracked_state::types::{
|
|
9
|
+
TrackedStateDeltaEntry, TrackedStateDeltaRef, TrackedStateIndexValue,
|
|
10
|
+
TrackedStateIndexValueRef, TrackedStateKey, TrackedStateKeyRef, TRACKED_STATE_HASH_BYTES,
|
|
7
11
|
};
|
|
8
12
|
use crate::LixError;
|
|
9
13
|
|
|
10
|
-
const NODE_VERSION: u8 =
|
|
11
|
-
const VALUE_VERSION: u8 =
|
|
14
|
+
const NODE_VERSION: u8 = 2;
|
|
15
|
+
const VALUE_VERSION: u8 = 7;
|
|
16
|
+
const VALUE_DELETED_FLAG: u8 = 0b1000_0000;
|
|
17
|
+
const VALUE_VERSION_MASK: u8 = 0b0111_1111;
|
|
18
|
+
const DELTA_PACK_VERSION: u8 = 7;
|
|
19
|
+
const DELTA_LOCATOR_SAME_COMMIT: u8 = 0;
|
|
20
|
+
const DELTA_LOCATOR_FULL: u8 = 1;
|
|
21
|
+
const DELTA_JSON_REFS_INLINE: u8 = 0;
|
|
22
|
+
const DELTA_JSON_REFS_MIXED_PACK_INDEX: u8 = 1;
|
|
23
|
+
const DELTA_JSON_REF_NONE: u8 = 0;
|
|
24
|
+
const DELTA_JSON_REF_PACK_INDEX: u8 = 1;
|
|
25
|
+
const DELTA_JSON_REF_INLINE: u8 = 2;
|
|
26
|
+
const DELTA_CHANGE_ID_FULL: u8 = 0;
|
|
27
|
+
const DELTA_CHANGE_ID_COMMIT_SUFFIX: u8 = 1;
|
|
28
|
+
const TIMESTAMP_UPDATED_SAME: u8 = 0;
|
|
29
|
+
const TIMESTAMP_UPDATED_DISTINCT: u8 = 1;
|
|
12
30
|
const NODE_KIND_LEAF: u8 = 1;
|
|
13
31
|
const NODE_KIND_INTERNAL: u8 = 2;
|
|
14
32
|
const WEIBULL_K: i32 = 4;
|
|
15
33
|
const ENTITY_IDENTITY_END: u8 = 0;
|
|
16
34
|
const ENTITY_IDENTITY_STRING: u8 = 1;
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
|
|
36
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
37
|
+
struct DeltaKeyPrefixRef<'a> {
|
|
38
|
+
schema_key: &'a str,
|
|
39
|
+
file_id: Option<&'a str>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
43
|
+
struct DeltaKeyPrefix {
|
|
44
|
+
schema_key: String,
|
|
45
|
+
file_id: Option<String>,
|
|
46
|
+
}
|
|
19
47
|
|
|
20
48
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
21
49
|
pub(crate) struct EncodedLeafEntry {
|
|
@@ -23,6 +51,21 @@ pub(crate) struct EncodedLeafEntry {
|
|
|
23
51
|
pub(crate) value: Vec<u8>,
|
|
24
52
|
}
|
|
25
53
|
|
|
54
|
+
#[derive(Debug, Clone, Copy)]
|
|
55
|
+
pub(crate) struct EncodedLeafEntryRef<'a> {
|
|
56
|
+
pub(crate) key: &'a [u8],
|
|
57
|
+
pub(crate) value: &'a [u8],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl EncodedLeafEntry {
|
|
61
|
+
pub(crate) fn as_ref(&self) -> EncodedLeafEntryRef<'_> {
|
|
62
|
+
EncodedLeafEntryRef {
|
|
63
|
+
key: &self.key,
|
|
64
|
+
value: &self.value,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
26
69
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
27
70
|
pub(crate) struct PendingChunkWrite {
|
|
28
71
|
pub(crate) hash: [u8; TRACKED_STATE_HASH_BYTES],
|
|
@@ -37,12 +80,37 @@ pub(crate) struct ChildSummary {
|
|
|
37
80
|
pub(crate) subtree_count: u64,
|
|
38
81
|
}
|
|
39
82
|
|
|
83
|
+
#[derive(Debug, Clone, Copy)]
|
|
84
|
+
pub(crate) struct ChildSummaryRef<'a> {
|
|
85
|
+
pub(crate) first_key: &'a [u8],
|
|
86
|
+
pub(crate) last_key: &'a [u8],
|
|
87
|
+
pub(crate) child_hash: [u8; TRACKED_STATE_HASH_BYTES],
|
|
88
|
+
pub(crate) subtree_count: u64,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl ChildSummary {
|
|
92
|
+
pub(crate) fn as_ref(&self) -> ChildSummaryRef<'_> {
|
|
93
|
+
ChildSummaryRef {
|
|
94
|
+
first_key: &self.first_key,
|
|
95
|
+
last_key: &self.last_key,
|
|
96
|
+
child_hash: self.child_hash,
|
|
97
|
+
subtree_count: self.subtree_count,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
40
102
|
#[derive(Debug, Clone)]
|
|
41
103
|
pub(crate) enum DecodedNode {
|
|
42
104
|
Leaf(DecodedLeafNode),
|
|
43
105
|
Internal(DecodedInternalNode),
|
|
44
106
|
}
|
|
45
107
|
|
|
108
|
+
#[derive(Debug, Clone)]
|
|
109
|
+
pub(crate) enum DecodedNodeRef<'a> {
|
|
110
|
+
Leaf(DecodedLeafNodeRef<'a>),
|
|
111
|
+
Internal(DecodedInternalNode),
|
|
112
|
+
}
|
|
113
|
+
|
|
46
114
|
#[derive(Debug, Clone)]
|
|
47
115
|
pub(crate) struct DecodedLeafNode {
|
|
48
116
|
entries: Vec<EncodedLeafEntry>,
|
|
@@ -54,6 +122,50 @@ impl DecodedLeafNode {
|
|
|
54
122
|
}
|
|
55
123
|
}
|
|
56
124
|
|
|
125
|
+
#[derive(Debug, Clone)]
|
|
126
|
+
pub(crate) struct DecodedLeafNodeRef<'a> {
|
|
127
|
+
bytes: &'a [u8],
|
|
128
|
+
payload_start: usize,
|
|
129
|
+
offsets: Vec<usize>,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl<'a> DecodedLeafNodeRef<'a> {
|
|
133
|
+
pub(crate) fn len(&self) -> usize {
|
|
134
|
+
self.offsets.len().saturating_sub(1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub(crate) fn entry(&self, index: usize) -> Result<Option<EncodedLeafEntryRef<'a>>, LixError> {
|
|
138
|
+
if index >= self.len() {
|
|
139
|
+
return Ok(None);
|
|
140
|
+
}
|
|
141
|
+
let start = self.payload_start + self.offsets[index];
|
|
142
|
+
let end = self.payload_start + self.offsets[index + 1];
|
|
143
|
+
let record = self.bytes.get(start..end).ok_or_else(|| {
|
|
144
|
+
LixError::new(
|
|
145
|
+
"LIX_ERROR_UNKNOWN",
|
|
146
|
+
"tracked-state leaf offset points outside node payload",
|
|
147
|
+
)
|
|
148
|
+
})?;
|
|
149
|
+
let mut cursor = 0usize;
|
|
150
|
+
let key = read_sized_slice(record, &mut cursor, "leaf key")?;
|
|
151
|
+
let value = read_sized_slice(record, &mut cursor, "leaf value")?;
|
|
152
|
+
if cursor != record.len() {
|
|
153
|
+
return Err(LixError::new(
|
|
154
|
+
"LIX_ERROR_UNKNOWN",
|
|
155
|
+
"tracked-state leaf entry decode found trailing bytes",
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
Ok(Some(EncodedLeafEntryRef { key, value }))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub(crate) fn key(&self, index: usize) -> Result<Option<&'a [u8]>, LixError> {
|
|
162
|
+
let Some(entry) = self.entry(index)? else {
|
|
163
|
+
return Ok(None);
|
|
164
|
+
};
|
|
165
|
+
Ok(Some(entry.key))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
57
169
|
#[derive(Debug, Clone)]
|
|
58
170
|
pub(crate) struct DecodedInternalNode {
|
|
59
171
|
children: Vec<ChildSummary>,
|
|
@@ -70,17 +182,29 @@ pub(crate) fn hash_bytes(bytes: &[u8]) -> [u8; TRACKED_STATE_HASH_BYTES] {
|
|
|
70
182
|
}
|
|
71
183
|
|
|
72
184
|
pub(crate) fn encode_key(key: &TrackedStateKey) -> Vec<u8> {
|
|
185
|
+
encode_key_ref(TrackedStateKeyRef {
|
|
186
|
+
schema_key: &key.schema_key,
|
|
187
|
+
file_id: key.file_id.as_deref(),
|
|
188
|
+
entity_id: &key.entity_id,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
pub(crate) fn encode_key_ref(key: TrackedStateKeyRef<'_>) -> Vec<u8> {
|
|
73
193
|
let mut out = Vec::new();
|
|
74
|
-
|
|
75
|
-
|
|
194
|
+
append_key_ref(&mut out, key);
|
|
195
|
+
out
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn append_key_ref(out: &mut Vec<u8>, key: TrackedStateKeyRef<'_>) {
|
|
199
|
+
push_sized_bytes(out, key.schema_key.as_bytes());
|
|
200
|
+
match key.file_id {
|
|
76
201
|
Some(file_id) => {
|
|
77
202
|
out.push(1);
|
|
78
|
-
push_sized_bytes(
|
|
203
|
+
push_sized_bytes(out, file_id.as_bytes());
|
|
79
204
|
}
|
|
80
205
|
None => out.push(0),
|
|
81
206
|
}
|
|
82
|
-
push_entity_identity(
|
|
83
|
-
out
|
|
207
|
+
push_entity_identity(out, key.entity_id);
|
|
84
208
|
}
|
|
85
209
|
|
|
86
210
|
pub(crate) fn encode_schema_key_prefix(schema_key: &str) -> Vec<u8> {
|
|
@@ -128,137 +252,573 @@ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
|
|
|
128
252
|
})
|
|
129
253
|
}
|
|
130
254
|
|
|
131
|
-
|
|
255
|
+
/// Decodes a key after the caller has already proven the schema/file prefix.
|
|
256
|
+
///
|
|
257
|
+
/// This is for scan paths that have matched an encoded prefix range and only
|
|
258
|
+
/// need to materialize the entity suffix plus the known projection fields.
|
|
259
|
+
pub(crate) fn decode_key_with_trusted_prefix(
|
|
260
|
+
bytes: &[u8],
|
|
261
|
+
schema_key: &str,
|
|
262
|
+
file_id: Option<&str>,
|
|
263
|
+
prefix_len: usize,
|
|
264
|
+
) -> Result<TrackedStateKey, LixError> {
|
|
265
|
+
let mut cursor = prefix_len;
|
|
266
|
+
let entity_id = read_entity_identity(bytes, &mut cursor)?;
|
|
267
|
+
if cursor != bytes.len() {
|
|
268
|
+
return Err(LixError::new(
|
|
269
|
+
"LIX_ERROR_UNKNOWN",
|
|
270
|
+
"tracked-state tree key decode found trailing bytes",
|
|
271
|
+
));
|
|
272
|
+
}
|
|
273
|
+
Ok(TrackedStateKey {
|
|
274
|
+
schema_key: schema_key.to_string(),
|
|
275
|
+
file_id: file_id.map(str::to_string),
|
|
276
|
+
entity_id,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[cfg(test)]
|
|
281
|
+
pub(crate) fn encode_value(value: &TrackedStateIndexValue) -> Vec<u8> {
|
|
282
|
+
encode_value_ref(TrackedStateIndexValueRef {
|
|
283
|
+
change_locator: value.change_locator.as_ref(),
|
|
284
|
+
deleted: value.deleted,
|
|
285
|
+
snapshot_ref: value.snapshot_ref.as_ref(),
|
|
286
|
+
metadata_ref: value.metadata_ref.as_ref(),
|
|
287
|
+
created_at: &value.created_at,
|
|
288
|
+
updated_at: &value.updated_at,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
pub(crate) fn encode_value_ref(value: TrackedStateIndexValueRef<'_>) -> Vec<u8> {
|
|
132
293
|
let mut out = Vec::new();
|
|
133
|
-
out
|
|
134
|
-
push_optional_json_ref(&mut out, value.snapshot_ref.as_ref());
|
|
135
|
-
push_optional_json_ref(&mut out, value.metadata_ref.as_ref());
|
|
136
|
-
push_sized_bytes(&mut out, value.schema_version.as_bytes());
|
|
137
|
-
push_sized_bytes(&mut out, value.created_at.as_bytes());
|
|
138
|
-
push_sized_bytes(&mut out, value.updated_at.as_bytes());
|
|
139
|
-
push_sized_bytes(&mut out, value.change_id.as_bytes());
|
|
140
|
-
push_sized_bytes(&mut out, value.commit_id.as_bytes());
|
|
141
|
-
out.push(u8::from(value.deleted));
|
|
294
|
+
append_value_ref(&mut out, value);
|
|
142
295
|
out
|
|
143
296
|
}
|
|
144
297
|
|
|
298
|
+
fn append_value_ref(out: &mut Vec<u8>, value: TrackedStateIndexValueRef<'_>) {
|
|
299
|
+
out.push(VALUE_VERSION | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
|
|
300
|
+
push_sized_bytes(out, value.change_locator.source_commit_id.as_bytes());
|
|
301
|
+
out.extend_from_slice(&value.change_locator.source_pack_id.to_be_bytes());
|
|
302
|
+
out.extend_from_slice(&value.change_locator.source_ordinal.to_be_bytes());
|
|
303
|
+
push_sized_bytes(out, value.change_locator.change_id.as_bytes());
|
|
304
|
+
push_timestamp_pair(out, value.created_at, value.updated_at);
|
|
305
|
+
push_optional_json_ref(out, value.snapshot_ref);
|
|
306
|
+
push_optional_json_ref(out, value.metadata_ref);
|
|
307
|
+
}
|
|
308
|
+
|
|
145
309
|
#[cfg(test)]
|
|
146
|
-
pub(crate) fn encoded_value_len(value: &
|
|
147
|
-
1 +
|
|
310
|
+
pub(crate) fn encoded_value_len(value: &TrackedStateIndexValue) -> usize {
|
|
311
|
+
1 + sized_bytes_len(value.change_locator.source_commit_id.as_bytes())
|
|
312
|
+
+ 4
|
|
313
|
+
+ 4
|
|
314
|
+
+ sized_bytes_len(value.change_locator.change_id.as_bytes())
|
|
315
|
+
+ timestamp_pair_len(&value.created_at, &value.updated_at)
|
|
316
|
+
+ optional_json_ref_len(value.snapshot_ref.as_ref())
|
|
148
317
|
+ optional_json_ref_len(value.metadata_ref.as_ref())
|
|
149
|
-
+ sized_bytes_len(value.schema_version.as_bytes())
|
|
150
|
-
+ sized_bytes_len(value.created_at.as_bytes())
|
|
151
|
-
+ sized_bytes_len(value.updated_at.as_bytes())
|
|
152
|
-
+ sized_bytes_len(value.change_id.as_bytes())
|
|
153
|
-
+ sized_bytes_len(value.commit_id.as_bytes())
|
|
154
|
-
+ 1
|
|
155
318
|
}
|
|
156
319
|
|
|
157
|
-
pub(crate) fn decode_value(bytes: &[u8]) -> Result<
|
|
320
|
+
pub(crate) fn decode_value(bytes: &[u8]) -> Result<TrackedStateIndexValue, LixError> {
|
|
321
|
+
let mut cursor = 0usize;
|
|
322
|
+
let value_header = read_u8(bytes, &mut cursor, "value header")?;
|
|
323
|
+
let deleted = decode_value_header(value_header)?;
|
|
324
|
+
decode_value_after_header(bytes, cursor, deleted)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub(crate) fn decode_visible_value(
|
|
328
|
+
bytes: &[u8],
|
|
329
|
+
include_tombstones: bool,
|
|
330
|
+
) -> Result<Option<TrackedStateIndexValue>, LixError> {
|
|
158
331
|
let mut cursor = 0usize;
|
|
159
|
-
let
|
|
332
|
+
let value_header = read_u8(bytes, &mut cursor, "value header")?;
|
|
333
|
+
let deleted = decode_value_header(value_header)?;
|
|
334
|
+
if deleted && !include_tombstones {
|
|
335
|
+
return Ok(None);
|
|
336
|
+
}
|
|
337
|
+
decode_value_after_header(bytes, cursor, deleted).map(Some)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn decode_value_header(value_header: u8) -> Result<bool, LixError> {
|
|
341
|
+
let version = value_header & VALUE_VERSION_MASK;
|
|
342
|
+
let deleted = value_header & VALUE_DELETED_FLAG != 0;
|
|
160
343
|
if version != VALUE_VERSION {
|
|
161
344
|
return Err(LixError::new(
|
|
162
345
|
"LIX_ERROR_UNKNOWN",
|
|
163
346
|
format!("unsupported tracked-state tree value version {version}"),
|
|
164
347
|
));
|
|
165
348
|
}
|
|
349
|
+
Ok(deleted)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn decode_value_after_header(
|
|
353
|
+
bytes: &[u8],
|
|
354
|
+
mut cursor: usize,
|
|
355
|
+
deleted: bool,
|
|
356
|
+
) -> Result<TrackedStateIndexValue, LixError> {
|
|
357
|
+
let source_commit_id = read_sized_string(bytes, &mut cursor, "source_commit_id")?;
|
|
358
|
+
let source_pack_id =
|
|
359
|
+
u32::try_from(read_u32(bytes, &mut cursor, "source_pack_id")?).map_err(|_| {
|
|
360
|
+
LixError::new(
|
|
361
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
362
|
+
"tracked-state source_pack_id exceeds u32",
|
|
363
|
+
)
|
|
364
|
+
})?;
|
|
365
|
+
let source_ordinal =
|
|
366
|
+
u32::try_from(read_u32(bytes, &mut cursor, "source_ordinal")?).map_err(|_| {
|
|
367
|
+
LixError::new(
|
|
368
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
369
|
+
"tracked-state source_ordinal exceeds u32",
|
|
370
|
+
)
|
|
371
|
+
})?;
|
|
372
|
+
let change_id = read_sized_string(bytes, &mut cursor, "change_id")?;
|
|
373
|
+
let (created_at, updated_at) = read_timestamp_pair(bytes, &mut cursor)?;
|
|
166
374
|
let snapshot_ref = read_optional_json_ref(bytes, &mut cursor, "snapshot_ref")?;
|
|
167
375
|
let metadata_ref = read_optional_json_ref(bytes, &mut cursor, "metadata_ref")?;
|
|
168
|
-
let schema_version = read_sized_string(bytes, &mut cursor, "schema_version")?;
|
|
169
|
-
let created_at = read_sized_string(bytes, &mut cursor, "created_at")?;
|
|
170
|
-
let updated_at = read_sized_string(bytes, &mut cursor, "updated_at")?;
|
|
171
|
-
let change_id = read_sized_string(bytes, &mut cursor, "change_id")?;
|
|
172
|
-
let commit_id = read_sized_string(bytes, &mut cursor, "commit_id")?;
|
|
173
|
-
let deleted = match read_u8(bytes, &mut cursor, "deleted")? {
|
|
174
|
-
0 => false,
|
|
175
|
-
1 => true,
|
|
176
|
-
other => {
|
|
177
|
-
return Err(LixError::new(
|
|
178
|
-
"LIX_ERROR_UNKNOWN",
|
|
179
|
-
format!("tracked-state tree value has invalid deleted byte {other}"),
|
|
180
|
-
))
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
376
|
if cursor != bytes.len() {
|
|
184
377
|
return Err(LixError::new(
|
|
185
378
|
"LIX_ERROR_UNKNOWN",
|
|
186
379
|
"tracked-state tree value decode found trailing bytes",
|
|
187
380
|
));
|
|
188
381
|
}
|
|
189
|
-
Ok(
|
|
382
|
+
Ok(TrackedStateIndexValue {
|
|
383
|
+
change_locator: ChangeLocator {
|
|
384
|
+
source_commit_id,
|
|
385
|
+
source_pack_id,
|
|
386
|
+
source_ordinal,
|
|
387
|
+
change_id,
|
|
388
|
+
},
|
|
389
|
+
deleted,
|
|
190
390
|
snapshot_ref,
|
|
191
391
|
metadata_ref,
|
|
192
|
-
schema_version,
|
|
193
392
|
created_at,
|
|
194
393
|
updated_at,
|
|
195
|
-
change_id,
|
|
196
|
-
commit_id,
|
|
197
|
-
deleted,
|
|
198
394
|
})
|
|
199
395
|
}
|
|
200
396
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
397
|
+
pub(crate) fn encode_delta_pack_refs(
|
|
398
|
+
commit_id: &str,
|
|
399
|
+
deltas: &[TrackedStateDeltaRef<'_>],
|
|
400
|
+
) -> Result<Vec<u8>, LixError> {
|
|
401
|
+
encode_delta_pack_refs_with_json_pack_indexes(commit_id, deltas, None)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
pub(crate) fn encode_delta_pack_refs_with_json_pack_indexes(
|
|
405
|
+
commit_id: &str,
|
|
406
|
+
deltas: &[TrackedStateDeltaRef<'_>],
|
|
407
|
+
json_pack_indexes: Option<&HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>>,
|
|
408
|
+
) -> Result<Vec<u8>, LixError> {
|
|
409
|
+
let json_pack_indexes = json_pack_indexes.filter(|indexes| !indexes.is_empty());
|
|
410
|
+
let mut out = Vec::new();
|
|
411
|
+
out.extend_from_slice(b"LXTD");
|
|
412
|
+
out.push(DELTA_PACK_VERSION);
|
|
413
|
+
push_var_sized_bytes(&mut out, commit_id.as_bytes(), "delta pack commit_id")?;
|
|
414
|
+
let (key_prefixes, delta_prefix_indexes) = delta_key_prefixes(deltas);
|
|
415
|
+
push_var_u32(&mut out, key_prefixes.len(), "delta key prefix count")?;
|
|
416
|
+
for prefix in &key_prefixes {
|
|
417
|
+
append_delta_key_prefix_ref(&mut out, *prefix)?;
|
|
418
|
+
}
|
|
419
|
+
push_var_u32(&mut out, deltas.len(), "delta pack entry count")?;
|
|
420
|
+
out.push(if json_pack_indexes.is_some() {
|
|
421
|
+
DELTA_JSON_REFS_MIXED_PACK_INDEX
|
|
422
|
+
} else {
|
|
423
|
+
DELTA_JSON_REFS_INLINE
|
|
424
|
+
});
|
|
425
|
+
for (delta, prefix_index) in deltas.iter().zip(delta_prefix_indexes) {
|
|
426
|
+
append_delta_key_ref(
|
|
427
|
+
&mut out,
|
|
428
|
+
&key_prefixes,
|
|
429
|
+
prefix_index,
|
|
430
|
+
TrackedStateKeyRef {
|
|
431
|
+
schema_key: delta.change.schema_key,
|
|
432
|
+
file_id: delta.change.file_id,
|
|
433
|
+
entity_id: delta.change.entity_id,
|
|
434
|
+
},
|
|
435
|
+
)?;
|
|
436
|
+
append_delta_value_ref(
|
|
437
|
+
&mut out,
|
|
438
|
+
commit_id,
|
|
439
|
+
json_pack_indexes,
|
|
440
|
+
TrackedStateIndexValueRef {
|
|
441
|
+
change_locator: delta.locator,
|
|
442
|
+
deleted: delta.change.snapshot_ref.is_none(),
|
|
443
|
+
snapshot_ref: delta.change.snapshot_ref,
|
|
444
|
+
metadata_ref: delta.change.metadata_ref,
|
|
445
|
+
created_at: delta.created_at,
|
|
446
|
+
updated_at: delta.updated_at,
|
|
447
|
+
},
|
|
448
|
+
)?;
|
|
449
|
+
}
|
|
450
|
+
Ok(out)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fn delta_key_prefixes<'a>(
|
|
454
|
+
deltas: &'a [TrackedStateDeltaRef<'a>],
|
|
455
|
+
) -> (Vec<DeltaKeyPrefixRef<'a>>, Vec<usize>) {
|
|
456
|
+
let mut prefixes = Vec::new();
|
|
457
|
+
let mut delta_prefix_indexes = Vec::with_capacity(deltas.len());
|
|
458
|
+
for delta in deltas {
|
|
459
|
+
let prefix = DeltaKeyPrefixRef {
|
|
460
|
+
schema_key: delta.change.schema_key,
|
|
461
|
+
file_id: delta.change.file_id,
|
|
462
|
+
};
|
|
463
|
+
let prefix_index = match prefixes.iter().position(|candidate| *candidate == prefix) {
|
|
464
|
+
Some(prefix_index) => prefix_index,
|
|
465
|
+
None => {
|
|
466
|
+
let prefix_index = prefixes.len();
|
|
467
|
+
prefixes.push(prefix);
|
|
468
|
+
prefix_index
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
delta_prefix_indexes.push(prefix_index);
|
|
472
|
+
}
|
|
473
|
+
(prefixes, delta_prefix_indexes)
|
|
204
474
|
}
|
|
205
475
|
|
|
206
|
-
fn
|
|
207
|
-
|
|
208
|
-
|
|
476
|
+
fn append_delta_key_prefix_ref(
|
|
477
|
+
out: &mut Vec<u8>,
|
|
478
|
+
prefix: DeltaKeyPrefixRef<'_>,
|
|
479
|
+
) -> Result<(), LixError> {
|
|
480
|
+
push_var_sized_bytes(
|
|
481
|
+
out,
|
|
482
|
+
prefix.schema_key.as_bytes(),
|
|
483
|
+
"delta key prefix schema_key",
|
|
484
|
+
)?;
|
|
485
|
+
match prefix.file_id {
|
|
486
|
+
Some(file_id) => {
|
|
209
487
|
out.push(1);
|
|
210
|
-
out.
|
|
488
|
+
push_var_sized_bytes(out, file_id.as_bytes(), "delta key prefix file_id")?;
|
|
211
489
|
}
|
|
212
490
|
None => out.push(0),
|
|
213
491
|
}
|
|
492
|
+
Ok(())
|
|
214
493
|
}
|
|
215
494
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
match
|
|
219
|
-
|
|
220
|
-
|
|
495
|
+
fn decode_delta_key_prefix(bytes: &[u8], cursor: &mut usize) -> Result<DeltaKeyPrefix, LixError> {
|
|
496
|
+
let schema_key = read_var_sized_string(bytes, cursor, "delta key prefix schema_key")?;
|
|
497
|
+
let file_id = match read_u8(bytes, cursor, "delta key prefix file_id presence")? {
|
|
498
|
+
0 => None,
|
|
499
|
+
1 => Some(read_var_sized_string(
|
|
500
|
+
bytes,
|
|
501
|
+
cursor,
|
|
502
|
+
"delta key prefix file_id",
|
|
503
|
+
)?),
|
|
504
|
+
other => {
|
|
505
|
+
return Err(LixError::new(
|
|
506
|
+
"LIX_ERROR_UNKNOWN",
|
|
507
|
+
format!("tracked-state delta key prefix has invalid file_id presence byte {other}"),
|
|
508
|
+
))
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
Ok(DeltaKeyPrefix {
|
|
512
|
+
schema_key,
|
|
513
|
+
file_id,
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
fn append_delta_key_ref(
|
|
518
|
+
out: &mut Vec<u8>,
|
|
519
|
+
prefixes: &[DeltaKeyPrefixRef<'_>],
|
|
520
|
+
prefix_index: usize,
|
|
521
|
+
key: TrackedStateKeyRef<'_>,
|
|
522
|
+
) -> Result<(), LixError> {
|
|
523
|
+
let prefix = DeltaKeyPrefixRef {
|
|
524
|
+
schema_key: key.schema_key,
|
|
525
|
+
file_id: key.file_id,
|
|
526
|
+
};
|
|
527
|
+
debug_assert_eq!(prefixes.get(prefix_index), Some(&prefix));
|
|
528
|
+
push_var_u32(out, prefix_index, "delta key prefix index")?;
|
|
529
|
+
push_var_entity_identity(out, key.entity_id)?;
|
|
530
|
+
Ok(())
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fn append_delta_value_ref(
|
|
534
|
+
out: &mut Vec<u8>,
|
|
535
|
+
pack_commit_id: &str,
|
|
536
|
+
json_pack_indexes: Option<&HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>>,
|
|
537
|
+
value: TrackedStateIndexValueRef<'_>,
|
|
538
|
+
) -> Result<(), LixError> {
|
|
539
|
+
out.push(VALUE_VERSION | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
|
|
540
|
+
if value.change_locator.source_commit_id == pack_commit_id {
|
|
541
|
+
out.push(DELTA_LOCATOR_SAME_COMMIT);
|
|
542
|
+
} else {
|
|
543
|
+
out.push(DELTA_LOCATOR_FULL);
|
|
544
|
+
push_var_sized_bytes(
|
|
545
|
+
out,
|
|
546
|
+
value.change_locator.source_commit_id.as_bytes(),
|
|
547
|
+
"source_commit_id",
|
|
548
|
+
)?;
|
|
549
|
+
}
|
|
550
|
+
push_var_u32(
|
|
551
|
+
out,
|
|
552
|
+
value.change_locator.source_pack_id as usize,
|
|
553
|
+
"source_pack_id",
|
|
554
|
+
)?;
|
|
555
|
+
push_var_u32(
|
|
556
|
+
out,
|
|
557
|
+
value.change_locator.source_ordinal as usize,
|
|
558
|
+
"source_ordinal",
|
|
559
|
+
)?;
|
|
560
|
+
push_var_delta_change_id(
|
|
561
|
+
out,
|
|
562
|
+
value.change_locator.source_commit_id,
|
|
563
|
+
value.change_locator.change_id,
|
|
564
|
+
)?;
|
|
565
|
+
push_var_timestamp_pair(out, value.created_at, value.updated_at)?;
|
|
566
|
+
match json_pack_indexes {
|
|
567
|
+
Some(indexes) => {
|
|
568
|
+
push_mixed_optional_json_ref(out, indexes, value.snapshot_ref)?;
|
|
569
|
+
push_mixed_optional_json_ref(out, indexes, value.metadata_ref)?;
|
|
570
|
+
}
|
|
571
|
+
None => {
|
|
572
|
+
push_optional_json_ref(out, value.snapshot_ref);
|
|
573
|
+
push_optional_json_ref(out, value.metadata_ref);
|
|
574
|
+
}
|
|
221
575
|
}
|
|
576
|
+
Ok(())
|
|
222
577
|
}
|
|
223
578
|
|
|
224
|
-
fn
|
|
579
|
+
pub(crate) fn decode_delta_pack(
|
|
580
|
+
bytes: &[u8],
|
|
581
|
+
pack_json_refs: Option<&[JsonRef]>,
|
|
582
|
+
) -> Result<(String, Vec<TrackedStateDeltaEntry>), LixError> {
|
|
583
|
+
let mut cursor = 0usize;
|
|
584
|
+
let magic = bytes.get(0..4).ok_or_else(|| {
|
|
585
|
+
LixError::new(
|
|
586
|
+
"LIX_ERROR_UNKNOWN",
|
|
587
|
+
"tracked-state delta pack is truncated before magic",
|
|
588
|
+
)
|
|
589
|
+
})?;
|
|
590
|
+
if magic != b"LXTD" {
|
|
591
|
+
return Err(LixError::new(
|
|
592
|
+
"LIX_ERROR_UNKNOWN",
|
|
593
|
+
"tracked-state delta pack has invalid magic",
|
|
594
|
+
));
|
|
595
|
+
}
|
|
596
|
+
cursor += 4;
|
|
597
|
+
let version = read_u8(bytes, &mut cursor, "delta pack version")?;
|
|
598
|
+
if version != DELTA_PACK_VERSION {
|
|
599
|
+
return Err(LixError::new(
|
|
600
|
+
"LIX_ERROR_UNKNOWN",
|
|
601
|
+
format!("unsupported tracked-state delta pack version {version}"),
|
|
602
|
+
));
|
|
603
|
+
}
|
|
604
|
+
let commit_id = read_var_sized_string(bytes, &mut cursor, "delta pack commit_id")?;
|
|
605
|
+
let prefix_count = read_var_u32(bytes, &mut cursor, "delta key prefix count")?;
|
|
606
|
+
let mut key_prefixes = Vec::new();
|
|
607
|
+
for _ in 0..prefix_count {
|
|
608
|
+
key_prefixes.push(decode_delta_key_prefix(bytes, &mut cursor)?);
|
|
609
|
+
}
|
|
610
|
+
let count = read_var_u32(bytes, &mut cursor, "delta pack entry count")?;
|
|
611
|
+
let json_ref_mode = decode_delta_json_ref_mode(bytes, &mut cursor, pack_json_refs)?;
|
|
612
|
+
let mut entries = Vec::new();
|
|
613
|
+
for _ in 0..count {
|
|
614
|
+
let key = decode_delta_key(bytes, &mut cursor, &key_prefixes)?;
|
|
615
|
+
let value = decode_delta_value(bytes, &mut cursor, &commit_id, &json_ref_mode)?;
|
|
616
|
+
entries.push(TrackedStateDeltaEntry { key, value });
|
|
617
|
+
}
|
|
618
|
+
if cursor != bytes.len() {
|
|
619
|
+
return Err(LixError::new(
|
|
620
|
+
"LIX_ERROR_UNKNOWN",
|
|
621
|
+
"tracked-state delta pack decode found trailing bytes",
|
|
622
|
+
));
|
|
623
|
+
}
|
|
624
|
+
Ok((commit_id, entries))
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
pub(crate) fn delta_pack_uses_json_pack_indexes(bytes: &[u8]) -> Result<bool, LixError> {
|
|
628
|
+
let mut cursor = 0usize;
|
|
629
|
+
let magic = bytes.get(0..4).ok_or_else(|| {
|
|
630
|
+
LixError::new(
|
|
631
|
+
"LIX_ERROR_UNKNOWN",
|
|
632
|
+
"tracked-state delta pack is truncated before magic",
|
|
633
|
+
)
|
|
634
|
+
})?;
|
|
635
|
+
if magic != b"LXTD" {
|
|
636
|
+
return Err(LixError::new(
|
|
637
|
+
"LIX_ERROR_UNKNOWN",
|
|
638
|
+
"tracked-state delta pack has invalid magic",
|
|
639
|
+
));
|
|
640
|
+
}
|
|
641
|
+
cursor += 4;
|
|
642
|
+
let version = read_u8(bytes, &mut cursor, "delta pack version")?;
|
|
643
|
+
if version != DELTA_PACK_VERSION {
|
|
644
|
+
return Err(LixError::new(
|
|
645
|
+
"LIX_ERROR_UNKNOWN",
|
|
646
|
+
format!("unsupported tracked-state delta pack version {version}"),
|
|
647
|
+
));
|
|
648
|
+
}
|
|
649
|
+
let _commit_id = read_var_sized_string(bytes, &mut cursor, "delta pack commit_id")?;
|
|
650
|
+
let prefix_count = read_var_u32(bytes, &mut cursor, "delta key prefix count")?;
|
|
651
|
+
for _ in 0..prefix_count {
|
|
652
|
+
let _ = decode_delta_key_prefix(bytes, &mut cursor)?;
|
|
653
|
+
}
|
|
654
|
+
let _count = read_var_u32(bytes, &mut cursor, "delta pack entry count")?;
|
|
655
|
+
match read_u8(bytes, &mut cursor, "delta JSON ref mode")? {
|
|
656
|
+
DELTA_JSON_REFS_INLINE => Ok(false),
|
|
657
|
+
DELTA_JSON_REFS_MIXED_PACK_INDEX => Ok(true),
|
|
658
|
+
other => Err(LixError::new(
|
|
659
|
+
"LIX_ERROR_UNKNOWN",
|
|
660
|
+
format!("tracked-state delta pack has invalid JSON ref mode {other}"),
|
|
661
|
+
)),
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
fn decode_delta_key(
|
|
225
666
|
bytes: &[u8],
|
|
226
667
|
cursor: &mut usize,
|
|
227
|
-
|
|
228
|
-
) -> Result<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
668
|
+
prefixes: &[DeltaKeyPrefix],
|
|
669
|
+
) -> Result<TrackedStateKey, LixError> {
|
|
670
|
+
let prefix_index = read_var_u32(bytes, cursor, "delta key prefix index")?;
|
|
671
|
+
let prefix = prefixes.get(prefix_index).ok_or_else(|| {
|
|
672
|
+
LixError::new(
|
|
673
|
+
"LIX_ERROR_UNKNOWN",
|
|
674
|
+
format!("tracked-state delta key prefix index {prefix_index} is out of bounds"),
|
|
675
|
+
)
|
|
676
|
+
})?;
|
|
677
|
+
let entity_id = read_var_entity_identity(bytes, cursor)?;
|
|
678
|
+
Ok(TrackedStateKey {
|
|
679
|
+
schema_key: prefix.schema_key.clone(),
|
|
680
|
+
file_id: prefix.file_id.clone(),
|
|
681
|
+
entity_id,
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
enum DeltaJsonRefDecodeMode<'a> {
|
|
686
|
+
Inline,
|
|
687
|
+
MixedPackIndex(&'a [JsonRef]),
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
fn decode_delta_json_ref_mode<'a>(
|
|
691
|
+
bytes: &[u8],
|
|
692
|
+
cursor: &mut usize,
|
|
693
|
+
pack_json_refs: Option<&'a [JsonRef]>,
|
|
694
|
+
) -> Result<DeltaJsonRefDecodeMode<'a>, LixError> {
|
|
695
|
+
match read_u8(bytes, cursor, "delta JSON ref mode")? {
|
|
696
|
+
DELTA_JSON_REFS_INLINE => Ok(DeltaJsonRefDecodeMode::Inline),
|
|
697
|
+
DELTA_JSON_REFS_MIXED_PACK_INDEX => {
|
|
698
|
+
let refs = pack_json_refs.ok_or_else(|| {
|
|
699
|
+
LixError::new(
|
|
700
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
701
|
+
"tracked-state delta pack needs JSON pack refs but none were provided",
|
|
702
|
+
)
|
|
703
|
+
})?;
|
|
704
|
+
Ok(DeltaJsonRefDecodeMode::MixedPackIndex(refs))
|
|
234
705
|
}
|
|
235
706
|
other => Err(LixError::new(
|
|
236
707
|
"LIX_ERROR_UNKNOWN",
|
|
237
|
-
format!("tracked-state
|
|
708
|
+
format!("tracked-state delta pack has invalid JSON ref mode {other}"),
|
|
238
709
|
)),
|
|
239
710
|
}
|
|
240
711
|
}
|
|
241
712
|
|
|
713
|
+
fn decode_delta_value(
|
|
714
|
+
bytes: &[u8],
|
|
715
|
+
cursor: &mut usize,
|
|
716
|
+
pack_commit_id: &str,
|
|
717
|
+
json_ref_mode: &DeltaJsonRefDecodeMode<'_>,
|
|
718
|
+
) -> Result<TrackedStateIndexValue, LixError> {
|
|
719
|
+
let value_header = read_u8(bytes, cursor, "delta value header")?;
|
|
720
|
+
let deleted = decode_value_header(value_header)?;
|
|
721
|
+
let source_commit_id = match read_u8(bytes, cursor, "delta locator tag")? {
|
|
722
|
+
DELTA_LOCATOR_SAME_COMMIT => pack_commit_id.to_string(),
|
|
723
|
+
DELTA_LOCATOR_FULL => read_var_sized_string(bytes, cursor, "source_commit_id")?,
|
|
724
|
+
other => {
|
|
725
|
+
return Err(LixError::new(
|
|
726
|
+
"LIX_ERROR_UNKNOWN",
|
|
727
|
+
format!("tracked-state delta value has invalid locator tag {other}"),
|
|
728
|
+
))
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
let source_pack_id =
|
|
732
|
+
u32::try_from(read_var_u32(bytes, cursor, "source_pack_id")?).map_err(|_| {
|
|
733
|
+
LixError::new(
|
|
734
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
735
|
+
"tracked-state source_pack_id exceeds u32",
|
|
736
|
+
)
|
|
737
|
+
})?;
|
|
738
|
+
let source_ordinal =
|
|
739
|
+
u32::try_from(read_var_u32(bytes, cursor, "source_ordinal")?).map_err(|_| {
|
|
740
|
+
LixError::new(
|
|
741
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
742
|
+
"tracked-state source_ordinal exceeds u32",
|
|
743
|
+
)
|
|
744
|
+
})?;
|
|
745
|
+
let change_id = read_var_delta_change_id(bytes, cursor, &source_commit_id)?;
|
|
746
|
+
let (created_at, updated_at) = read_var_timestamp_pair(bytes, cursor)?;
|
|
747
|
+
let (snapshot_ref, metadata_ref) = match json_ref_mode {
|
|
748
|
+
DeltaJsonRefDecodeMode::Inline => (
|
|
749
|
+
read_optional_json_ref(bytes, cursor, "snapshot_ref")?,
|
|
750
|
+
read_optional_json_ref(bytes, cursor, "metadata_ref")?,
|
|
751
|
+
),
|
|
752
|
+
DeltaJsonRefDecodeMode::MixedPackIndex(refs) => (
|
|
753
|
+
read_mixed_optional_json_ref(bytes, cursor, refs, "snapshot_ref")?,
|
|
754
|
+
read_mixed_optional_json_ref(bytes, cursor, refs, "metadata_ref")?,
|
|
755
|
+
),
|
|
756
|
+
};
|
|
757
|
+
Ok(TrackedStateIndexValue {
|
|
758
|
+
change_locator: ChangeLocator {
|
|
759
|
+
source_commit_id,
|
|
760
|
+
source_pack_id,
|
|
761
|
+
source_ordinal,
|
|
762
|
+
change_id,
|
|
763
|
+
},
|
|
764
|
+
deleted,
|
|
765
|
+
snapshot_ref,
|
|
766
|
+
metadata_ref,
|
|
767
|
+
created_at,
|
|
768
|
+
updated_at,
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
#[cfg(test)]
|
|
773
|
+
fn sized_bytes_len(bytes: &[u8]) -> usize {
|
|
774
|
+
4 + bytes.len()
|
|
775
|
+
}
|
|
776
|
+
|
|
242
777
|
pub(crate) fn encode_leaf_node(entries: &[EncodedLeafEntry]) -> Vec<u8> {
|
|
778
|
+
let entries = entries
|
|
779
|
+
.iter()
|
|
780
|
+
.map(EncodedLeafEntry::as_ref)
|
|
781
|
+
.collect::<Vec<_>>();
|
|
782
|
+
encode_leaf_node_refs(&entries)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
pub(crate) fn encode_leaf_node_refs(entries: &[EncodedLeafEntryRef<'_>]) -> Vec<u8> {
|
|
243
786
|
let mut out = Vec::new();
|
|
244
787
|
out.push(NODE_KIND_LEAF);
|
|
245
788
|
out.push(NODE_VERSION);
|
|
246
789
|
push_u32(&mut out, entries.len());
|
|
790
|
+
|
|
791
|
+
let mut offsets = Vec::with_capacity(entries.len().saturating_add(1));
|
|
792
|
+
let mut payload = Vec::new();
|
|
793
|
+
offsets.push(0usize);
|
|
247
794
|
for entry in entries {
|
|
248
|
-
push_sized_bytes(&mut
|
|
249
|
-
push_sized_bytes(&mut
|
|
795
|
+
push_sized_bytes(&mut payload, entry.key);
|
|
796
|
+
push_sized_bytes(&mut payload, entry.value);
|
|
797
|
+
offsets.push(payload.len());
|
|
250
798
|
}
|
|
799
|
+
for offset in offsets {
|
|
800
|
+
push_u32(&mut out, offset);
|
|
801
|
+
}
|
|
802
|
+
out.extend_from_slice(&payload);
|
|
251
803
|
out
|
|
252
804
|
}
|
|
253
805
|
|
|
254
806
|
pub(crate) fn encode_internal_node(children: &[ChildSummary]) -> Vec<u8> {
|
|
807
|
+
let children = children
|
|
808
|
+
.iter()
|
|
809
|
+
.map(ChildSummary::as_ref)
|
|
810
|
+
.collect::<Vec<_>>();
|
|
811
|
+
encode_internal_node_refs(&children)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
pub(crate) fn encode_internal_node_refs(children: &[ChildSummaryRef<'_>]) -> Vec<u8> {
|
|
255
815
|
let mut out = Vec::new();
|
|
256
816
|
out.push(NODE_KIND_INTERNAL);
|
|
257
817
|
out.push(NODE_VERSION);
|
|
258
818
|
push_u32(&mut out, children.len());
|
|
259
819
|
for child in children {
|
|
260
|
-
push_sized_bytes(&mut out,
|
|
261
|
-
push_sized_bytes(&mut out,
|
|
820
|
+
push_sized_bytes(&mut out, child.first_key);
|
|
821
|
+
push_sized_bytes(&mut out, child.last_key);
|
|
262
822
|
out.extend_from_slice(&child.child_hash);
|
|
263
823
|
out.extend_from_slice(&child.subtree_count.to_be_bytes());
|
|
264
824
|
}
|
|
@@ -266,6 +826,28 @@ pub(crate) fn encode_internal_node(children: &[ChildSummary]) -> Vec<u8> {
|
|
|
266
826
|
}
|
|
267
827
|
|
|
268
828
|
pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
|
|
829
|
+
match decode_node_ref(bytes)? {
|
|
830
|
+
DecodedNodeRef::Leaf(leaf) => {
|
|
831
|
+
let mut entries = Vec::with_capacity(leaf.len());
|
|
832
|
+
for index in 0..leaf.len() {
|
|
833
|
+
let entry = leaf.entry(index)?.ok_or_else(|| {
|
|
834
|
+
LixError::new(
|
|
835
|
+
"LIX_ERROR_UNKNOWN",
|
|
836
|
+
"tracked-state leaf entry disappeared during owned decode",
|
|
837
|
+
)
|
|
838
|
+
})?;
|
|
839
|
+
entries.push(EncodedLeafEntry {
|
|
840
|
+
key: entry.key.to_vec(),
|
|
841
|
+
value: entry.value.to_vec(),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
Ok(DecodedNode::Leaf(DecodedLeafNode { entries }))
|
|
845
|
+
}
|
|
846
|
+
DecodedNodeRef::Internal(internal) => Ok(DecodedNode::Internal(internal)),
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
pub(crate) fn decode_node_ref(bytes: &[u8]) -> Result<DecodedNodeRef<'_>, LixError> {
|
|
269
851
|
let mut cursor = 0usize;
|
|
270
852
|
let kind = read_u8(bytes, &mut cursor, "node kind")?;
|
|
271
853
|
let version = read_u8(bytes, &mut cursor, "node version")?;
|
|
@@ -278,14 +860,8 @@ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
|
|
|
278
860
|
let count = read_u32(bytes, &mut cursor, "entry count")?;
|
|
279
861
|
let node = match kind {
|
|
280
862
|
NODE_KIND_LEAF => {
|
|
281
|
-
let
|
|
282
|
-
|
|
283
|
-
entries.push(EncodedLeafEntry {
|
|
284
|
-
key: read_sized_bytes(bytes, &mut cursor, "leaf key")?,
|
|
285
|
-
value: read_sized_bytes(bytes, &mut cursor, "leaf value")?,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
DecodedNode::Leaf(DecodedLeafNode { entries })
|
|
863
|
+
let leaf = decode_leaf_node_ref_after_count(bytes, &mut cursor, count)?;
|
|
864
|
+
DecodedNodeRef::Leaf(leaf)
|
|
289
865
|
}
|
|
290
866
|
NODE_KIND_INTERNAL => {
|
|
291
867
|
let mut children = Vec::with_capacity(count);
|
|
@@ -301,7 +877,7 @@ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
|
|
|
301
877
|
subtree_count,
|
|
302
878
|
});
|
|
303
879
|
}
|
|
304
|
-
|
|
880
|
+
DecodedNodeRef::Internal(DecodedInternalNode { children })
|
|
305
881
|
}
|
|
306
882
|
other => {
|
|
307
883
|
return Err(LixError::new(
|
|
@@ -319,6 +895,50 @@ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
|
|
|
319
895
|
Ok(node)
|
|
320
896
|
}
|
|
321
897
|
|
|
898
|
+
fn decode_leaf_node_ref_after_count<'a>(
|
|
899
|
+
bytes: &'a [u8],
|
|
900
|
+
cursor: &mut usize,
|
|
901
|
+
count: usize,
|
|
902
|
+
) -> Result<DecodedLeafNodeRef<'a>, LixError> {
|
|
903
|
+
let mut offsets = Vec::with_capacity(count.saturating_add(1));
|
|
904
|
+
for _ in 0..=count {
|
|
905
|
+
offsets.push(read_u32(bytes, cursor, "leaf entry offset")?);
|
|
906
|
+
}
|
|
907
|
+
if offsets.first().copied() != Some(0) {
|
|
908
|
+
return Err(LixError::new(
|
|
909
|
+
"LIX_ERROR_UNKNOWN",
|
|
910
|
+
"tracked-state leaf offset table must start at zero",
|
|
911
|
+
));
|
|
912
|
+
}
|
|
913
|
+
for window in offsets.windows(2) {
|
|
914
|
+
if window[0] > window[1] {
|
|
915
|
+
return Err(LixError::new(
|
|
916
|
+
"LIX_ERROR_UNKNOWN",
|
|
917
|
+
"tracked-state leaf offsets must be monotonic",
|
|
918
|
+
));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
let payload_len = bytes.len().checked_sub(*cursor).ok_or_else(|| {
|
|
922
|
+
LixError::new(
|
|
923
|
+
"LIX_ERROR_UNKNOWN",
|
|
924
|
+
"tracked-state leaf payload start is past node end",
|
|
925
|
+
)
|
|
926
|
+
})?;
|
|
927
|
+
if offsets.last().copied().unwrap_or_default() != payload_len {
|
|
928
|
+
return Err(LixError::new(
|
|
929
|
+
"LIX_ERROR_UNKNOWN",
|
|
930
|
+
"tracked-state leaf offset table does not cover full payload",
|
|
931
|
+
));
|
|
932
|
+
}
|
|
933
|
+
let payload_start = *cursor;
|
|
934
|
+
*cursor = bytes.len();
|
|
935
|
+
Ok(DecodedLeafNodeRef {
|
|
936
|
+
bytes,
|
|
937
|
+
payload_start,
|
|
938
|
+
offsets,
|
|
939
|
+
})
|
|
940
|
+
}
|
|
941
|
+
|
|
322
942
|
pub(crate) fn child_summary_from_node(
|
|
323
943
|
node_bytes: Vec<u8>,
|
|
324
944
|
first_key: Vec<u8>,
|
|
@@ -384,20 +1004,8 @@ fn push_entity_identity(out: &mut Vec<u8>, identity: &EntityIdentity) {
|
|
|
384
1004
|
"tracked-state key entity identity must contain at least one part"
|
|
385
1005
|
);
|
|
386
1006
|
for part in &identity.parts {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
out.push(ENTITY_IDENTITY_STRING);
|
|
390
|
-
push_sized_bytes(out, value.as_bytes());
|
|
391
|
-
}
|
|
392
|
-
EntityIdentityPart::Bool(value) => {
|
|
393
|
-
out.push(ENTITY_IDENTITY_BOOL);
|
|
394
|
-
out.push(u8::from(*value));
|
|
395
|
-
}
|
|
396
|
-
EntityIdentityPart::Number(value) => {
|
|
397
|
-
out.push(ENTITY_IDENTITY_NUMBER);
|
|
398
|
-
push_sized_bytes(out, value.as_bytes());
|
|
399
|
-
}
|
|
400
|
-
}
|
|
1007
|
+
out.push(ENTITY_IDENTITY_STRING);
|
|
1008
|
+
push_sized_bytes(out, part.as_bytes());
|
|
401
1009
|
}
|
|
402
1010
|
out.push(ENTITY_IDENTITY_END);
|
|
403
1011
|
}
|
|
@@ -409,33 +1017,11 @@ fn read_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdenti
|
|
|
409
1017
|
match tag {
|
|
410
1018
|
ENTITY_IDENTITY_END => break,
|
|
411
1019
|
ENTITY_IDENTITY_STRING => {
|
|
412
|
-
parts.push(
|
|
1020
|
+
parts.push(read_sized_string(
|
|
413
1021
|
bytes,
|
|
414
1022
|
cursor,
|
|
415
1023
|
"entity identity string part",
|
|
416
|
-
)?)
|
|
417
|
-
}
|
|
418
|
-
ENTITY_IDENTITY_BOOL => {
|
|
419
|
-
let value = match read_u8(bytes, cursor, "entity identity bool part")? {
|
|
420
|
-
0 => false,
|
|
421
|
-
1 => true,
|
|
422
|
-
other => {
|
|
423
|
-
return Err(LixError::new(
|
|
424
|
-
"LIX_ERROR_UNKNOWN",
|
|
425
|
-
format!(
|
|
426
|
-
"tracked-state tree key has invalid entity identity bool byte {other}"
|
|
427
|
-
),
|
|
428
|
-
))
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
parts.push(EntityIdentityPart::Bool(value));
|
|
432
|
-
}
|
|
433
|
-
ENTITY_IDENTITY_NUMBER => {
|
|
434
|
-
parts.push(EntityIdentityPart::Number(read_sized_string(
|
|
435
|
-
bytes,
|
|
436
|
-
cursor,
|
|
437
|
-
"entity identity number part",
|
|
438
|
-
)?));
|
|
1024
|
+
)?);
|
|
439
1025
|
}
|
|
440
1026
|
other => {
|
|
441
1027
|
return Err(LixError::new(
|
|
@@ -459,6 +1045,180 @@ fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
|
|
459
1045
|
out.extend_from_slice(bytes);
|
|
460
1046
|
}
|
|
461
1047
|
|
|
1048
|
+
fn push_var_u32(out: &mut Vec<u8>, value: usize, field_name: &str) -> Result<(), LixError> {
|
|
1049
|
+
let (encoded, len) = var_u32_bytes(value, field_name)?;
|
|
1050
|
+
out.extend_from_slice(&encoded[..len]);
|
|
1051
|
+
Ok(())
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
fn var_u32_bytes(value: usize, field_name: &str) -> Result<([u8; 5], usize), LixError> {
|
|
1055
|
+
let mut value = u32::try_from(value).map_err(|_| {
|
|
1056
|
+
LixError::new(
|
|
1057
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
1058
|
+
format!("tracked-state delta pack field '{field_name}' exceeds u32"),
|
|
1059
|
+
)
|
|
1060
|
+
})?;
|
|
1061
|
+
let mut encoded = [0_u8; 5];
|
|
1062
|
+
let mut len = 0usize;
|
|
1063
|
+
while value >= 0x80 {
|
|
1064
|
+
encoded[len] = (value as u8 & 0x7f) | 0x80;
|
|
1065
|
+
len += 1;
|
|
1066
|
+
value >>= 7;
|
|
1067
|
+
}
|
|
1068
|
+
encoded[len] = value as u8;
|
|
1069
|
+
len += 1;
|
|
1070
|
+
Ok((encoded, len))
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
fn push_var_sized_bytes(out: &mut Vec<u8>, bytes: &[u8], field_name: &str) -> Result<(), LixError> {
|
|
1074
|
+
push_var_u32(out, bytes.len(), field_name)?;
|
|
1075
|
+
out.extend_from_slice(bytes);
|
|
1076
|
+
Ok(())
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
fn push_var_entity_identity(out: &mut Vec<u8>, identity: &EntityIdentity) -> Result<(), LixError> {
|
|
1080
|
+
assert!(
|
|
1081
|
+
!identity.parts.is_empty(),
|
|
1082
|
+
"tracked-state delta key entity identity must contain at least one part"
|
|
1083
|
+
);
|
|
1084
|
+
push_var_u32(out, identity.parts.len(), "entity identity part count")?;
|
|
1085
|
+
for part in &identity.parts {
|
|
1086
|
+
push_var_sized_bytes(out, part.as_bytes(), "entity identity string part")?;
|
|
1087
|
+
}
|
|
1088
|
+
Ok(())
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
fn push_optional_json_ref(out: &mut Vec<u8>, json_ref: Option<&JsonRef>) {
|
|
1092
|
+
match json_ref {
|
|
1093
|
+
Some(json_ref) => {
|
|
1094
|
+
out.push(1);
|
|
1095
|
+
out.extend_from_slice(json_ref.as_hash_bytes());
|
|
1096
|
+
}
|
|
1097
|
+
None => out.push(0),
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
fn push_mixed_optional_json_ref(
|
|
1102
|
+
out: &mut Vec<u8>,
|
|
1103
|
+
indexes: &HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>,
|
|
1104
|
+
json_ref: Option<&JsonRef>,
|
|
1105
|
+
) -> Result<(), LixError> {
|
|
1106
|
+
let Some(json_ref) = json_ref else {
|
|
1107
|
+
out.push(DELTA_JSON_REF_NONE);
|
|
1108
|
+
return Ok(());
|
|
1109
|
+
};
|
|
1110
|
+
if let Some(index) = indexes.get(json_ref.as_hash_array()).copied() {
|
|
1111
|
+
out.push(DELTA_JSON_REF_PACK_INDEX);
|
|
1112
|
+
push_var_u32(out, index, "json ref pack index")
|
|
1113
|
+
} else {
|
|
1114
|
+
out.push(DELTA_JSON_REF_INLINE);
|
|
1115
|
+
out.extend_from_slice(json_ref.as_hash_bytes());
|
|
1116
|
+
Ok(())
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
fn push_var_delta_change_id(
|
|
1121
|
+
out: &mut Vec<u8>,
|
|
1122
|
+
source_commit_id: &str,
|
|
1123
|
+
change_id: &str,
|
|
1124
|
+
) -> Result<(), LixError> {
|
|
1125
|
+
if let Some(suffix) = change_id.strip_prefix(source_commit_id) {
|
|
1126
|
+
out.push(DELTA_CHANGE_ID_COMMIT_SUFFIX);
|
|
1127
|
+
push_var_sized_bytes(out, suffix.as_bytes(), "change_id")
|
|
1128
|
+
} else {
|
|
1129
|
+
out.push(DELTA_CHANGE_ID_FULL);
|
|
1130
|
+
push_var_sized_bytes(out, change_id.as_bytes(), "change_id")
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
fn read_var_delta_change_id(
|
|
1135
|
+
bytes: &[u8],
|
|
1136
|
+
cursor: &mut usize,
|
|
1137
|
+
source_commit_id: &str,
|
|
1138
|
+
) -> Result<String, LixError> {
|
|
1139
|
+
let tag = read_u8(bytes, cursor, "delta change_id tag")?;
|
|
1140
|
+
let value = read_var_sized_string(bytes, cursor, "change_id")?;
|
|
1141
|
+
match tag {
|
|
1142
|
+
DELTA_CHANGE_ID_FULL => Ok(value),
|
|
1143
|
+
DELTA_CHANGE_ID_COMMIT_SUFFIX => Ok(format!("{source_commit_id}{value}")),
|
|
1144
|
+
other => Err(LixError::new(
|
|
1145
|
+
"LIX_ERROR_UNKNOWN",
|
|
1146
|
+
format!("tracked-state delta value has invalid change_id tag {other}"),
|
|
1147
|
+
)),
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
#[cfg(test)]
|
|
1152
|
+
fn optional_json_ref_len(json_ref: Option<&JsonRef>) -> usize {
|
|
1153
|
+
1 + json_ref.map_or(0, |_| TRACKED_STATE_HASH_BYTES)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
fn push_timestamp_pair(out: &mut Vec<u8>, created_at: &str, updated_at: &str) {
|
|
1157
|
+
push_sized_bytes(out, created_at.as_bytes());
|
|
1158
|
+
if updated_at == created_at {
|
|
1159
|
+
out.push(TIMESTAMP_UPDATED_SAME);
|
|
1160
|
+
} else {
|
|
1161
|
+
out.push(TIMESTAMP_UPDATED_DISTINCT);
|
|
1162
|
+
push_sized_bytes(out, updated_at.as_bytes());
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
fn push_var_timestamp_pair(
|
|
1167
|
+
out: &mut Vec<u8>,
|
|
1168
|
+
created_at: &str,
|
|
1169
|
+
updated_at: &str,
|
|
1170
|
+
) -> Result<(), LixError> {
|
|
1171
|
+
push_var_sized_bytes(out, created_at.as_bytes(), "created_at")?;
|
|
1172
|
+
if updated_at == created_at {
|
|
1173
|
+
out.push(TIMESTAMP_UPDATED_SAME);
|
|
1174
|
+
} else {
|
|
1175
|
+
out.push(TIMESTAMP_UPDATED_DISTINCT);
|
|
1176
|
+
push_var_sized_bytes(out, updated_at.as_bytes(), "updated_at")?;
|
|
1177
|
+
}
|
|
1178
|
+
Ok(())
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
#[cfg(test)]
|
|
1182
|
+
fn timestamp_pair_len(created_at: &str, updated_at: &str) -> usize {
|
|
1183
|
+
sized_bytes_len(created_at.as_bytes())
|
|
1184
|
+
+ 1
|
|
1185
|
+
+ if updated_at == created_at {
|
|
1186
|
+
0
|
|
1187
|
+
} else {
|
|
1188
|
+
sized_bytes_len(updated_at.as_bytes())
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
fn read_timestamp_pair(bytes: &[u8], cursor: &mut usize) -> Result<(String, String), LixError> {
|
|
1193
|
+
let created_at = read_sized_string(bytes, cursor, "created_at")?;
|
|
1194
|
+
let updated_at = match read_u8(bytes, cursor, "updated_at tag")? {
|
|
1195
|
+
TIMESTAMP_UPDATED_SAME => created_at.clone(),
|
|
1196
|
+
TIMESTAMP_UPDATED_DISTINCT => read_sized_string(bytes, cursor, "updated_at")?,
|
|
1197
|
+
other => {
|
|
1198
|
+
return Err(LixError::new(
|
|
1199
|
+
"LIX_ERROR_UNKNOWN",
|
|
1200
|
+
format!("tracked-state timestamp pair has invalid updated_at tag {other}"),
|
|
1201
|
+
))
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
Ok((created_at, updated_at))
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
fn read_var_timestamp_pair(bytes: &[u8], cursor: &mut usize) -> Result<(String, String), LixError> {
|
|
1208
|
+
let created_at = read_var_sized_string(bytes, cursor, "created_at")?;
|
|
1209
|
+
let updated_at = match read_u8(bytes, cursor, "updated_at tag")? {
|
|
1210
|
+
TIMESTAMP_UPDATED_SAME => created_at.clone(),
|
|
1211
|
+
TIMESTAMP_UPDATED_DISTINCT => read_var_sized_string(bytes, cursor, "updated_at")?,
|
|
1212
|
+
other => {
|
|
1213
|
+
return Err(LixError::new(
|
|
1214
|
+
"LIX_ERROR_UNKNOWN",
|
|
1215
|
+
format!("tracked-state timestamp pair has invalid updated_at tag {other}"),
|
|
1216
|
+
))
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
Ok((created_at, updated_at))
|
|
1220
|
+
}
|
|
1221
|
+
|
|
462
1222
|
fn push_u32(out: &mut Vec<u8>, value: usize) {
|
|
463
1223
|
out.extend_from_slice(&(value as u32).to_be_bytes());
|
|
464
1224
|
}
|
|
@@ -481,6 +1241,14 @@ fn read_sized_bytes(
|
|
|
481
1241
|
cursor: &mut usize,
|
|
482
1242
|
field_name: &str,
|
|
483
1243
|
) -> Result<Vec<u8>, LixError> {
|
|
1244
|
+
read_sized_slice(bytes, cursor, field_name).map(<[u8]>::to_vec)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
fn read_sized_slice<'a>(
|
|
1248
|
+
bytes: &'a [u8],
|
|
1249
|
+
cursor: &mut usize,
|
|
1250
|
+
field_name: &str,
|
|
1251
|
+
) -> Result<&'a [u8], LixError> {
|
|
484
1252
|
let len = read_u32(bytes, cursor, field_name)?;
|
|
485
1253
|
let end = cursor.checked_add(len).ok_or_else(|| {
|
|
486
1254
|
LixError::new(
|
|
@@ -495,7 +1263,61 @@ fn read_sized_bytes(
|
|
|
495
1263
|
)
|
|
496
1264
|
})?;
|
|
497
1265
|
*cursor = end;
|
|
498
|
-
Ok(slice
|
|
1266
|
+
Ok(slice)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
fn read_var_sized_string(
|
|
1270
|
+
bytes: &[u8],
|
|
1271
|
+
cursor: &mut usize,
|
|
1272
|
+
field_name: &str,
|
|
1273
|
+
) -> Result<String, LixError> {
|
|
1274
|
+
String::from_utf8(read_var_sized_slice(bytes, cursor, field_name)?.to_vec()).map_err(|error| {
|
|
1275
|
+
LixError::new(
|
|
1276
|
+
"LIX_ERROR_UNKNOWN",
|
|
1277
|
+
format!("tracked-state delta pack field '{field_name}' is invalid UTF-8: {error}"),
|
|
1278
|
+
)
|
|
1279
|
+
})
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
fn read_var_sized_slice<'a>(
|
|
1283
|
+
bytes: &'a [u8],
|
|
1284
|
+
cursor: &mut usize,
|
|
1285
|
+
field_name: &str,
|
|
1286
|
+
) -> Result<&'a [u8], LixError> {
|
|
1287
|
+
let len = read_var_u32(bytes, cursor, field_name)?;
|
|
1288
|
+
let end = cursor.checked_add(len).ok_or_else(|| {
|
|
1289
|
+
LixError::new(
|
|
1290
|
+
"LIX_ERROR_UNKNOWN",
|
|
1291
|
+
format!("tracked-state delta pack field '{field_name}' length overflow"),
|
|
1292
|
+
)
|
|
1293
|
+
})?;
|
|
1294
|
+
let slice = bytes.get(*cursor..end).ok_or_else(|| {
|
|
1295
|
+
LixError::new(
|
|
1296
|
+
"LIX_ERROR_UNKNOWN",
|
|
1297
|
+
format!("tracked-state delta pack field '{field_name}' is truncated"),
|
|
1298
|
+
)
|
|
1299
|
+
})?;
|
|
1300
|
+
*cursor = end;
|
|
1301
|
+
Ok(slice)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
fn read_var_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdentity, LixError> {
|
|
1305
|
+
let count = read_var_u32(bytes, cursor, "entity identity part count")?;
|
|
1306
|
+
let mut parts = Vec::new();
|
|
1307
|
+
for _ in 0..count {
|
|
1308
|
+
parts.push(read_var_sized_string(
|
|
1309
|
+
bytes,
|
|
1310
|
+
cursor,
|
|
1311
|
+
"entity identity string part",
|
|
1312
|
+
)?);
|
|
1313
|
+
}
|
|
1314
|
+
if parts.is_empty() {
|
|
1315
|
+
return Err(LixError::new(
|
|
1316
|
+
"LIX_ERROR_UNKNOWN",
|
|
1317
|
+
"tracked-state delta key entity identity must contain at least one part",
|
|
1318
|
+
));
|
|
1319
|
+
}
|
|
1320
|
+
Ok(EntityIdentity { parts })
|
|
499
1321
|
}
|
|
500
1322
|
|
|
501
1323
|
fn read_fixed_hash(
|
|
@@ -516,6 +1338,51 @@ fn read_fixed_hash(
|
|
|
516
1338
|
Ok(out)
|
|
517
1339
|
}
|
|
518
1340
|
|
|
1341
|
+
fn read_optional_json_ref(
|
|
1342
|
+
bytes: &[u8],
|
|
1343
|
+
cursor: &mut usize,
|
|
1344
|
+
field_name: &str,
|
|
1345
|
+
) -> Result<Option<JsonRef>, LixError> {
|
|
1346
|
+
match read_u8(bytes, cursor, field_name)? {
|
|
1347
|
+
0 => Ok(None),
|
|
1348
|
+
1 => Ok(Some(JsonRef::from_hash_bytes(read_fixed_hash(
|
|
1349
|
+
bytes, cursor, field_name,
|
|
1350
|
+
)?))),
|
|
1351
|
+
other => Err(LixError::new(
|
|
1352
|
+
"LIX_ERROR_UNKNOWN",
|
|
1353
|
+
format!("tracked-state tree field '{field_name}' has invalid JSON ref tag {other}"),
|
|
1354
|
+
)),
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
fn read_mixed_optional_json_ref(
|
|
1359
|
+
bytes: &[u8],
|
|
1360
|
+
cursor: &mut usize,
|
|
1361
|
+
refs: &[JsonRef],
|
|
1362
|
+
field_name: &str,
|
|
1363
|
+
) -> Result<Option<JsonRef>, LixError> {
|
|
1364
|
+
match read_u8(bytes, cursor, field_name)? {
|
|
1365
|
+
DELTA_JSON_REF_NONE => Ok(None),
|
|
1366
|
+
DELTA_JSON_REF_PACK_INDEX => {
|
|
1367
|
+
let index = read_var_u32(bytes, cursor, field_name)?;
|
|
1368
|
+
refs.get(index).copied().map(Some).ok_or_else(|| {
|
|
1369
|
+
LixError::new(
|
|
1370
|
+
"LIX_ERROR_UNKNOWN",
|
|
1371
|
+
format!("tracked-state delta JSON ref index {index} is out of bounds"),
|
|
1372
|
+
)
|
|
1373
|
+
})
|
|
1374
|
+
}
|
|
1375
|
+
DELTA_JSON_REF_INLINE => {
|
|
1376
|
+
let hash = read_fixed_hash(bytes, cursor, field_name)?;
|
|
1377
|
+
Ok(Some(JsonRef::from_hash_bytes(hash)))
|
|
1378
|
+
}
|
|
1379
|
+
other => Err(LixError::new(
|
|
1380
|
+
"LIX_ERROR_UNKNOWN",
|
|
1381
|
+
format!("tracked-state tree field '{field_name}' has invalid JSON ref tag {other}"),
|
|
1382
|
+
)),
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
519
1386
|
fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, LixError> {
|
|
520
1387
|
let value = *bytes.get(*cursor).ok_or_else(|| {
|
|
521
1388
|
LixError::new(
|
|
@@ -527,6 +1394,35 @@ fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, Lix
|
|
|
527
1394
|
Ok(value)
|
|
528
1395
|
}
|
|
529
1396
|
|
|
1397
|
+
fn read_var_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
|
|
1398
|
+
let mut value = 0u32;
|
|
1399
|
+
let mut shift = 0u32;
|
|
1400
|
+
for byte_index in 0..5 {
|
|
1401
|
+
let byte = read_u8(bytes, cursor, field_name)?;
|
|
1402
|
+
if shift == 28 && (byte & 0x80 != 0 || byte & 0x70 != 0) {
|
|
1403
|
+
return Err(LixError::new(
|
|
1404
|
+
"LIX_ERROR_UNKNOWN",
|
|
1405
|
+
format!("tracked-state delta pack field '{field_name}' varint exceeds u32"),
|
|
1406
|
+
));
|
|
1407
|
+
}
|
|
1408
|
+
if byte_index > 0 && byte & 0x80 == 0 && byte == 0 {
|
|
1409
|
+
return Err(LixError::new(
|
|
1410
|
+
"LIX_ERROR_UNKNOWN",
|
|
1411
|
+
format!("tracked-state delta pack field '{field_name}' has non-canonical varint"),
|
|
1412
|
+
));
|
|
1413
|
+
}
|
|
1414
|
+
value |= ((byte & 0x7f) as u32) << shift;
|
|
1415
|
+
if byte & 0x80 == 0 {
|
|
1416
|
+
return Ok(value as usize);
|
|
1417
|
+
}
|
|
1418
|
+
shift += 7;
|
|
1419
|
+
}
|
|
1420
|
+
Err(LixError::new(
|
|
1421
|
+
"LIX_ERROR_UNKNOWN",
|
|
1422
|
+
format!("tracked-state delta pack field '{field_name}' varint exceeds u32"),
|
|
1423
|
+
))
|
|
1424
|
+
}
|
|
1425
|
+
|
|
530
1426
|
fn read_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
|
|
531
1427
|
let end = *cursor + 4;
|
|
532
1428
|
let slice = bytes.get(*cursor..end).ok_or_else(|| {
|
|
@@ -592,15 +1488,15 @@ mod tests {
|
|
|
592
1488
|
}
|
|
593
1489
|
|
|
594
1490
|
#[test]
|
|
595
|
-
fn
|
|
1491
|
+
fn key_codec_encodes_composite_identity_as_string_tuple_parts() {
|
|
596
1492
|
let key = TrackedStateKey {
|
|
597
1493
|
schema_key: "schema".to_string(),
|
|
598
1494
|
file_id: None,
|
|
599
1495
|
entity_id: EntityIdentity {
|
|
600
1496
|
parts: vec![
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1497
|
+
"namespace".to_string(),
|
|
1498
|
+
"true".to_string(),
|
|
1499
|
+
"42".to_string(),
|
|
604
1500
|
],
|
|
605
1501
|
},
|
|
606
1502
|
};
|
|
@@ -608,32 +1504,45 @@ mod tests {
|
|
|
608
1504
|
let encoded = encode_key(&key);
|
|
609
1505
|
|
|
610
1506
|
assert_eq!(decode_key(&encoded).expect("key should decode"), key);
|
|
611
|
-
assert!(
|
|
612
|
-
!encoded
|
|
613
|
-
.windows(b"pk:v1:".len())
|
|
614
|
-
.any(|window| window == b"pk:v1:"),
|
|
615
|
-
"tracked-state keys should not store the SQL entity_id projection"
|
|
616
|
-
);
|
|
617
1507
|
}
|
|
618
1508
|
|
|
619
1509
|
#[test]
|
|
620
|
-
fn
|
|
621
|
-
let
|
|
1510
|
+
fn key_codec_decodes_entity_suffix_with_trusted_prefix() {
|
|
1511
|
+
let key = TrackedStateKey {
|
|
622
1512
|
schema_key: "schema".to_string(),
|
|
623
|
-
file_id:
|
|
1513
|
+
file_id: Some("file".to_string()),
|
|
624
1514
|
entity_id: EntityIdentity {
|
|
625
|
-
parts: vec![
|
|
1515
|
+
parts: vec!["namespace".to_string(), "id".to_string()],
|
|
626
1516
|
},
|
|
627
|
-
}
|
|
628
|
-
let
|
|
1517
|
+
};
|
|
1518
|
+
let encoded = encode_key(&key);
|
|
1519
|
+
let prefix = encode_schema_file_prefix("schema", Some("file"));
|
|
1520
|
+
|
|
1521
|
+
assert_eq!(
|
|
1522
|
+
decode_key_with_trusted_prefix(&encoded, "schema", Some("file"), prefix.len())
|
|
1523
|
+
.expect("key suffix should decode"),
|
|
1524
|
+
key
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
#[test]
|
|
1529
|
+
fn key_codec_rejects_non_string_identity_part_tags() {
|
|
1530
|
+
let mut encoded = encode_key(&TrackedStateKey {
|
|
629
1531
|
schema_key: "schema".to_string(),
|
|
630
1532
|
file_id: None,
|
|
631
1533
|
entity_id: EntityIdentity {
|
|
632
|
-
parts: vec![
|
|
1534
|
+
parts: vec!["true".to_string()],
|
|
633
1535
|
},
|
|
634
1536
|
});
|
|
1537
|
+
let schema_key_len = "schema".len();
|
|
1538
|
+
let file_scope_offset = 4 + schema_key_len;
|
|
1539
|
+
let entity_tag_offset = file_scope_offset + 1;
|
|
1540
|
+
encoded[entity_tag_offset] = 2;
|
|
635
1541
|
|
|
636
|
-
|
|
1542
|
+
let error = decode_key(&encoded).expect_err("non-string identity tag should reject");
|
|
1543
|
+
assert!(error
|
|
1544
|
+
.to_string()
|
|
1545
|
+
.contains("invalid entity identity part tag 2"));
|
|
637
1546
|
}
|
|
638
1547
|
|
|
639
1548
|
#[test]
|
|
@@ -642,17 +1551,14 @@ mod tests {
|
|
|
642
1551
|
schema_key: "schema".to_string(),
|
|
643
1552
|
file_id: None,
|
|
644
1553
|
entity_id: EntityIdentity {
|
|
645
|
-
parts: vec![
|
|
1554
|
+
parts: vec!["a".to_string()],
|
|
646
1555
|
},
|
|
647
1556
|
});
|
|
648
1557
|
let extended = encode_key(&TrackedStateKey {
|
|
649
1558
|
schema_key: "schema".to_string(),
|
|
650
1559
|
file_id: None,
|
|
651
1560
|
entity_id: EntityIdentity {
|
|
652
|
-
parts: vec![
|
|
653
|
-
EntityIdentityPart::String("a".to_string()),
|
|
654
|
-
EntityIdentityPart::String("b".to_string()),
|
|
655
|
-
],
|
|
1561
|
+
parts: vec!["a".to_string(), "b".to_string()],
|
|
656
1562
|
},
|
|
657
1563
|
});
|
|
658
1564
|
|
|
@@ -660,16 +1566,19 @@ mod tests {
|
|
|
660
1566
|
}
|
|
661
1567
|
|
|
662
1568
|
#[test]
|
|
663
|
-
fn
|
|
664
|
-
let value =
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1569
|
+
fn value_codec_roundtrips_locator_value() {
|
|
1570
|
+
let value = TrackedStateIndexValue {
|
|
1571
|
+
change_locator: ChangeLocator {
|
|
1572
|
+
source_commit_id: "commit".to_string(),
|
|
1573
|
+
source_pack_id: 7,
|
|
1574
|
+
source_ordinal: 11,
|
|
1575
|
+
change_id: "change".to_string(),
|
|
1576
|
+
},
|
|
1577
|
+
deleted: false,
|
|
1578
|
+
snapshot_ref: Some(JsonRef::from_hash_bytes([1; 32])),
|
|
1579
|
+
metadata_ref: Some(JsonRef::from_hash_bytes([2; 32])),
|
|
668
1580
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
669
1581
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
670
|
-
change_id: "change".to_string(),
|
|
671
|
-
commit_id: "commit".to_string(),
|
|
672
|
-
deleted: true,
|
|
673
1582
|
};
|
|
674
1583
|
|
|
675
1584
|
let encoded = encode_value(&value);
|
|
@@ -677,54 +1586,397 @@ mod tests {
|
|
|
677
1586
|
}
|
|
678
1587
|
|
|
679
1588
|
#[test]
|
|
680
|
-
fn
|
|
681
|
-
let value =
|
|
682
|
-
|
|
1589
|
+
fn value_codec_roundtrips_second_locator_value() {
|
|
1590
|
+
let value = TrackedStateIndexValue {
|
|
1591
|
+
change_locator: ChangeLocator {
|
|
1592
|
+
source_commit_id: "other-commit".to_string(),
|
|
1593
|
+
source_pack_id: 0,
|
|
1594
|
+
source_ordinal: 1,
|
|
1595
|
+
change_id: "other-change".to_string(),
|
|
1596
|
+
},
|
|
1597
|
+
deleted: true,
|
|
1598
|
+
snapshot_ref: None,
|
|
683
1599
|
metadata_ref: None,
|
|
684
|
-
schema_version: "1".to_string(),
|
|
685
1600
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
686
1601
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
687
|
-
change_id: "change".to_string(),
|
|
688
|
-
commit_id: "commit".to_string(),
|
|
689
|
-
deleted: false,
|
|
690
1602
|
};
|
|
691
1603
|
|
|
692
1604
|
let encoded = encode_value(&value);
|
|
693
1605
|
assert_eq!(decode_value(&encoded).expect("value"), value);
|
|
694
1606
|
}
|
|
695
1607
|
|
|
1608
|
+
#[test]
|
|
1609
|
+
fn value_codec_compacts_matching_timestamps() {
|
|
1610
|
+
let mut compact = TrackedStateIndexValue {
|
|
1611
|
+
change_locator: ChangeLocator {
|
|
1612
|
+
source_commit_id: "commit".to_string(),
|
|
1613
|
+
source_pack_id: 0,
|
|
1614
|
+
source_ordinal: 1,
|
|
1615
|
+
change_id: "change".to_string(),
|
|
1616
|
+
},
|
|
1617
|
+
deleted: false,
|
|
1618
|
+
snapshot_ref: None,
|
|
1619
|
+
metadata_ref: None,
|
|
1620
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1621
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1622
|
+
};
|
|
1623
|
+
let compact_len = encode_value(&compact).len();
|
|
1624
|
+
assert_eq!(
|
|
1625
|
+
decode_value(&encode_value(&compact)).expect("value"),
|
|
1626
|
+
compact
|
|
1627
|
+
);
|
|
1628
|
+
|
|
1629
|
+
compact.updated_at = "2026-01-02T00:00:00Z".to_string();
|
|
1630
|
+
let distinct_len = encode_value(&compact).len();
|
|
1631
|
+
|
|
1632
|
+
assert!(compact_len < distinct_len);
|
|
1633
|
+
assert_eq!(
|
|
1634
|
+
distinct_len - compact_len,
|
|
1635
|
+
sized_bytes_len(compact.updated_at.as_bytes())
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
#[test]
|
|
1640
|
+
fn delta_pack_ref_encoder_roundtrips_entries() {
|
|
1641
|
+
let entity_id = EntityIdentity {
|
|
1642
|
+
parts: vec!["entity-a".to_string()],
|
|
1643
|
+
};
|
|
1644
|
+
let snapshot_ref = JsonRef::from_hash_bytes([1; 32]);
|
|
1645
|
+
let metadata_ref = JsonRef::from_hash_bytes([2; 32]);
|
|
1646
|
+
let live_change = crate::commit_store::ChangeRef {
|
|
1647
|
+
id: "commit-a:change-live",
|
|
1648
|
+
entity_id: &entity_id,
|
|
1649
|
+
schema_key: "schema",
|
|
1650
|
+
file_id: Some("file-a"),
|
|
1651
|
+
snapshot_ref: Some(&snapshot_ref),
|
|
1652
|
+
metadata_ref: Some(&metadata_ref),
|
|
1653
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1654
|
+
};
|
|
1655
|
+
let tombstone_change = crate::commit_store::ChangeRef {
|
|
1656
|
+
id: "change-deleted",
|
|
1657
|
+
entity_id: &entity_id,
|
|
1658
|
+
schema_key: "schema",
|
|
1659
|
+
file_id: None,
|
|
1660
|
+
snapshot_ref: None,
|
|
1661
|
+
metadata_ref: None,
|
|
1662
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1663
|
+
};
|
|
1664
|
+
let live_locator = crate::commit_store::ChangeLocatorRef {
|
|
1665
|
+
source_commit_id: "commit-a",
|
|
1666
|
+
source_pack_id: 3,
|
|
1667
|
+
source_ordinal: 5,
|
|
1668
|
+
change_id: "commit-a:change-live",
|
|
1669
|
+
};
|
|
1670
|
+
let tombstone_locator = crate::commit_store::ChangeLocatorRef {
|
|
1671
|
+
source_commit_id: "source-commit",
|
|
1672
|
+
source_pack_id: 3,
|
|
1673
|
+
source_ordinal: 6,
|
|
1674
|
+
change_id: "commit-a:borrowed",
|
|
1675
|
+
};
|
|
1676
|
+
let encoded = encode_delta_pack_refs(
|
|
1677
|
+
"commit-a",
|
|
1678
|
+
&[
|
|
1679
|
+
TrackedStateDeltaRef {
|
|
1680
|
+
change: live_change,
|
|
1681
|
+
locator: live_locator,
|
|
1682
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1683
|
+
updated_at: "2026-01-02T00:00:00Z",
|
|
1684
|
+
},
|
|
1685
|
+
TrackedStateDeltaRef {
|
|
1686
|
+
change: tombstone_change,
|
|
1687
|
+
locator: tombstone_locator,
|
|
1688
|
+
created_at: "2026-01-03T00:00:00Z",
|
|
1689
|
+
updated_at: "2026-01-04T00:00:00Z",
|
|
1690
|
+
},
|
|
1691
|
+
],
|
|
1692
|
+
)
|
|
1693
|
+
.expect("delta pack should encode");
|
|
1694
|
+
|
|
1695
|
+
let mut cursor = 5usize;
|
|
1696
|
+
assert_eq!(
|
|
1697
|
+
read_var_sized_string(&encoded, &mut cursor, "delta pack commit_id")
|
|
1698
|
+
.expect("commit id should decode"),
|
|
1699
|
+
"commit-a"
|
|
1700
|
+
);
|
|
1701
|
+
assert_eq!(
|
|
1702
|
+
read_var_u32(&encoded, &mut cursor, "delta key prefix count")
|
|
1703
|
+
.expect("prefix count should decode"),
|
|
1704
|
+
2
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
let (decoded_commit_id, decoded) =
|
|
1708
|
+
decode_delta_pack(&encoded, None).expect("delta pack should decode");
|
|
1709
|
+
|
|
1710
|
+
assert_eq!(decoded_commit_id, "commit-a");
|
|
1711
|
+
assert_eq!(
|
|
1712
|
+
decoded,
|
|
1713
|
+
vec![
|
|
1714
|
+
TrackedStateDeltaEntry {
|
|
1715
|
+
key: TrackedStateKey {
|
|
1716
|
+
schema_key: "schema".to_string(),
|
|
1717
|
+
file_id: Some("file-a".to_string()),
|
|
1718
|
+
entity_id: entity_id.clone(),
|
|
1719
|
+
},
|
|
1720
|
+
value: TrackedStateIndexValue {
|
|
1721
|
+
change_locator: ChangeLocator {
|
|
1722
|
+
source_commit_id: "commit-a".to_string(),
|
|
1723
|
+
source_pack_id: 3,
|
|
1724
|
+
source_ordinal: 5,
|
|
1725
|
+
change_id: "commit-a:change-live".to_string(),
|
|
1726
|
+
},
|
|
1727
|
+
deleted: false,
|
|
1728
|
+
snapshot_ref: Some(snapshot_ref),
|
|
1729
|
+
metadata_ref: Some(metadata_ref),
|
|
1730
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1731
|
+
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
1734
|
+
TrackedStateDeltaEntry {
|
|
1735
|
+
key: TrackedStateKey {
|
|
1736
|
+
schema_key: "schema".to_string(),
|
|
1737
|
+
file_id: None,
|
|
1738
|
+
entity_id,
|
|
1739
|
+
},
|
|
1740
|
+
value: TrackedStateIndexValue {
|
|
1741
|
+
change_locator: ChangeLocator {
|
|
1742
|
+
source_commit_id: "source-commit".to_string(),
|
|
1743
|
+
source_pack_id: 3,
|
|
1744
|
+
source_ordinal: 6,
|
|
1745
|
+
change_id: "commit-a:borrowed".to_string(),
|
|
1746
|
+
},
|
|
1747
|
+
deleted: true,
|
|
1748
|
+
snapshot_ref: None,
|
|
1749
|
+
metadata_ref: None,
|
|
1750
|
+
created_at: "2026-01-03T00:00:00Z".to_string(),
|
|
1751
|
+
updated_at: "2026-01-04T00:00:00Z".to_string(),
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1754
|
+
]
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
#[test]
|
|
1759
|
+
fn delta_pack_ref_encoder_roundtrips_mixed_json_pack_indexes() {
|
|
1760
|
+
let entity_id = EntityIdentity::single("entity-a");
|
|
1761
|
+
let snapshot_ref = JsonRef::from_hash_bytes([1; 32]);
|
|
1762
|
+
let metadata_ref = JsonRef::from_hash_bytes([2; 32]);
|
|
1763
|
+
let change = crate::commit_store::ChangeRef {
|
|
1764
|
+
id: "commit-a:change-live",
|
|
1765
|
+
entity_id: &entity_id,
|
|
1766
|
+
schema_key: "schema",
|
|
1767
|
+
file_id: Some("file-a"),
|
|
1768
|
+
snapshot_ref: Some(&snapshot_ref),
|
|
1769
|
+
metadata_ref: Some(&metadata_ref),
|
|
1770
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1771
|
+
};
|
|
1772
|
+
let locator = crate::commit_store::ChangeLocatorRef {
|
|
1773
|
+
source_commit_id: "commit-a",
|
|
1774
|
+
source_pack_id: 0,
|
|
1775
|
+
source_ordinal: 0,
|
|
1776
|
+
change_id: "commit-a:change-live",
|
|
1777
|
+
};
|
|
1778
|
+
let delta = TrackedStateDeltaRef {
|
|
1779
|
+
change,
|
|
1780
|
+
locator,
|
|
1781
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1782
|
+
updated_at: "2026-01-01T00:00:00Z",
|
|
1783
|
+
};
|
|
1784
|
+
let mut pack_indexes = HashMap::new();
|
|
1785
|
+
pack_indexes.insert(*snapshot_ref.as_hash_array(), 1);
|
|
1786
|
+
let pack_refs = vec![JsonRef::from_hash_bytes([9; 32]), snapshot_ref];
|
|
1787
|
+
|
|
1788
|
+
let inline = encode_delta_pack_refs("commit-a", &[delta]).expect("inline delta pack");
|
|
1789
|
+
assert!(!delta_pack_uses_json_pack_indexes(&inline).expect("inline mode should peek"));
|
|
1790
|
+
let empty_indexes = HashMap::new();
|
|
1791
|
+
let empty_index_pack = encode_delta_pack_refs_with_json_pack_indexes(
|
|
1792
|
+
"commit-a",
|
|
1793
|
+
&[delta],
|
|
1794
|
+
Some(&empty_indexes),
|
|
1795
|
+
)
|
|
1796
|
+
.expect("empty-index delta pack");
|
|
1797
|
+
assert_eq!(empty_index_pack, inline);
|
|
1798
|
+
assert!(!delta_pack_uses_json_pack_indexes(&empty_index_pack)
|
|
1799
|
+
.expect("empty index mode should peek"));
|
|
1800
|
+
decode_delta_pack(&empty_index_pack, None).expect("empty index pack should decode inline");
|
|
1801
|
+
|
|
1802
|
+
let mixed = encode_delta_pack_refs_with_json_pack_indexes(
|
|
1803
|
+
"commit-a",
|
|
1804
|
+
&[delta],
|
|
1805
|
+
Some(&pack_indexes),
|
|
1806
|
+
)
|
|
1807
|
+
.expect("mixed delta pack");
|
|
1808
|
+
assert!(delta_pack_uses_json_pack_indexes(&mixed).expect("mixed mode should peek"));
|
|
1809
|
+
|
|
1810
|
+
assert!(
|
|
1811
|
+
mixed.len() < inline.len(),
|
|
1812
|
+
"pack-index refs should be smaller than inline refs"
|
|
1813
|
+
);
|
|
1814
|
+
assert!(decode_delta_pack(&mixed, None)
|
|
1815
|
+
.expect_err("mixed refs require JSON pack refs")
|
|
1816
|
+
.to_string()
|
|
1817
|
+
.contains("needs JSON pack refs"));
|
|
1818
|
+
let (_, decoded) =
|
|
1819
|
+
decode_delta_pack(&mixed, Some(&pack_refs)).expect("mixed delta pack should decode");
|
|
1820
|
+
assert_eq!(decoded[0].value.snapshot_ref, Some(snapshot_ref));
|
|
1821
|
+
assert_eq!(decoded[0].value.metadata_ref, Some(metadata_ref));
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
#[test]
|
|
1825
|
+
fn delta_pack_stream_decoder_rejects_trailing_entry_bytes() {
|
|
1826
|
+
let entity_id = EntityIdentity::single("entity");
|
|
1827
|
+
let change = crate::commit_store::ChangeRef {
|
|
1828
|
+
id: "commit-a:change-0",
|
|
1829
|
+
entity_id: &entity_id,
|
|
1830
|
+
schema_key: "schema",
|
|
1831
|
+
file_id: None,
|
|
1832
|
+
snapshot_ref: None,
|
|
1833
|
+
metadata_ref: None,
|
|
1834
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1835
|
+
};
|
|
1836
|
+
let locator = crate::commit_store::ChangeLocatorRef {
|
|
1837
|
+
source_commit_id: "commit-a",
|
|
1838
|
+
source_pack_id: 0,
|
|
1839
|
+
source_ordinal: 0,
|
|
1840
|
+
change_id: "commit-a:change-0",
|
|
1841
|
+
};
|
|
1842
|
+
let mut encoded = encode_delta_pack_refs(
|
|
1843
|
+
"commit-a",
|
|
1844
|
+
&[TrackedStateDeltaRef {
|
|
1845
|
+
change,
|
|
1846
|
+
locator,
|
|
1847
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
1848
|
+
updated_at: "2026-01-01T00:00:00Z",
|
|
1849
|
+
}],
|
|
1850
|
+
)
|
|
1851
|
+
.expect("delta pack should encode");
|
|
1852
|
+
|
|
1853
|
+
let mut cursor = 5usize;
|
|
1854
|
+
let _ = read_var_sized_string(&encoded, &mut cursor, "delta pack commit_id")
|
|
1855
|
+
.expect("commit id should decode");
|
|
1856
|
+
assert_eq!(
|
|
1857
|
+
read_var_u32(&encoded, &mut cursor, "delta key prefix count")
|
|
1858
|
+
.expect("prefix count should decode"),
|
|
1859
|
+
1
|
|
1860
|
+
);
|
|
1861
|
+
let _ =
|
|
1862
|
+
decode_delta_key_prefix(&encoded, &mut cursor).expect("delta key prefix should decode");
|
|
1863
|
+
encoded[cursor] = 0;
|
|
1864
|
+
|
|
1865
|
+
let error =
|
|
1866
|
+
decode_delta_pack(&encoded, None).expect_err("trailing entry bytes should reject");
|
|
1867
|
+
assert!(
|
|
1868
|
+
error.to_string().contains("trailing bytes"),
|
|
1869
|
+
"error should mention trailing bytes: {error}"
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
#[test]
|
|
1874
|
+
fn delta_pack_rejects_overlong_varint() {
|
|
1875
|
+
let mut encoded = Vec::new();
|
|
1876
|
+
encoded.extend_from_slice(b"LXTD");
|
|
1877
|
+
encoded.push(DELTA_PACK_VERSION);
|
|
1878
|
+
encoded.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80]);
|
|
1879
|
+
|
|
1880
|
+
let error = decode_delta_pack(&encoded, None).expect_err("overlong varint should reject");
|
|
1881
|
+
assert!(
|
|
1882
|
+
error.to_string().contains("varint exceeds u32"),
|
|
1883
|
+
"error should mention overlong varint: {error}"
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
#[test]
|
|
1888
|
+
fn delta_pack_rejects_varint_above_u32() {
|
|
1889
|
+
let mut encoded = Vec::new();
|
|
1890
|
+
encoded.extend_from_slice(b"LXTD");
|
|
1891
|
+
encoded.push(DELTA_PACK_VERSION);
|
|
1892
|
+
encoded.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x1f]);
|
|
1893
|
+
|
|
1894
|
+
let error = decode_delta_pack(&encoded, None).expect_err("too-large varint should reject");
|
|
1895
|
+
assert!(
|
|
1896
|
+
error.to_string().contains("varint exceeds u32"),
|
|
1897
|
+
"error should mention oversized varint: {error}"
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
#[test]
|
|
1902
|
+
fn delta_pack_rejects_non_canonical_varint() {
|
|
1903
|
+
let mut encoded = Vec::new();
|
|
1904
|
+
encoded.extend_from_slice(b"LXTD");
|
|
1905
|
+
encoded.push(DELTA_PACK_VERSION);
|
|
1906
|
+
encoded.extend_from_slice(&[0x80, 0x00]);
|
|
1907
|
+
|
|
1908
|
+
let error =
|
|
1909
|
+
decode_delta_pack(&encoded, None).expect_err("non-canonical varint should reject");
|
|
1910
|
+
assert!(
|
|
1911
|
+
error.to_string().contains("non-canonical varint"),
|
|
1912
|
+
"error should mention non-canonical varint: {error}"
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
#[test]
|
|
1917
|
+
fn delta_key_decoder_rejects_out_of_bounds_prefix_index() {
|
|
1918
|
+
let mut encoded_key = Vec::new();
|
|
1919
|
+
push_var_u32(&mut encoded_key, 1, "delta key prefix index").expect("prefix index");
|
|
1920
|
+
push_var_entity_identity(&mut encoded_key, &EntityIdentity::single("entity"))
|
|
1921
|
+
.expect("entity identity");
|
|
1922
|
+
|
|
1923
|
+
let mut cursor = 0usize;
|
|
1924
|
+
let err = decode_delta_key(
|
|
1925
|
+
&encoded_key,
|
|
1926
|
+
&mut cursor,
|
|
1927
|
+
&[DeltaKeyPrefix {
|
|
1928
|
+
schema_key: "schema".to_string(),
|
|
1929
|
+
file_id: None,
|
|
1930
|
+
}],
|
|
1931
|
+
)
|
|
1932
|
+
.expect_err("out-of-bounds prefix index should reject");
|
|
1933
|
+
|
|
1934
|
+
assert!(err
|
|
1935
|
+
.to_string()
|
|
1936
|
+
.contains("tracked-state delta key prefix index 1 is out of bounds"));
|
|
1937
|
+
}
|
|
1938
|
+
|
|
696
1939
|
#[test]
|
|
697
1940
|
fn encoded_value_len_matches_encoded_value_bytes() {
|
|
698
1941
|
let values = [
|
|
699
|
-
|
|
1942
|
+
TrackedStateIndexValue {
|
|
1943
|
+
change_locator: ChangeLocator {
|
|
1944
|
+
source_commit_id: "commit".to_string(),
|
|
1945
|
+
source_pack_id: 0,
|
|
1946
|
+
source_ordinal: 0,
|
|
1947
|
+
change_id: "change".to_string(),
|
|
1948
|
+
},
|
|
1949
|
+
deleted: false,
|
|
700
1950
|
snapshot_ref: None,
|
|
701
1951
|
metadata_ref: None,
|
|
702
|
-
schema_version: "1".to_string(),
|
|
703
1952
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
704
1953
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
705
|
-
change_id: "change".to_string(),
|
|
706
|
-
commit_id: "commit".to_string(),
|
|
707
|
-
deleted: true,
|
|
708
1954
|
},
|
|
709
|
-
|
|
1955
|
+
TrackedStateIndexValue {
|
|
1956
|
+
change_locator: ChangeLocator {
|
|
1957
|
+
source_commit_id: "commit".to_string(),
|
|
1958
|
+
source_pack_id: 1,
|
|
1959
|
+
source_ordinal: 2,
|
|
1960
|
+
change_id: "change-2".to_string(),
|
|
1961
|
+
},
|
|
1962
|
+
deleted: true,
|
|
710
1963
|
snapshot_ref: Some(JsonRef::from_hash_bytes([3; 32])),
|
|
711
|
-
metadata_ref:
|
|
712
|
-
schema_version: "1".to_string(),
|
|
1964
|
+
metadata_ref: None,
|
|
713
1965
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
714
1966
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
715
|
-
change_id: "change".to_string(),
|
|
716
|
-
commit_id: "commit".to_string(),
|
|
717
|
-
deleted: false,
|
|
718
1967
|
},
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1968
|
+
TrackedStateIndexValue {
|
|
1969
|
+
change_locator: ChangeLocator {
|
|
1970
|
+
source_commit_id: "other".to_string(),
|
|
1971
|
+
source_pack_id: 4,
|
|
1972
|
+
source_ordinal: 8,
|
|
1973
|
+
change_id: "change-3".to_string(),
|
|
1974
|
+
},
|
|
1975
|
+
deleted: false,
|
|
1976
|
+
snapshot_ref: None,
|
|
1977
|
+
metadata_ref: Some(JsonRef::from_hash_bytes([4; 32])),
|
|
723
1978
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
724
1979
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
725
|
-
change_id: "change".to_string(),
|
|
726
|
-
commit_id: "commit".to_string(),
|
|
727
|
-
deleted: false,
|
|
728
1980
|
},
|
|
729
1981
|
];
|
|
730
1982
|
|
|
@@ -733,6 +1985,92 @@ mod tests {
|
|
|
733
1985
|
}
|
|
734
1986
|
}
|
|
735
1987
|
|
|
1988
|
+
#[test]
|
|
1989
|
+
fn leaf_node_codec_uses_indexable_offset_table() {
|
|
1990
|
+
let entries = vec![
|
|
1991
|
+
EncodedLeafEntry {
|
|
1992
|
+
key: b"alpha".to_vec(),
|
|
1993
|
+
value: b"one".to_vec(),
|
|
1994
|
+
},
|
|
1995
|
+
EncodedLeafEntry {
|
|
1996
|
+
key: b"bravo".to_vec(),
|
|
1997
|
+
value: b"two-two".to_vec(),
|
|
1998
|
+
},
|
|
1999
|
+
];
|
|
2000
|
+
|
|
2001
|
+
let encoded = encode_leaf_node(&entries);
|
|
2002
|
+
assert_eq!(encoded[0], NODE_KIND_LEAF);
|
|
2003
|
+
assert_eq!(encoded[1], NODE_VERSION);
|
|
2004
|
+
assert_eq!(&encoded[2..6], 2u32.to_be_bytes().as_slice());
|
|
2005
|
+
assert_eq!(&encoded[6..10], 0u32.to_be_bytes().as_slice());
|
|
2006
|
+
|
|
2007
|
+
let DecodedNodeRef::Leaf(leaf) = decode_node_ref(&encoded).expect("leaf ref") else {
|
|
2008
|
+
panic!("expected leaf node");
|
|
2009
|
+
};
|
|
2010
|
+
assert_eq!(leaf.len(), 2);
|
|
2011
|
+
assert_eq!(leaf.key(1).expect("second key"), Some(b"bravo".as_slice()));
|
|
2012
|
+
let second = leaf
|
|
2013
|
+
.entry(1)
|
|
2014
|
+
.expect("second entry")
|
|
2015
|
+
.expect("second entry exists");
|
|
2016
|
+
assert_eq!(second.key, b"bravo");
|
|
2017
|
+
assert_eq!(second.value, b"two-two");
|
|
2018
|
+
|
|
2019
|
+
let DecodedNode::Leaf(owned) = decode_node(&encoded).expect("owned leaf") else {
|
|
2020
|
+
panic!("expected owned leaf node");
|
|
2021
|
+
};
|
|
2022
|
+
assert_eq!(owned.entries(), entries.as_slice());
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
#[test]
|
|
2026
|
+
fn leaf_node_codec_roundtrips_empty_leaf() {
|
|
2027
|
+
let encoded = encode_leaf_node(&[]);
|
|
2028
|
+
assert_eq!(encoded.len(), 10);
|
|
2029
|
+
|
|
2030
|
+
let DecodedNodeRef::Leaf(leaf) = decode_node_ref(&encoded).expect("leaf ref") else {
|
|
2031
|
+
panic!("expected leaf node");
|
|
2032
|
+
};
|
|
2033
|
+
assert_eq!(leaf.len(), 0);
|
|
2034
|
+
assert!(leaf.entry(0).expect("missing entry").is_none());
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
#[test]
|
|
2038
|
+
fn leaf_node_codec_rejects_malformed_offsets() {
|
|
2039
|
+
let entries = vec![
|
|
2040
|
+
EncodedLeafEntry {
|
|
2041
|
+
key: b"alpha".to_vec(),
|
|
2042
|
+
value: b"one".to_vec(),
|
|
2043
|
+
},
|
|
2044
|
+
EncodedLeafEntry {
|
|
2045
|
+
key: b"bravo".to_vec(),
|
|
2046
|
+
value: b"two".to_vec(),
|
|
2047
|
+
},
|
|
2048
|
+
];
|
|
2049
|
+
let encoded = encode_leaf_node(&entries);
|
|
2050
|
+
|
|
2051
|
+
let mut non_zero_first = encoded.clone();
|
|
2052
|
+
non_zero_first[6..10].copy_from_slice(&1u32.to_be_bytes());
|
|
2053
|
+
assert!(decode_node_ref(&non_zero_first)
|
|
2054
|
+
.expect_err("non-zero first offset should reject")
|
|
2055
|
+
.to_string()
|
|
2056
|
+
.contains("offset table must start at zero"));
|
|
2057
|
+
|
|
2058
|
+
let mut non_monotonic = encoded.clone();
|
|
2059
|
+
non_monotonic[10..14].copy_from_slice(&100u32.to_be_bytes());
|
|
2060
|
+
assert!(decode_node_ref(&non_monotonic)
|
|
2061
|
+
.expect_err("non-monotonic offsets should reject")
|
|
2062
|
+
.to_string()
|
|
2063
|
+
.contains("offsets must be monotonic"));
|
|
2064
|
+
|
|
2065
|
+
let mut short_coverage = encoded;
|
|
2066
|
+
let payload_len = short_coverage.len() - 18;
|
|
2067
|
+
short_coverage[14..18].copy_from_slice(&((payload_len - 1) as u32).to_be_bytes());
|
|
2068
|
+
assert!(decode_node_ref(&short_coverage)
|
|
2069
|
+
.expect_err("short offset coverage should reject")
|
|
2070
|
+
.to_string()
|
|
2071
|
+
.contains("offset table does not cover full payload"));
|
|
2072
|
+
}
|
|
2073
|
+
|
|
736
2074
|
#[test]
|
|
737
2075
|
fn content_hash_is_blake3() {
|
|
738
2076
|
assert_eq!(hash_bytes(b"abc"), *blake3::hash(b"abc").as_bytes());
|