@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5
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/README.md +1 -1
- package/SKILL.md +65 -64
- package/dist/engine-wasm/index.js +4 -4
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
- package/dist/engine-wasm/wasm/lix_engine.js +130 -118
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
- package/dist/generated/builtin-schemas.d.ts +69 -69
- package/dist/generated/builtin-schemas.js +94 -94
- package/dist/open-lix.d.ts +33 -26
- package/dist/open-lix.js +10 -10
- package/dist/sqlite/index.js +86 -30
- package/dist-engine-src/README.md +3 -3
- package/dist-engine-src/src/backend/capabilities.rs +67 -0
- package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
- package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
- package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
- package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
- package/dist-engine-src/src/backend/conformance/model.rs +28 -0
- package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
- package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
- package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
- package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
- package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
- package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
- package/dist-engine-src/src/backend/conformance/write.rs +16 -0
- package/dist-engine-src/src/backend/error.rs +94 -0
- package/dist-engine-src/src/backend/in_memory.rs +670 -0
- package/dist-engine-src/src/backend/mod.rs +36 -9
- package/dist-engine-src/src/backend/predicate.rs +80 -0
- package/dist-engine-src/src/backend/traits.rs +260 -0
- package/dist-engine-src/src/backend/types.rs +224 -81
- package/dist-engine-src/src/binary_cas/context.rs +8 -8
- package/dist-engine-src/src/binary_cas/kv.rs +234 -259
- package/dist-engine-src/src/{version → branch}/context.rs +12 -12
- package/dist-engine-src/src/branch/lifecycle.rs +221 -0
- package/dist-engine-src/src/branch/mod.rs +13 -0
- package/dist-engine-src/src/branch/refs.rs +321 -0
- package/dist-engine-src/src/branch/stage_rows.rs +67 -0
- package/dist-engine-src/src/branch/types.rs +21 -0
- package/dist-engine-src/src/catalog/context.rs +18 -18
- package/dist-engine-src/src/catalog/snapshot.rs +8 -8
- package/dist-engine-src/src/changelog/bench_support.rs +785 -0
- package/dist-engine-src/src/changelog/change.rs +1 -0
- package/dist-engine-src/src/changelog/codec.rs +497 -0
- package/dist-engine-src/src/changelog/commit.rs +1 -0
- package/dist-engine-src/src/changelog/context.rs +1614 -0
- package/dist-engine-src/src/changelog/mod.rs +29 -0
- package/dist-engine-src/src/changelog/store.rs +163 -0
- package/dist-engine-src/src/changelog/test_support.rs +54 -0
- package/dist-engine-src/src/changelog/types.rs +213 -0
- package/dist-engine-src/src/commit_graph/context.rs +317 -274
- package/dist-engine-src/src/commit_graph/mod.rs +2 -4
- package/dist-engine-src/src/commit_graph/types.rs +22 -42
- package/dist-engine-src/src/commit_graph/walker.rs +133 -103
- package/dist-engine-src/src/common/error.rs +52 -18
- package/dist-engine-src/src/common/identity.rs +2 -2
- package/dist-engine-src/src/common/mod.rs +1 -1
- package/dist-engine-src/src/domain.rs +42 -46
- package/dist-engine-src/src/engine.rs +74 -96
- package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
- package/dist-engine-src/src/functions/context.rs +56 -52
- package/dist-engine-src/src/functions/state.rs +51 -52
- package/dist-engine-src/src/init.rs +288 -154
- package/dist-engine-src/src/json_store/context.rs +15 -266
- package/dist-engine-src/src/json_store/mod.rs +26 -0
- package/dist-engine-src/src/json_store/store.rs +103 -718
- package/dist-engine-src/src/json_store/types.rs +4 -9
- package/dist-engine-src/src/lib.rs +49 -19
- package/dist-engine-src/src/live_state/context.rs +654 -790
- package/dist-engine-src/src/live_state/mod.rs +9 -3
- package/dist-engine-src/src/live_state/overlay.rs +4 -4
- package/dist-engine-src/src/live_state/types.rs +30 -21
- package/dist-engine-src/src/live_state/visibility.rs +514 -71
- package/dist-engine-src/src/plugin/install.rs +48 -48
- package/dist-engine-src/src/plugin/manifest.rs +7 -7
- package/dist-engine-src/src/plugin/materializer.rs +0 -275
- package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
- package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
- package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
- package/dist-engine-src/src/schema/compatibility.rs +11 -11
- package/dist-engine-src/src/schema/definition.json +2 -2
- package/dist-engine-src/src/schema/definition.rs +5 -5
- package/dist-engine-src/src/schema/key.rs +3 -3
- package/dist-engine-src/src/schema/mod.rs +1 -1
- package/dist-engine-src/src/schema/tests.rs +18 -18
- package/dist-engine-src/src/session/context.rs +803 -148
- package/dist-engine-src/src/session/create_branch.rs +94 -0
- package/dist-engine-src/src/session/execute.rs +223 -83
- package/dist-engine-src/src/session/merge/analysis.rs +9 -3
- package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
- package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
- package/dist-engine-src/src/session/merge/mod.rs +5 -6
- package/dist-engine-src/src/session/merge/stats.rs +7 -11
- package/dist-engine-src/src/session/mod.rs +15 -12
- package/dist-engine-src/src/session/switch_branch.rs +113 -0
- package/dist-engine-src/src/session/transaction.rs +495 -14
- package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
- package/dist-engine-src/src/sql2/bind/error.rs +5 -0
- package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
- package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
- package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
- package/dist-engine-src/src/sql2/bind/read.rs +65 -0
- package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
- package/dist-engine-src/src/sql2/bind/table.rs +273 -0
- package/dist-engine-src/src/sql2/bind/write.rs +86 -0
- package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
- package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
- package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
- package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
- package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
- package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
- package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
- package/dist-engine-src/src/sql2/context.rs +36 -30
- package/dist-engine-src/src/sql2/error.rs +1 -1
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
- package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
- package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
- package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
- package/dist-engine-src/src/sql2/exec/write.rs +661 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
- package/dist-engine-src/src/sql2/history_projection.rs +8 -8
- package/dist-engine-src/src/sql2/history_route.rs +35 -31
- package/dist-engine-src/src/sql2/mod.rs +28 -23
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
- package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
- package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
- package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
- package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
- package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
- package/dist-engine-src/src/sql2/plan/write.rs +147 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
- package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
- package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
- package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
- package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
- package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
- package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
- package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
- package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
- package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
- package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
- package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
- package/dist-engine-src/src/sql2/read_only.rs +2 -2
- package/dist-engine-src/src/sql2/session.rs +47 -96
- package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
- package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
- package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
- package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
- package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
- package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
- package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
- package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
- package/dist-engine-src/src/storage/conformance.rs +399 -0
- package/dist-engine-src/src/storage/context.rs +552 -288
- package/dist-engine-src/src/storage/mod.rs +48 -10
- package/dist-engine-src/src/storage/point.rs +440 -0
- package/dist-engine-src/src/storage/read_scope.rs +43 -64
- package/dist-engine-src/src/storage/reader.rs +867 -0
- package/dist-engine-src/src/storage/scan.rs +784 -0
- package/dist-engine-src/src/storage/spaces.rs +236 -0
- package/dist-engine-src/src/storage/stats.rs +80 -0
- package/dist-engine-src/src/storage/write_set.rs +962 -0
- package/dist-engine-src/src/storage_bench.rs +136 -4828
- package/dist-engine-src/src/test_support.rs +360 -138
- package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
- package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
- package/dist-engine-src/src/tracked_state/context.rs +1927 -993
- package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
- package/dist-engine-src/src/tracked_state/merge.rs +74 -88
- package/dist-engine-src/src/tracked_state/mod.rs +19 -16
- package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
- package/dist-engine-src/src/tracked_state/storage.rs +243 -191
- package/dist-engine-src/src/tracked_state/tree.rs +247 -371
- package/dist-engine-src/src/tracked_state/types.rs +49 -42
- package/dist-engine-src/src/transaction/bench_support.rs +407 -0
- package/dist-engine-src/src/transaction/commit.rs +821 -713
- package/dist-engine-src/src/transaction/context.rs +705 -600
- package/dist-engine-src/src/transaction/mod.rs +13 -2
- package/dist-engine-src/src/transaction/normalization.rs +63 -76
- package/dist-engine-src/src/transaction/prep.rs +13 -13
- package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
- package/dist-engine-src/src/transaction/staging.rs +228 -434
- package/dist-engine-src/src/transaction/types.rs +41 -98
- package/dist-engine-src/src/transaction/validation.rs +382 -446
- package/dist-engine-src/src/untracked_state/codec.rs +337 -29
- package/dist-engine-src/src/untracked_state/context.rs +7 -7
- package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
- package/dist-engine-src/src/untracked_state/mod.rs +1 -1
- package/dist-engine-src/src/untracked_state/storage.rs +659 -157
- package/dist-engine-src/src/untracked_state/types.rs +21 -21
- package/package.json +71 -68
- package/dist-engine-src/src/backend/kv.rs +0 -358
- package/dist-engine-src/src/backend/testing.rs +0 -658
- package/dist-engine-src/src/commit_store/codec.rs +0 -887
- package/dist-engine-src/src/commit_store/context.rs +0 -944
- package/dist-engine-src/src/commit_store/materialization.rs +0 -84
- package/dist-engine-src/src/commit_store/mod.rs +0 -16
- package/dist-engine-src/src/commit_store/storage.rs +0 -600
- package/dist-engine-src/src/commit_store/types.rs +0 -215
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
- package/dist-engine-src/src/session/create_version.rs +0 -88
- package/dist-engine-src/src/session/merge/apply.rs +0 -23
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
- package/dist-engine-src/src/session/switch_version.rs +0 -110
- package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
- package/dist-engine-src/src/sql2/execute.rs +0 -3533
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
- package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
- package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
- package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
- package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
- package/dist-engine-src/src/sql2/version_scope.rs +0 -394
- package/dist-engine-src/src/storage/types.rs +0 -501
- package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
- package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
- package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
- package/dist-engine-src/src/version/lifecycle.rs +0 -221
- package/dist-engine-src/src/version/mod.rs +0 -13
- package/dist-engine-src/src/version/refs.rs +0 -330
- package/dist-engine-src/src/version/stage_rows.rs +0 -67
- package/dist-engine-src/src/version/types.rs +0 -21
|
@@ -1,49 +1,24 @@
|
|
|
1
|
-
use std::collections::HashMap;
|
|
2
|
-
|
|
3
1
|
use xxhash_rust::xxh3::xxh3_64_with_seed;
|
|
4
2
|
|
|
5
|
-
use crate::
|
|
6
|
-
use crate::entity_identity::EntityIdentity;
|
|
3
|
+
use crate::entity_pk::EntityPk;
|
|
7
4
|
use crate::json_store::JsonRef;
|
|
8
5
|
use crate::tracked_state::types::{
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
TrackedStateIndexValue, TrackedStateIndexValueRef, TrackedStateKey, TrackedStateKeyRef,
|
|
7
|
+
TRACKED_STATE_HASH_BYTES,
|
|
11
8
|
};
|
|
12
9
|
use crate::LixError;
|
|
13
10
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
11
|
+
const NODE_BRANCH: u8 = 2;
|
|
12
|
+
const VALUE_BRANCH: u8 = 8;
|
|
16
13
|
const VALUE_DELETED_FLAG: u8 = 0b1000_0000;
|
|
17
|
-
const
|
|
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;
|
|
14
|
+
const VALUE_BRANCH_MASK: u8 = 0b0111_1111;
|
|
28
15
|
const TIMESTAMP_UPDATED_SAME: u8 = 0;
|
|
29
16
|
const TIMESTAMP_UPDATED_DISTINCT: u8 = 1;
|
|
30
17
|
const NODE_KIND_LEAF: u8 = 1;
|
|
31
18
|
const NODE_KIND_INTERNAL: u8 = 2;
|
|
32
19
|
const WEIBULL_K: i32 = 4;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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
|
-
}
|
|
20
|
+
const ENTITY_PKENTITY_END: u8 = 0;
|
|
21
|
+
const ENTITY_PKENTITY_STRING: u8 = 1;
|
|
47
22
|
|
|
48
23
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
49
24
|
pub(crate) struct EncodedLeafEntry {
|
|
@@ -185,7 +160,7 @@ pub(crate) fn encode_key(key: &TrackedStateKey) -> Vec<u8> {
|
|
|
185
160
|
encode_key_ref(TrackedStateKeyRef {
|
|
186
161
|
schema_key: &key.schema_key,
|
|
187
162
|
file_id: key.file_id.as_deref(),
|
|
188
|
-
|
|
163
|
+
entity_pk: &key.entity_pk,
|
|
189
164
|
})
|
|
190
165
|
}
|
|
191
166
|
|
|
@@ -204,7 +179,7 @@ fn append_key_ref(out: &mut Vec<u8>, key: TrackedStateKeyRef<'_>) {
|
|
|
204
179
|
}
|
|
205
180
|
None => out.push(0),
|
|
206
181
|
}
|
|
207
|
-
|
|
182
|
+
push_entity_pk(out, key.entity_pk);
|
|
208
183
|
}
|
|
209
184
|
|
|
210
185
|
pub(crate) fn encode_schema_key_prefix(schema_key: &str) -> Vec<u8> {
|
|
@@ -238,7 +213,7 @@ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
|
|
|
238
213
|
))
|
|
239
214
|
}
|
|
240
215
|
};
|
|
241
|
-
let
|
|
216
|
+
let entity_pk = read_entity_pk(bytes, &mut cursor)?;
|
|
242
217
|
if cursor != bytes.len() {
|
|
243
218
|
return Err(LixError::new(
|
|
244
219
|
"LIX_ERROR_UNKNOWN",
|
|
@@ -248,14 +223,14 @@ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
|
|
|
248
223
|
Ok(TrackedStateKey {
|
|
249
224
|
schema_key,
|
|
250
225
|
file_id,
|
|
251
|
-
|
|
226
|
+
entity_pk,
|
|
252
227
|
})
|
|
253
228
|
}
|
|
254
229
|
|
|
255
230
|
/// Decodes a key after the caller has already proven the schema/file prefix.
|
|
256
231
|
///
|
|
257
232
|
/// This is for scan paths that have matched an encoded prefix range and only
|
|
258
|
-
/// need to materialize the entity suffix plus the
|
|
233
|
+
/// need to materialize the entity suffix plus the selected columns.
|
|
259
234
|
pub(crate) fn decode_key_with_trusted_prefix(
|
|
260
235
|
bytes: &[u8],
|
|
261
236
|
schema_key: &str,
|
|
@@ -263,7 +238,7 @@ pub(crate) fn decode_key_with_trusted_prefix(
|
|
|
263
238
|
prefix_len: usize,
|
|
264
239
|
) -> Result<TrackedStateKey, LixError> {
|
|
265
240
|
let mut cursor = prefix_len;
|
|
266
|
-
let
|
|
241
|
+
let entity_pk = read_entity_pk(bytes, &mut cursor)?;
|
|
267
242
|
if cursor != bytes.len() {
|
|
268
243
|
return Err(LixError::new(
|
|
269
244
|
"LIX_ERROR_UNKNOWN",
|
|
@@ -273,14 +248,15 @@ pub(crate) fn decode_key_with_trusted_prefix(
|
|
|
273
248
|
Ok(TrackedStateKey {
|
|
274
249
|
schema_key: schema_key.to_string(),
|
|
275
250
|
file_id: file_id.map(str::to_string),
|
|
276
|
-
|
|
251
|
+
entity_pk,
|
|
277
252
|
})
|
|
278
253
|
}
|
|
279
254
|
|
|
280
255
|
#[cfg(test)]
|
|
281
256
|
pub(crate) fn encode_value(value: &TrackedStateIndexValue) -> Vec<u8> {
|
|
282
257
|
encode_value_ref(TrackedStateIndexValueRef {
|
|
283
|
-
|
|
258
|
+
change_id: &value.change_id,
|
|
259
|
+
commit_id: &value.commit_id,
|
|
284
260
|
deleted: value.deleted,
|
|
285
261
|
snapshot_ref: value.snapshot_ref.as_ref(),
|
|
286
262
|
metadata_ref: value.metadata_ref.as_ref(),
|
|
@@ -296,11 +272,9 @@ pub(crate) fn encode_value_ref(value: TrackedStateIndexValueRef<'_>) -> Vec<u8>
|
|
|
296
272
|
}
|
|
297
273
|
|
|
298
274
|
fn append_value_ref(out: &mut Vec<u8>, value: TrackedStateIndexValueRef<'_>) {
|
|
299
|
-
out.push(
|
|
300
|
-
push_sized_bytes(out, value.
|
|
301
|
-
out
|
|
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());
|
|
275
|
+
out.push(VALUE_BRANCH | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
|
|
276
|
+
push_sized_bytes(out, value.change_id.as_bytes());
|
|
277
|
+
push_sized_bytes(out, value.commit_id.as_bytes());
|
|
304
278
|
push_timestamp_pair(out, value.created_at, value.updated_at);
|
|
305
279
|
push_optional_json_ref(out, value.snapshot_ref);
|
|
306
280
|
push_optional_json_ref(out, value.metadata_ref);
|
|
@@ -308,10 +282,8 @@ fn append_value_ref(out: &mut Vec<u8>, value: TrackedStateIndexValueRef<'_>) {
|
|
|
308
282
|
|
|
309
283
|
#[cfg(test)]
|
|
310
284
|
pub(crate) fn encoded_value_len(value: &TrackedStateIndexValue) -> usize {
|
|
311
|
-
1 + sized_bytes_len(value.
|
|
312
|
-
+
|
|
313
|
-
+ 4
|
|
314
|
-
+ sized_bytes_len(value.change_locator.change_id.as_bytes())
|
|
285
|
+
1 + sized_bytes_len(value.change_id.as_bytes())
|
|
286
|
+
+ sized_bytes_len(value.commit_id.as_bytes())
|
|
315
287
|
+ timestamp_pair_len(&value.created_at, &value.updated_at)
|
|
316
288
|
+ optional_json_ref_len(value.snapshot_ref.as_ref())
|
|
317
289
|
+ optional_json_ref_len(value.metadata_ref.as_ref())
|
|
@@ -338,12 +310,12 @@ pub(crate) fn decode_visible_value(
|
|
|
338
310
|
}
|
|
339
311
|
|
|
340
312
|
fn decode_value_header(value_header: u8) -> Result<bool, LixError> {
|
|
341
|
-
let
|
|
313
|
+
let branch = value_header & VALUE_BRANCH_MASK;
|
|
342
314
|
let deleted = value_header & VALUE_DELETED_FLAG != 0;
|
|
343
|
-
if
|
|
315
|
+
if branch != VALUE_BRANCH {
|
|
344
316
|
return Err(LixError::new(
|
|
345
317
|
"LIX_ERROR_UNKNOWN",
|
|
346
|
-
format!("unsupported tracked-state tree value
|
|
318
|
+
format!("unsupported tracked-state tree value branch {branch}"),
|
|
347
319
|
));
|
|
348
320
|
}
|
|
349
321
|
Ok(deleted)
|
|
@@ -354,22 +326,8 @@ fn decode_value_after_header(
|
|
|
354
326
|
mut cursor: usize,
|
|
355
327
|
deleted: bool,
|
|
356
328
|
) -> 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
329
|
let change_id = read_sized_string(bytes, &mut cursor, "change_id")?;
|
|
330
|
+
let commit_id = read_sized_string(bytes, &mut cursor, "commit_id")?;
|
|
373
331
|
let (created_at, updated_at) = read_timestamp_pair(bytes, &mut cursor)?;
|
|
374
332
|
let snapshot_ref = read_optional_json_ref(bytes, &mut cursor, "snapshot_ref")?;
|
|
375
333
|
let metadata_ref = read_optional_json_ref(bytes, &mut cursor, "metadata_ref")?;
|
|
@@ -380,387 +338,8 @@ fn decode_value_after_header(
|
|
|
380
338
|
));
|
|
381
339
|
}
|
|
382
340
|
Ok(TrackedStateIndexValue {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
source_pack_id,
|
|
386
|
-
source_ordinal,
|
|
387
|
-
change_id,
|
|
388
|
-
},
|
|
389
|
-
deleted,
|
|
390
|
-
snapshot_ref,
|
|
391
|
-
metadata_ref,
|
|
392
|
-
created_at,
|
|
393
|
-
updated_at,
|
|
394
|
-
})
|
|
395
|
-
}
|
|
396
|
-
|
|
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)
|
|
474
|
-
}
|
|
475
|
-
|
|
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) => {
|
|
487
|
-
out.push(1);
|
|
488
|
-
push_var_sized_bytes(out, file_id.as_bytes(), "delta key prefix file_id")?;
|
|
489
|
-
}
|
|
490
|
-
None => out.push(0),
|
|
491
|
-
}
|
|
492
|
-
Ok(())
|
|
493
|
-
}
|
|
494
|
-
|
|
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
|
-
}
|
|
575
|
-
}
|
|
576
|
-
Ok(())
|
|
577
|
-
}
|
|
578
|
-
|
|
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(
|
|
666
|
-
bytes: &[u8],
|
|
667
|
-
cursor: &mut usize,
|
|
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))
|
|
705
|
-
}
|
|
706
|
-
other => Err(LixError::new(
|
|
707
|
-
"LIX_ERROR_UNKNOWN",
|
|
708
|
-
format!("tracked-state delta pack has invalid JSON ref mode {other}"),
|
|
709
|
-
)),
|
|
710
|
-
}
|
|
711
|
-
}
|
|
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
|
-
},
|
|
341
|
+
change_id,
|
|
342
|
+
commit_id,
|
|
764
343
|
deleted,
|
|
765
344
|
snapshot_ref,
|
|
766
345
|
metadata_ref,
|
|
@@ -785,7 +364,7 @@ pub(crate) fn encode_leaf_node(entries: &[EncodedLeafEntry]) -> Vec<u8> {
|
|
|
785
364
|
pub(crate) fn encode_leaf_node_refs(entries: &[EncodedLeafEntryRef<'_>]) -> Vec<u8> {
|
|
786
365
|
let mut out = Vec::new();
|
|
787
366
|
out.push(NODE_KIND_LEAF);
|
|
788
|
-
out.push(
|
|
367
|
+
out.push(NODE_BRANCH);
|
|
789
368
|
push_u32(&mut out, entries.len());
|
|
790
369
|
|
|
791
370
|
let mut offsets = Vec::with_capacity(entries.len().saturating_add(1));
|
|
@@ -814,7 +393,7 @@ pub(crate) fn encode_internal_node(children: &[ChildSummary]) -> Vec<u8> {
|
|
|
814
393
|
pub(crate) fn encode_internal_node_refs(children: &[ChildSummaryRef<'_>]) -> Vec<u8> {
|
|
815
394
|
let mut out = Vec::new();
|
|
816
395
|
out.push(NODE_KIND_INTERNAL);
|
|
817
|
-
out.push(
|
|
396
|
+
out.push(NODE_BRANCH);
|
|
818
397
|
push_u32(&mut out, children.len());
|
|
819
398
|
for child in children {
|
|
820
399
|
push_sized_bytes(&mut out, child.first_key);
|
|
@@ -850,11 +429,11 @@ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
|
|
|
850
429
|
pub(crate) fn decode_node_ref(bytes: &[u8]) -> Result<DecodedNodeRef<'_>, LixError> {
|
|
851
430
|
let mut cursor = 0usize;
|
|
852
431
|
let kind = read_u8(bytes, &mut cursor, "node kind")?;
|
|
853
|
-
let
|
|
854
|
-
if
|
|
432
|
+
let branch = read_u8(bytes, &mut cursor, "node branch")?;
|
|
433
|
+
if branch != NODE_BRANCH {
|
|
855
434
|
return Err(LixError::new(
|
|
856
435
|
"LIX_ERROR_UNKNOWN",
|
|
857
|
-
format!("unsupported tracked-state tree node
|
|
436
|
+
format!("unsupported tracked-state tree node branch {branch}"),
|
|
858
437
|
));
|
|
859
438
|
}
|
|
860
439
|
let count = read_u32(bytes, &mut cursor, "entry count")?;
|
|
@@ -864,6 +443,13 @@ pub(crate) fn decode_node_ref(bytes: &[u8]) -> Result<DecodedNodeRef<'_>, LixErr
|
|
|
864
443
|
DecodedNodeRef::Leaf(leaf)
|
|
865
444
|
}
|
|
866
445
|
NODE_KIND_INTERNAL => {
|
|
446
|
+
ensure_counted_records_fit_remaining(
|
|
447
|
+
bytes,
|
|
448
|
+
cursor,
|
|
449
|
+
count,
|
|
450
|
+
internal_child_min_len(),
|
|
451
|
+
"internal children",
|
|
452
|
+
)?;
|
|
867
453
|
let mut children = Vec::with_capacity(count);
|
|
868
454
|
for _ in 0..count {
|
|
869
455
|
let first_key = read_sized_bytes(bytes, &mut cursor, "internal first_key")?;
|
|
@@ -900,7 +486,14 @@ fn decode_leaf_node_ref_after_count<'a>(
|
|
|
900
486
|
cursor: &mut usize,
|
|
901
487
|
count: usize,
|
|
902
488
|
) -> Result<DecodedLeafNodeRef<'a>, LixError> {
|
|
903
|
-
let
|
|
489
|
+
let offset_count = count.checked_add(1).ok_or_else(|| {
|
|
490
|
+
LixError::new(
|
|
491
|
+
"LIX_ERROR_UNKNOWN",
|
|
492
|
+
"tracked-state leaf offset count overflows",
|
|
493
|
+
)
|
|
494
|
+
})?;
|
|
495
|
+
ensure_counted_records_fit_remaining(bytes, *cursor, offset_count, 4, "leaf offsets")?;
|
|
496
|
+
let mut offsets = Vec::with_capacity(offset_count);
|
|
904
497
|
for _ in 0..=count {
|
|
905
498
|
offsets.push(read_u32(bytes, cursor, "leaf entry offset")?);
|
|
906
499
|
}
|
|
@@ -939,6 +532,38 @@ fn decode_leaf_node_ref_after_count<'a>(
|
|
|
939
532
|
})
|
|
940
533
|
}
|
|
941
534
|
|
|
535
|
+
fn ensure_counted_records_fit_remaining(
|
|
536
|
+
bytes: &[u8],
|
|
537
|
+
cursor: usize,
|
|
538
|
+
count: usize,
|
|
539
|
+
record_min_len: usize,
|
|
540
|
+
field_name: &str,
|
|
541
|
+
) -> Result<(), LixError> {
|
|
542
|
+
let required = count.checked_mul(record_min_len).ok_or_else(|| {
|
|
543
|
+
LixError::new(
|
|
544
|
+
"LIX_ERROR_UNKNOWN",
|
|
545
|
+
format!("tracked-state tree field '{field_name}' byte count overflows"),
|
|
546
|
+
)
|
|
547
|
+
})?;
|
|
548
|
+
let remaining = bytes.len().checked_sub(cursor).ok_or_else(|| {
|
|
549
|
+
LixError::new(
|
|
550
|
+
"LIX_ERROR_UNKNOWN",
|
|
551
|
+
format!("tracked-state tree field '{field_name}' starts past node end"),
|
|
552
|
+
)
|
|
553
|
+
})?;
|
|
554
|
+
if required > remaining {
|
|
555
|
+
return Err(LixError::new(
|
|
556
|
+
"LIX_ERROR_UNKNOWN",
|
|
557
|
+
format!("tracked-state tree field '{field_name}' exceeds remaining node bytes"),
|
|
558
|
+
));
|
|
559
|
+
}
|
|
560
|
+
Ok(())
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fn internal_child_min_len() -> usize {
|
|
564
|
+
4 + 4 + TRACKED_STATE_HASH_BYTES + 8
|
|
565
|
+
}
|
|
566
|
+
|
|
942
567
|
pub(crate) fn child_summary_from_node(
|
|
943
568
|
node_bytes: Vec<u8>,
|
|
944
569
|
first_key: Vec<u8>,
|
|
@@ -998,35 +623,37 @@ fn level_salt(level: usize) -> u64 {
|
|
|
998
623
|
value ^ (value >> 31)
|
|
999
624
|
}
|
|
1000
625
|
|
|
1001
|
-
fn
|
|
626
|
+
fn push_entity_pk(out: &mut Vec<u8>, identity: &EntityPk) {
|
|
1002
627
|
assert!(
|
|
1003
628
|
!identity.parts.is_empty(),
|
|
1004
|
-
"tracked-state key entity
|
|
629
|
+
"tracked-state key entity primary key must contain at least one part"
|
|
1005
630
|
);
|
|
1006
631
|
for part in &identity.parts {
|
|
1007
|
-
out.push(
|
|
632
|
+
out.push(ENTITY_PKENTITY_STRING);
|
|
1008
633
|
push_sized_bytes(out, part.as_bytes());
|
|
1009
634
|
}
|
|
1010
|
-
out.push(
|
|
635
|
+
out.push(ENTITY_PKENTITY_END);
|
|
1011
636
|
}
|
|
1012
637
|
|
|
1013
|
-
fn
|
|
638
|
+
fn read_entity_pk(bytes: &[u8], cursor: &mut usize) -> Result<EntityPk, LixError> {
|
|
1014
639
|
let mut parts = Vec::new();
|
|
1015
640
|
loop {
|
|
1016
|
-
let tag = read_u8(bytes, cursor, "entity
|
|
641
|
+
let tag = read_u8(bytes, cursor, "entity primary key part tag")?;
|
|
1017
642
|
match tag {
|
|
1018
|
-
|
|
1019
|
-
|
|
643
|
+
ENTITY_PKENTITY_END => break,
|
|
644
|
+
ENTITY_PKENTITY_STRING => {
|
|
1020
645
|
parts.push(read_sized_string(
|
|
1021
646
|
bytes,
|
|
1022
647
|
cursor,
|
|
1023
|
-
"entity
|
|
648
|
+
"entity primary key string part",
|
|
1024
649
|
)?);
|
|
1025
650
|
}
|
|
1026
651
|
other => {
|
|
1027
652
|
return Err(LixError::new(
|
|
1028
653
|
"LIX_ERROR_UNKNOWN",
|
|
1029
|
-
format!(
|
|
654
|
+
format!(
|
|
655
|
+
"tracked-state tree key has invalid entity primary key part tag {other}"
|
|
656
|
+
),
|
|
1030
657
|
))
|
|
1031
658
|
}
|
|
1032
659
|
}
|
|
@@ -1034,10 +661,10 @@ fn read_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdenti
|
|
|
1034
661
|
if parts.is_empty() {
|
|
1035
662
|
return Err(LixError::new(
|
|
1036
663
|
"LIX_ERROR_UNKNOWN",
|
|
1037
|
-
"tracked-state tree key entity
|
|
664
|
+
"tracked-state tree key entity primary key must contain at least one part",
|
|
1038
665
|
));
|
|
1039
666
|
}
|
|
1040
|
-
Ok(
|
|
667
|
+
Ok(EntityPk { parts })
|
|
1041
668
|
}
|
|
1042
669
|
|
|
1043
670
|
fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
|
@@ -1045,49 +672,6 @@ fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
|
|
1045
672
|
out.extend_from_slice(bytes);
|
|
1046
673
|
}
|
|
1047
674
|
|
|
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
675
|
fn push_optional_json_ref(out: &mut Vec<u8>, json_ref: Option<&JsonRef>) {
|
|
1092
676
|
match json_ref {
|
|
1093
677
|
Some(json_ref) => {
|
|
@@ -1098,56 +682,6 @@ fn push_optional_json_ref(out: &mut Vec<u8>, json_ref: Option<&JsonRef>) {
|
|
|
1098
682
|
}
|
|
1099
683
|
}
|
|
1100
684
|
|
|
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
685
|
#[cfg(test)]
|
|
1152
686
|
fn optional_json_ref_len(json_ref: Option<&JsonRef>) -> usize {
|
|
1153
687
|
1 + json_ref.map_or(0, |_| TRACKED_STATE_HASH_BYTES)
|
|
@@ -1163,21 +697,6 @@ fn push_timestamp_pair(out: &mut Vec<u8>, created_at: &str, updated_at: &str) {
|
|
|
1163
697
|
}
|
|
1164
698
|
}
|
|
1165
699
|
|
|
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
700
|
#[cfg(test)]
|
|
1182
701
|
fn timestamp_pair_len(created_at: &str, updated_at: &str) -> usize {
|
|
1183
702
|
sized_bytes_len(created_at.as_bytes())
|
|
@@ -1204,21 +723,6 @@ fn read_timestamp_pair(bytes: &[u8], cursor: &mut usize) -> Result<(String, Stri
|
|
|
1204
723
|
Ok((created_at, updated_at))
|
|
1205
724
|
}
|
|
1206
725
|
|
|
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
|
-
|
|
1222
726
|
fn push_u32(out: &mut Vec<u8>, value: usize) {
|
|
1223
727
|
out.extend_from_slice(&(value as u32).to_be_bytes());
|
|
1224
728
|
}
|
|
@@ -1266,60 +770,6 @@ fn read_sized_slice<'a>(
|
|
|
1266
770
|
Ok(slice)
|
|
1267
771
|
}
|
|
1268
772
|
|
|
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 })
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
773
|
fn read_fixed_hash(
|
|
1324
774
|
bytes: &[u8],
|
|
1325
775
|
cursor: &mut usize,
|
|
@@ -1355,34 +805,6 @@ fn read_optional_json_ref(
|
|
|
1355
805
|
}
|
|
1356
806
|
}
|
|
1357
807
|
|
|
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
|
-
|
|
1386
808
|
fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, LixError> {
|
|
1387
809
|
let value = *bytes.get(*cursor).ok_or_else(|| {
|
|
1388
810
|
LixError::new(
|
|
@@ -1394,35 +816,6 @@ fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, Lix
|
|
|
1394
816
|
Ok(value)
|
|
1395
817
|
}
|
|
1396
818
|
|
|
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
|
-
|
|
1426
819
|
fn read_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
|
|
1427
820
|
let end = *cursor + 4;
|
|
1428
821
|
let slice = bytes.get(*cursor..end).ok_or_else(|| {
|
|
@@ -1455,17 +848,29 @@ fn read_u64(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u64, L
|
|
|
1455
848
|
mod tests {
|
|
1456
849
|
use super::*;
|
|
1457
850
|
|
|
851
|
+
fn test_value(commit_id: &str, change_id: &str) -> TrackedStateIndexValue {
|
|
852
|
+
TrackedStateIndexValue {
|
|
853
|
+
change_id: change_id.to_string(),
|
|
854
|
+
commit_id: commit_id.to_string(),
|
|
855
|
+
deleted: false,
|
|
856
|
+
snapshot_ref: None,
|
|
857
|
+
metadata_ref: None,
|
|
858
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
859
|
+
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
1458
863
|
#[test]
|
|
1459
864
|
fn key_codec_distinguishes_null_and_value_file_id() {
|
|
1460
865
|
let null_key = encode_key(&TrackedStateKey {
|
|
1461
866
|
schema_key: "schema".to_string(),
|
|
1462
867
|
file_id: None,
|
|
1463
|
-
|
|
868
|
+
entity_pk: EntityPk::single("entity"),
|
|
1464
869
|
});
|
|
1465
870
|
let file_key = encode_key(&TrackedStateKey {
|
|
1466
871
|
schema_key: "schema".to_string(),
|
|
1467
872
|
file_id: Some("file".to_string()),
|
|
1468
|
-
|
|
873
|
+
entity_pk: EntityPk::single("entity"),
|
|
1469
874
|
});
|
|
1470
875
|
|
|
1471
876
|
assert_ne!(null_key, file_key);
|
|
@@ -1474,7 +879,7 @@ mod tests {
|
|
|
1474
879
|
TrackedStateKey {
|
|
1475
880
|
schema_key: "schema".to_string(),
|
|
1476
881
|
file_id: None,
|
|
1477
|
-
|
|
882
|
+
entity_pk: EntityPk::single("entity"),
|
|
1478
883
|
}
|
|
1479
884
|
);
|
|
1480
885
|
assert_eq!(
|
|
@@ -1482,7 +887,7 @@ mod tests {
|
|
|
1482
887
|
TrackedStateKey {
|
|
1483
888
|
schema_key: "schema".to_string(),
|
|
1484
889
|
file_id: Some("file".to_string()),
|
|
1485
|
-
|
|
890
|
+
entity_pk: EntityPk::single("entity"),
|
|
1486
891
|
}
|
|
1487
892
|
);
|
|
1488
893
|
}
|
|
@@ -1492,7 +897,7 @@ mod tests {
|
|
|
1492
897
|
let key = TrackedStateKey {
|
|
1493
898
|
schema_key: "schema".to_string(),
|
|
1494
899
|
file_id: None,
|
|
1495
|
-
|
|
900
|
+
entity_pk: EntityPk {
|
|
1496
901
|
parts: vec![
|
|
1497
902
|
"namespace".to_string(),
|
|
1498
903
|
"true".to_string(),
|
|
@@ -1511,7 +916,7 @@ mod tests {
|
|
|
1511
916
|
let key = TrackedStateKey {
|
|
1512
917
|
schema_key: "schema".to_string(),
|
|
1513
918
|
file_id: Some("file".to_string()),
|
|
1514
|
-
|
|
919
|
+
entity_pk: EntityPk {
|
|
1515
920
|
parts: vec!["namespace".to_string(), "id".to_string()],
|
|
1516
921
|
},
|
|
1517
922
|
};
|
|
@@ -1530,7 +935,7 @@ mod tests {
|
|
|
1530
935
|
let mut encoded = encode_key(&TrackedStateKey {
|
|
1531
936
|
schema_key: "schema".to_string(),
|
|
1532
937
|
file_id: None,
|
|
1533
|
-
|
|
938
|
+
entity_pk: EntityPk {
|
|
1534
939
|
parts: vec!["true".to_string()],
|
|
1535
940
|
},
|
|
1536
941
|
});
|
|
@@ -1542,7 +947,7 @@ mod tests {
|
|
|
1542
947
|
let error = decode_key(&encoded).expect_err("non-string identity tag should reject");
|
|
1543
948
|
assert!(error
|
|
1544
949
|
.to_string()
|
|
1545
|
-
.contains("invalid entity
|
|
950
|
+
.contains("invalid entity primary key part tag 2"));
|
|
1546
951
|
}
|
|
1547
952
|
|
|
1548
953
|
#[test]
|
|
@@ -1550,14 +955,14 @@ mod tests {
|
|
|
1550
955
|
let prefix = encode_key(&TrackedStateKey {
|
|
1551
956
|
schema_key: "schema".to_string(),
|
|
1552
957
|
file_id: None,
|
|
1553
|
-
|
|
958
|
+
entity_pk: EntityPk {
|
|
1554
959
|
parts: vec!["a".to_string()],
|
|
1555
960
|
},
|
|
1556
961
|
});
|
|
1557
962
|
let extended = encode_key(&TrackedStateKey {
|
|
1558
963
|
schema_key: "schema".to_string(),
|
|
1559
964
|
file_id: None,
|
|
1560
|
-
|
|
965
|
+
entity_pk: EntityPk {
|
|
1561
966
|
parts: vec!["a".to_string(), "b".to_string()],
|
|
1562
967
|
},
|
|
1563
968
|
});
|
|
@@ -1566,14 +971,10 @@ mod tests {
|
|
|
1566
971
|
}
|
|
1567
972
|
|
|
1568
973
|
#[test]
|
|
1569
|
-
fn
|
|
974
|
+
fn value_codec_roundtrips_change_ref_value() {
|
|
1570
975
|
let value = TrackedStateIndexValue {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
source_pack_id: 7,
|
|
1574
|
-
source_ordinal: 11,
|
|
1575
|
-
change_id: "change".to_string(),
|
|
1576
|
-
},
|
|
976
|
+
change_id: "change".to_string(),
|
|
977
|
+
commit_id: "commit".to_string(),
|
|
1577
978
|
deleted: false,
|
|
1578
979
|
snapshot_ref: Some(JsonRef::from_hash_bytes([1; 32])),
|
|
1579
980
|
metadata_ref: Some(JsonRef::from_hash_bytes([2; 32])),
|
|
@@ -1586,14 +987,10 @@ mod tests {
|
|
|
1586
987
|
}
|
|
1587
988
|
|
|
1588
989
|
#[test]
|
|
1589
|
-
fn
|
|
990
|
+
fn value_codec_roundtrips_second_change_ref_value() {
|
|
1590
991
|
let value = TrackedStateIndexValue {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
source_pack_id: 0,
|
|
1594
|
-
source_ordinal: 1,
|
|
1595
|
-
change_id: "other-change".to_string(),
|
|
1596
|
-
},
|
|
992
|
+
change_id: "other-change".to_string(),
|
|
993
|
+
commit_id: "other-commit".to_string(),
|
|
1597
994
|
deleted: true,
|
|
1598
995
|
snapshot_ref: None,
|
|
1599
996
|
metadata_ref: None,
|
|
@@ -1607,19 +1004,8 @@ mod tests {
|
|
|
1607
1004
|
|
|
1608
1005
|
#[test]
|
|
1609
1006
|
fn value_codec_compacts_matching_timestamps() {
|
|
1610
|
-
let mut compact =
|
|
1611
|
-
|
|
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
|
-
};
|
|
1007
|
+
let mut compact = test_value("commit", "change");
|
|
1008
|
+
compact.updated_at = compact.created_at.clone();
|
|
1623
1009
|
let compact_len = encode_value(&compact).len();
|
|
1624
1010
|
assert_eq!(
|
|
1625
1011
|
decode_value(&encode_value(&compact)).expect("value"),
|
|
@@ -1636,316 +1022,12 @@ mod tests {
|
|
|
1636
1022
|
);
|
|
1637
1023
|
}
|
|
1638
1024
|
|
|
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
|
-
|
|
1939
1025
|
#[test]
|
|
1940
1026
|
fn encoded_value_len_matches_encoded_value_bytes() {
|
|
1941
1027
|
let values = [
|
|
1942
1028
|
TrackedStateIndexValue {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
source_pack_id: 0,
|
|
1946
|
-
source_ordinal: 0,
|
|
1947
|
-
change_id: "change".to_string(),
|
|
1948
|
-
},
|
|
1029
|
+
change_id: "change".to_string(),
|
|
1030
|
+
commit_id: "commit".to_string(),
|
|
1949
1031
|
deleted: false,
|
|
1950
1032
|
snapshot_ref: None,
|
|
1951
1033
|
metadata_ref: None,
|
|
@@ -1953,12 +1035,8 @@ mod tests {
|
|
|
1953
1035
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
1954
1036
|
},
|
|
1955
1037
|
TrackedStateIndexValue {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
source_pack_id: 1,
|
|
1959
|
-
source_ordinal: 2,
|
|
1960
|
-
change_id: "change-2".to_string(),
|
|
1961
|
-
},
|
|
1038
|
+
change_id: "change-2".to_string(),
|
|
1039
|
+
commit_id: "commit".to_string(),
|
|
1962
1040
|
deleted: true,
|
|
1963
1041
|
snapshot_ref: Some(JsonRef::from_hash_bytes([3; 32])),
|
|
1964
1042
|
metadata_ref: None,
|
|
@@ -1966,12 +1044,8 @@ mod tests {
|
|
|
1966
1044
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
|
1967
1045
|
},
|
|
1968
1046
|
TrackedStateIndexValue {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
source_pack_id: 4,
|
|
1972
|
-
source_ordinal: 8,
|
|
1973
|
-
change_id: "change-3".to_string(),
|
|
1974
|
-
},
|
|
1047
|
+
change_id: "change-3".to_string(),
|
|
1048
|
+
commit_id: "other".to_string(),
|
|
1975
1049
|
deleted: false,
|
|
1976
1050
|
snapshot_ref: None,
|
|
1977
1051
|
metadata_ref: Some(JsonRef::from_hash_bytes([4; 32])),
|
|
@@ -2000,7 +1074,7 @@ mod tests {
|
|
|
2000
1074
|
|
|
2001
1075
|
let encoded = encode_leaf_node(&entries);
|
|
2002
1076
|
assert_eq!(encoded[0], NODE_KIND_LEAF);
|
|
2003
|
-
assert_eq!(encoded[1],
|
|
1077
|
+
assert_eq!(encoded[1], NODE_BRANCH);
|
|
2004
1078
|
assert_eq!(&encoded[2..6], 2u32.to_be_bytes().as_slice());
|
|
2005
1079
|
assert_eq!(&encoded[6..10], 0u32.to_be_bytes().as_slice());
|
|
2006
1080
|
|
|
@@ -2071,6 +1145,30 @@ mod tests {
|
|
|
2071
1145
|
.contains("offset table does not cover full payload"));
|
|
2072
1146
|
}
|
|
2073
1147
|
|
|
1148
|
+
#[test]
|
|
1149
|
+
fn leaf_node_codec_rejects_count_that_exceeds_remaining_bytes_before_allocating() {
|
|
1150
|
+
let mut encoded = vec![NODE_KIND_LEAF, NODE_BRANCH];
|
|
1151
|
+
encoded.extend_from_slice(&u32::MAX.to_be_bytes());
|
|
1152
|
+
|
|
1153
|
+
let error = decode_node_ref(&encoded).expect_err("impossible leaf count should reject");
|
|
1154
|
+
|
|
1155
|
+
assert!(error
|
|
1156
|
+
.to_string()
|
|
1157
|
+
.contains("field 'leaf offsets' exceeds remaining node bytes"));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
#[test]
|
|
1161
|
+
fn internal_node_codec_rejects_count_that_exceeds_remaining_bytes_before_allocating() {
|
|
1162
|
+
let mut encoded = vec![NODE_KIND_INTERNAL, NODE_BRANCH];
|
|
1163
|
+
encoded.extend_from_slice(&u32::MAX.to_be_bytes());
|
|
1164
|
+
|
|
1165
|
+
let error = decode_node_ref(&encoded).expect_err("impossible internal count should reject");
|
|
1166
|
+
|
|
1167
|
+
assert!(error
|
|
1168
|
+
.to_string()
|
|
1169
|
+
.contains("field 'internal children' exceeds remaining node bytes"));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
2074
1172
|
#[test]
|
|
2075
1173
|
fn content_hash_is_blake3() {
|
|
2076
1174
|
assert_eq!(hash_bytes(b"abc"), *blake3::hash(b"abc").as_bytes());
|