@lix-js/sdk 0.6.0-preview.5 → 0.6.1
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 +76 -4
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -3
- package/dist/native.d.ts +1 -0
- package/dist/native.js +47 -0
- package/dist/open-lix.d.ts +38 -207
- package/dist/open-lix.js +59 -284
- package/dist/result.d.ts +18 -0
- package/dist/result.js +48 -0
- package/dist/types.d.ts +114 -1
- package/dist/value.d.ts +28 -0
- package/dist/value.js +245 -0
- package/package.json +38 -71
- package/SKILL.md +0 -507
- package/dist/builtin-schemas.d.ts +0 -1
- package/dist/builtin-schemas.js +0 -1
- package/dist/engine-wasm/index.d.ts +0 -87
- package/dist/engine-wasm/index.js +0 -339
- package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
- package/dist/engine-wasm/wasm/lix_engine.js +0 -833
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
- package/dist/generated/builtin-schemas.d.ts +0 -427
- package/dist/generated/builtin-schemas.js +0 -643
- package/dist/sqlite/index.d.ts +0 -12
- package/dist/sqlite/index.js +0 -359
- package/dist-engine-src/README.md +0 -18
- package/dist-engine-src/src/backend/capabilities.rs +0 -67
- package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
- package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
- package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
- package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
- package/dist-engine-src/src/backend/conformance/model.rs +0 -28
- package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
- package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
- package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
- package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
- package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
- package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
- package/dist-engine-src/src/backend/conformance/write.rs +0 -16
- package/dist-engine-src/src/backend/error.rs +0 -94
- package/dist-engine-src/src/backend/in_memory.rs +0 -670
- package/dist-engine-src/src/backend/mod.rs +0 -39
- package/dist-engine-src/src/backend/predicate.rs +0 -80
- package/dist-engine-src/src/backend/traits.rs +0 -260
- package/dist-engine-src/src/backend/types.rs +0 -239
- package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
- package/dist-engine-src/src/binary_cas/codec.rs +0 -346
- package/dist-engine-src/src/binary_cas/context.rs +0 -139
- package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
- package/dist-engine-src/src/binary_cas/mod.rs +0 -11
- package/dist-engine-src/src/binary_cas/types.rs +0 -121
- package/dist-engine-src/src/branch/context.rs +0 -40
- package/dist-engine-src/src/branch/lifecycle.rs +0 -221
- package/dist-engine-src/src/branch/mod.rs +0 -13
- package/dist-engine-src/src/branch/refs.rs +0 -321
- package/dist-engine-src/src/branch/stage_rows.rs +0 -67
- package/dist-engine-src/src/branch/types.rs +0 -21
- package/dist-engine-src/src/catalog/context.rs +0 -412
- package/dist-engine-src/src/catalog/mod.rs +0 -10
- package/dist-engine-src/src/catalog/schema.rs +0 -4
- package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
- package/dist-engine-src/src/cel/context.rs +0 -86
- package/dist-engine-src/src/cel/error.rs +0 -19
- package/dist-engine-src/src/cel/mod.rs +0 -8
- package/dist-engine-src/src/cel/provider.rs +0 -9
- package/dist-engine-src/src/cel/runtime.rs +0 -167
- package/dist-engine-src/src/cel/value.rs +0 -50
- package/dist-engine-src/src/changelog/bench_support.rs +0 -785
- package/dist-engine-src/src/changelog/change.rs +0 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -497
- package/dist-engine-src/src/changelog/commit.rs +0 -1
- package/dist-engine-src/src/changelog/context.rs +0 -1614
- package/dist-engine-src/src/changelog/mod.rs +0 -29
- package/dist-engine-src/src/changelog/store.rs +0 -163
- package/dist-engine-src/src/changelog/test_support.rs +0 -54
- package/dist-engine-src/src/changelog/types.rs +0 -213
- package/dist-engine-src/src/commit_graph/context.rs +0 -944
- package/dist-engine-src/src/commit_graph/mod.rs +0 -9
- package/dist-engine-src/src/commit_graph/types.rs +0 -89
- package/dist-engine-src/src/commit_graph/walker.rs +0 -786
- package/dist-engine-src/src/common/error.rs +0 -347
- package/dist-engine-src/src/common/fingerprint.rs +0 -3
- package/dist-engine-src/src/common/fs_path.rs +0 -1336
- package/dist-engine-src/src/common/identity.rs +0 -145
- package/dist-engine-src/src/common/json_pointer.rs +0 -67
- package/dist-engine-src/src/common/metadata.rs +0 -40
- package/dist-engine-src/src/common/mod.rs +0 -23
- package/dist-engine-src/src/common/types.rs +0 -105
- package/dist-engine-src/src/common/wire.rs +0 -222
- package/dist-engine-src/src/domain.rs +0 -320
- package/dist-engine-src/src/engine.rs +0 -203
- package/dist-engine-src/src/entity_pk.rs +0 -402
- package/dist-engine-src/src/functions/context.rs +0 -296
- package/dist-engine-src/src/functions/deterministic.rs +0 -113
- package/dist-engine-src/src/functions/mod.rs +0 -18
- package/dist-engine-src/src/functions/provider.rs +0 -130
- package/dist-engine-src/src/functions/state.rs +0 -335
- package/dist-engine-src/src/functions/types.rs +0 -37
- package/dist-engine-src/src/init.rs +0 -692
- package/dist-engine-src/src/json_store/compression.rs +0 -77
- package/dist-engine-src/src/json_store/context.rs +0 -172
- package/dist-engine-src/src/json_store/encoded.rs +0 -15
- package/dist-engine-src/src/json_store/mod.rs +0 -38
- package/dist-engine-src/src/json_store/store.rs +0 -494
- package/dist-engine-src/src/json_store/types.rs +0 -212
- package/dist-engine-src/src/lib.rs +0 -92
- package/dist-engine-src/src/live_state/context.rs +0 -1883
- package/dist-engine-src/src/live_state/mod.rs +0 -21
- package/dist-engine-src/src/live_state/overlay.rs +0 -75
- package/dist-engine-src/src/live_state/reader.rs +0 -23
- package/dist-engine-src/src/live_state/types.rs +0 -231
- package/dist-engine-src/src/live_state/visibility.rs +0 -666
- package/dist-engine-src/src/plugin/archive.rs +0 -438
- package/dist-engine-src/src/plugin/component.rs +0 -183
- package/dist-engine-src/src/plugin/install.rs +0 -619
- package/dist-engine-src/src/plugin/manifest.rs +0 -516
- package/dist-engine-src/src/plugin/materializer.rs +0 -202
- package/dist-engine-src/src/plugin/mod.rs +0 -33
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
- package/dist-engine-src/src/plugin/storage.rs +0 -74
- package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
- package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
- package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
- package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
- package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
- package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
- package/dist-engine-src/src/schema/compatibility.rs +0 -787
- package/dist-engine-src/src/schema/definition.json +0 -187
- package/dist-engine-src/src/schema/definition.rs +0 -742
- package/dist-engine-src/src/schema/key.rs +0 -138
- package/dist-engine-src/src/schema/mod.rs +0 -20
- package/dist-engine-src/src/schema/seed.rs +0 -14
- package/dist-engine-src/src/schema/tests.rs +0 -780
- package/dist-engine-src/src/session/context.rs +0 -1059
- package/dist-engine-src/src/session/create_branch.rs +0 -94
- package/dist-engine-src/src/session/execute.rs +0 -681
- package/dist-engine-src/src/session/merge/analysis.rs +0 -108
- package/dist-engine-src/src/session/merge/branch.rs +0 -417
- package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
- package/dist-engine-src/src/session/merge/mod.rs +0 -10
- package/dist-engine-src/src/session/merge/stats.rs +0 -61
- package/dist-engine-src/src/session/mod.rs +0 -30
- package/dist-engine-src/src/session/switch_branch.rs +0 -113
- package/dist-engine-src/src/session/transaction.rs +0 -557
- package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
- package/dist-engine-src/src/sql2/bind/error.rs +0 -5
- package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
- package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
- package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
- package/dist-engine-src/src/sql2/bind/read.rs +0 -65
- package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
- package/dist-engine-src/src/sql2/bind/table.rs +0 -273
- package/dist-engine-src/src/sql2/bind/write.rs +0 -86
- package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
- package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
- package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
- package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
- package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
- package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
- package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
- package/dist-engine-src/src/sql2/context.rs +0 -317
- package/dist-engine-src/src/sql2/dml.rs +0 -148
- package/dist-engine-src/src/sql2/error.rs +0 -215
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
- package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
- package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
- package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
- package/dist-engine-src/src/sql2/exec/write.rs +0 -661
- package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
- package/dist-engine-src/src/sql2/history_projection.rs +0 -56
- package/dist-engine-src/src/sql2/history_route.rs +0 -661
- package/dist-engine-src/src/sql2/mod.rs +0 -52
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
- package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
- package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
- package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
- package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
- package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
- package/dist-engine-src/src/sql2/plan/write.rs +0 -147
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
- package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
- package/dist-engine-src/src/sql2/providers/change.rs +0 -445
- package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
- package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
- package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
- package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
- package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
- package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
- package/dist-engine-src/src/sql2/providers/history.rs +0 -426
- package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
- package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
- package/dist-engine-src/src/sql2/read_only.rs +0 -63
- package/dist-engine-src/src/sql2/record_batch.rs +0 -17
- package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
- package/dist-engine-src/src/sql2/runtime.rs +0 -60
- package/dist-engine-src/src/sql2/session.rs +0 -83
- package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
- package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
- package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
- package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
- package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
- package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
- package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
- package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
- package/dist-engine-src/src/storage/conformance.rs +0 -399
- package/dist-engine-src/src/storage/context.rs +0 -620
- package/dist-engine-src/src/storage/mod.rs +0 -52
- package/dist-engine-src/src/storage/point.rs +0 -440
- package/dist-engine-src/src/storage/read_scope.rs +0 -67
- package/dist-engine-src/src/storage/reader.rs +0 -867
- package/dist-engine-src/src/storage/scan.rs +0 -784
- package/dist-engine-src/src/storage/spaces.rs +0 -236
- package/dist-engine-src/src/storage/stats.rs +0 -80
- package/dist-engine-src/src/storage/write_set.rs +0 -962
- package/dist-engine-src/src/storage_bench.rs +0 -171
- package/dist-engine-src/src/test_support.rs +0 -450
- package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
- package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
- package/dist-engine-src/src/tracked_state/context.rs +0 -2801
- package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
- package/dist-engine-src/src/tracked_state/merge.rs +0 -478
- package/dist-engine-src/src/tracked_state/mod.rs +0 -35
- package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
- package/dist-engine-src/src/tracked_state/storage.rs +0 -427
- package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
- package/dist-engine-src/src/tracked_state/types.rs +0 -238
- package/dist-engine-src/src/transaction/bench_support.rs +0 -407
- package/dist-engine-src/src/transaction/commit.rs +0 -1592
- package/dist-engine-src/src/transaction/context.rs +0 -1653
- package/dist-engine-src/src/transaction/mod.rs +0 -24
- package/dist-engine-src/src/transaction/normalization.rs +0 -877
- package/dist-engine-src/src/transaction/prep.rs +0 -37
- package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
- package/dist-engine-src/src/transaction/staging.rs +0 -1525
- package/dist-engine-src/src/transaction/types.rs +0 -403
- package/dist-engine-src/src/transaction/validation.rs +0 -5766
- package/dist-engine-src/src/untracked_state/codec.rs +0 -615
- package/dist-engine-src/src/untracked_state/context.rs +0 -98
- package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
- package/dist-engine-src/src/untracked_state/mod.rs +0 -15
- package/dist-engine-src/src/untracked_state/storage.rs +0 -898
- package/dist-engine-src/src/untracked_state/types.rs +0 -146
- package/dist-engine-src/src/wasm/mod.rs +0 -60
|
@@ -1,2140 +0,0 @@
|
|
|
1
|
-
use crate::entity_pk::EntityPk;
|
|
2
|
-
use crate::json_store::JsonRef;
|
|
3
|
-
use crate::tracked_state::types::{
|
|
4
|
-
TrackedStateIndexValue, TrackedStateKey, TrackedStateTreeScanRequest,
|
|
5
|
-
};
|
|
6
|
-
use crate::tracked_state::{TrackedStateFilter, TrackedStateStoreReader};
|
|
7
|
-
use crate::LixError;
|
|
8
|
-
|
|
9
|
-
/// Filter for comparing two tracked-state commit roots.
|
|
10
|
-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
11
|
-
pub(crate) struct TrackedStateDiffRequest {
|
|
12
|
-
pub(crate) filter: TrackedStateFilter,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/// Changed tracked-state rows between two commit roots.
|
|
16
|
-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
17
|
-
pub(crate) struct TrackedStateDiff {
|
|
18
|
-
pub(crate) entries: Vec<TrackedStateDiffEntry>,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/// One changed identity between two commit roots.
|
|
22
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
23
|
-
pub(crate) struct TrackedStateDiffEntry {
|
|
24
|
-
pub(crate) identity: TrackedStateDiffIdentity,
|
|
25
|
-
pub(crate) kind: TrackedStateDiffKind,
|
|
26
|
-
/// Raw row in the left root.
|
|
27
|
-
///
|
|
28
|
-
/// This can be a tombstone. Callers that need user-visible semantics
|
|
29
|
-
/// should use `visible_before()` instead of inspecting this directly.
|
|
30
|
-
pub(crate) before: Option<TrackedStateDiffRow>,
|
|
31
|
-
/// Raw row in the right root.
|
|
32
|
-
///
|
|
33
|
-
/// This can be a tombstone. Keeping the raw tombstone is what lets merge
|
|
34
|
-
/// apply deletes without reloading the source root.
|
|
35
|
-
pub(crate) after: Option<TrackedStateDiffRow>,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// Payload-light tracked-state row carried by diff and merge planning.
|
|
39
|
-
///
|
|
40
|
-
/// This deliberately stores JSON refs, not JSON payload strings. Diff can
|
|
41
|
-
/// compare and report rows from tracked-state tree values without hydrating
|
|
42
|
-
/// snapshot or metadata bytes.
|
|
43
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
44
|
-
pub(crate) struct TrackedStateDiffRow {
|
|
45
|
-
pub(crate) entity_pk: EntityPk,
|
|
46
|
-
pub(crate) schema_key: String,
|
|
47
|
-
pub(crate) file_id: Option<String>,
|
|
48
|
-
pub(crate) deleted: bool,
|
|
49
|
-
pub(crate) snapshot_ref: Option<JsonRef>,
|
|
50
|
-
pub(crate) metadata_ref: Option<JsonRef>,
|
|
51
|
-
pub(crate) created_at: String,
|
|
52
|
-
pub(crate) updated_at: String,
|
|
53
|
-
pub(crate) change_id: String,
|
|
54
|
-
pub(crate) commit_id: String,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/// Root-local tracked-state identity.
|
|
58
|
-
///
|
|
59
|
-
/// Entity pk used by merge/diff logic.
|
|
60
|
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
61
|
-
pub(crate) struct TrackedStateDiffIdentity {
|
|
62
|
-
pub(crate) schema_key: String,
|
|
63
|
-
pub(crate) entity_pk: EntityPk,
|
|
64
|
-
pub(crate) file_id: Option<String>,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
68
|
-
pub(crate) enum TrackedStateDiffKind {
|
|
69
|
-
Added,
|
|
70
|
-
Modified,
|
|
71
|
-
Removed,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// Diffs two tracked-state commit roots.
|
|
75
|
-
///
|
|
76
|
-
pub(crate) async fn diff_commits<S>(
|
|
77
|
-
reader: &mut TrackedStateStoreReader<S>,
|
|
78
|
-
left_commit_id: &str,
|
|
79
|
-
right_commit_id: &str,
|
|
80
|
-
request: &TrackedStateDiffRequest,
|
|
81
|
-
) -> Result<TrackedStateDiff, LixError>
|
|
82
|
-
where
|
|
83
|
-
S: crate::storage::StorageRead + Send + Sync,
|
|
84
|
-
{
|
|
85
|
-
diff_commits_with_validation(reader, left_commit_id, right_commit_id, request, true, true).await
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
pub(crate) async fn diff_commits_with_validation<S>(
|
|
89
|
-
reader: &mut TrackedStateStoreReader<S>,
|
|
90
|
-
left_commit_id: &str,
|
|
91
|
-
right_commit_id: &str,
|
|
92
|
-
request: &TrackedStateDiffRequest,
|
|
93
|
-
validate_left_root: bool,
|
|
94
|
-
validate_right_root: bool,
|
|
95
|
-
) -> Result<TrackedStateDiff, LixError>
|
|
96
|
-
where
|
|
97
|
-
S: crate::storage::StorageRead + Send + Sync,
|
|
98
|
-
{
|
|
99
|
-
let scan_request = scan_request_for_diff(request);
|
|
100
|
-
let tree_entries = reader
|
|
101
|
-
.diff_tree_entries_at_commits(left_commit_id, right_commit_id, &scan_request)
|
|
102
|
-
.await?;
|
|
103
|
-
if validate_left_root {
|
|
104
|
-
reader
|
|
105
|
-
.validate_tree_rows_at_commit_against_changelog(left_commit_id, &scan_request)
|
|
106
|
-
.await?;
|
|
107
|
-
}
|
|
108
|
-
if validate_right_root && left_commit_id != right_commit_id {
|
|
109
|
-
reader
|
|
110
|
-
.validate_tree_rows_at_commit_against_changelog(right_commit_id, &scan_request)
|
|
111
|
-
.await?;
|
|
112
|
-
}
|
|
113
|
-
let mut raw_rows = Vec::with_capacity(tree_entries.len());
|
|
114
|
-
for tree_entry in tree_entries.into_iter() {
|
|
115
|
-
let before = tree_entry
|
|
116
|
-
.before
|
|
117
|
-
.map(|(key, value)| TrackedStateDiffRow::from_tree_entry(key, value));
|
|
118
|
-
let after = tree_entry
|
|
119
|
-
.after
|
|
120
|
-
.map(|(key, value)| TrackedStateDiffRow::from_tree_entry(key, value));
|
|
121
|
-
raw_rows.push((before, after));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let mut entries = Vec::with_capacity(raw_rows.len());
|
|
125
|
-
for (before, after) in raw_rows {
|
|
126
|
-
let identity = match before.as_ref().or(after.as_ref()) {
|
|
127
|
-
Some(row) => TrackedStateDiffIdentity::from(row),
|
|
128
|
-
None => continue,
|
|
129
|
-
};
|
|
130
|
-
if identity.schema_key == "lix_commit" {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
let Some(entry) = classify_diff(identity, before, after) else {
|
|
134
|
-
continue;
|
|
135
|
-
};
|
|
136
|
-
entries.push(entry);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let diff = TrackedStateDiff { entries };
|
|
140
|
-
Ok(diff)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
fn scan_request_for_diff(request: &TrackedStateDiffRequest) -> TrackedStateTreeScanRequest {
|
|
144
|
-
let mut filter = request.filter.clone();
|
|
145
|
-
filter.include_tombstones = true;
|
|
146
|
-
TrackedStateTreeScanRequest {
|
|
147
|
-
schema_keys: filter.schema_keys,
|
|
148
|
-
entity_pks: filter.entity_pks,
|
|
149
|
-
file_ids: filter.file_ids,
|
|
150
|
-
include_tombstones: true,
|
|
151
|
-
limit: None,
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
fn classify_diff(
|
|
156
|
-
identity: TrackedStateDiffIdentity,
|
|
157
|
-
before: Option<TrackedStateDiffRow>,
|
|
158
|
-
after: Option<TrackedStateDiffRow>,
|
|
159
|
-
) -> Option<TrackedStateDiffEntry> {
|
|
160
|
-
match (is_live_row(before.as_ref()), is_live_row(after.as_ref())) {
|
|
161
|
-
(None, None) => None,
|
|
162
|
-
(None, Some(_)) => Some(TrackedStateDiffEntry {
|
|
163
|
-
identity,
|
|
164
|
-
kind: TrackedStateDiffKind::Added,
|
|
165
|
-
before,
|
|
166
|
-
after,
|
|
167
|
-
}),
|
|
168
|
-
(Some(_), None) => Some(TrackedStateDiffEntry {
|
|
169
|
-
identity,
|
|
170
|
-
kind: TrackedStateDiffKind::Removed,
|
|
171
|
-
before,
|
|
172
|
-
after,
|
|
173
|
-
}),
|
|
174
|
-
(Some(before), Some(after)) if tracked_row_payload_eq(before, after) => None,
|
|
175
|
-
(Some(_), Some(_)) => Some(TrackedStateDiffEntry {
|
|
176
|
-
identity,
|
|
177
|
-
kind: TrackedStateDiffKind::Modified,
|
|
178
|
-
before,
|
|
179
|
-
after,
|
|
180
|
-
}),
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
fn is_live_row(row: Option<&TrackedStateDiffRow>) -> Option<&TrackedStateDiffRow> {
|
|
185
|
-
row.filter(|row| !row.deleted)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
fn tracked_row_payload_eq(left: &TrackedStateDiffRow, right: &TrackedStateDiffRow) -> bool {
|
|
189
|
-
left.snapshot_ref == right.snapshot_ref && left.metadata_ref == right.metadata_ref
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
impl TrackedStateDiffIdentity {
|
|
193
|
-
fn from(row: &TrackedStateDiffRow) -> Self {
|
|
194
|
-
Self {
|
|
195
|
-
schema_key: row.schema_key.clone(),
|
|
196
|
-
entity_pk: row.entity_pk.clone(),
|
|
197
|
-
file_id: row.file_id.clone(),
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
impl TrackedStateDiffRow {
|
|
203
|
-
pub(crate) fn from_tree_entry(key: TrackedStateKey, value: TrackedStateIndexValue) -> Self {
|
|
204
|
-
Self {
|
|
205
|
-
entity_pk: key.entity_pk,
|
|
206
|
-
schema_key: key.schema_key,
|
|
207
|
-
file_id: key.file_id,
|
|
208
|
-
deleted: value.deleted,
|
|
209
|
-
snapshot_ref: value.snapshot_ref,
|
|
210
|
-
metadata_ref: value.metadata_ref,
|
|
211
|
-
created_at: value.created_at,
|
|
212
|
-
updated_at: value.updated_at,
|
|
213
|
-
change_id: value.change_id,
|
|
214
|
-
commit_id: value.commit_id,
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
pub(crate) fn into_index_entry(self) -> (TrackedStateKey, TrackedStateIndexValue) {
|
|
219
|
-
(
|
|
220
|
-
TrackedStateKey {
|
|
221
|
-
schema_key: self.schema_key,
|
|
222
|
-
file_id: self.file_id,
|
|
223
|
-
entity_pk: self.entity_pk,
|
|
224
|
-
},
|
|
225
|
-
TrackedStateIndexValue {
|
|
226
|
-
change_id: self.change_id,
|
|
227
|
-
commit_id: self.commit_id,
|
|
228
|
-
deleted: self.deleted,
|
|
229
|
-
snapshot_ref: self.snapshot_ref,
|
|
230
|
-
metadata_ref: self.metadata_ref,
|
|
231
|
-
created_at: self.created_at,
|
|
232
|
-
updated_at: self.updated_at,
|
|
233
|
-
},
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
impl TrackedStateDiffEntry {
|
|
239
|
-
#[cfg(test)]
|
|
240
|
-
pub(crate) fn before_is_live(&self) -> bool {
|
|
241
|
-
self.visible_before().is_some()
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
#[cfg(test)]
|
|
245
|
-
pub(crate) fn after_is_live(&self) -> bool {
|
|
246
|
-
self.visible_after().is_some()
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
#[cfg(test)]
|
|
250
|
-
pub(crate) fn visible_before(&self) -> Option<&TrackedStateDiffRow> {
|
|
251
|
-
self.before.as_ref().filter(|row| !row.deleted)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
#[cfg(test)]
|
|
255
|
-
pub(crate) fn visible_after(&self) -> Option<&TrackedStateDiffRow> {
|
|
256
|
-
self.after.as_ref().filter(|row| !row.deleted)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
#[cfg(test)]
|
|
261
|
-
mod tests {
|
|
262
|
-
use super::*;
|
|
263
|
-
use crate::storage::StorageContext;
|
|
264
|
-
use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
|
|
265
|
-
use crate::tracked_state::types::{
|
|
266
|
-
TrackedStateCommitRoot, TrackedStateCommitRootParent, TrackedStateMutation,
|
|
267
|
-
TrackedStateRootId,
|
|
268
|
-
};
|
|
269
|
-
use crate::tracked_state::{MaterializedTrackedStateRow, TrackedStateContext};
|
|
270
|
-
use crate::NullableKeyFilter;
|
|
271
|
-
|
|
272
|
-
#[tokio::test]
|
|
273
|
-
async fn diff_commits_reports_added_rows() {
|
|
274
|
-
let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
|
|
275
|
-
|
|
276
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
277
|
-
|
|
278
|
-
assert_eq!(
|
|
279
|
-
kinds(&diff),
|
|
280
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
|
|
281
|
-
);
|
|
282
|
-
assert!(diff.entries[0].before.is_none());
|
|
283
|
-
assert_eq!(
|
|
284
|
-
diff.entries[0]
|
|
285
|
-
.after
|
|
286
|
-
.as_ref()
|
|
287
|
-
.map(|row| row.change_id.as_str()),
|
|
288
|
-
Some("after")
|
|
289
|
-
);
|
|
290
|
-
assert!(!diff.entries[0].before_is_live());
|
|
291
|
-
assert!(diff.entries[0].after_is_live());
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
#[tokio::test]
|
|
295
|
-
async fn diff_commits_reports_removed_rows_when_right_side_is_absent() {
|
|
296
|
-
let (storage, tracked_state) = seed_roots(&[row("entity-a", None, "before")], &[]).await;
|
|
297
|
-
|
|
298
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
299
|
-
|
|
300
|
-
assert_eq!(
|
|
301
|
-
kinds(&diff),
|
|
302
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
|
|
303
|
-
);
|
|
304
|
-
assert_eq!(
|
|
305
|
-
diff.entries[0]
|
|
306
|
-
.before
|
|
307
|
-
.as_ref()
|
|
308
|
-
.map(|row| row.change_id.as_str()),
|
|
309
|
-
Some("before")
|
|
310
|
-
);
|
|
311
|
-
assert!(diff.entries[0].after.is_none());
|
|
312
|
-
assert!(diff.entries[0].before_is_live());
|
|
313
|
-
assert!(!diff.entries[0].after_is_live());
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#[tokio::test]
|
|
317
|
-
async fn diff_commits_reports_removed_rows_when_right_side_is_tombstone() {
|
|
318
|
-
let (storage, tracked_state) = seed_roots(
|
|
319
|
-
&[row("entity-a", None, "before")],
|
|
320
|
-
&[tombstone("entity-a", None, "delete")],
|
|
321
|
-
)
|
|
322
|
-
.await;
|
|
323
|
-
|
|
324
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
325
|
-
|
|
326
|
-
assert_eq!(
|
|
327
|
-
kinds(&diff),
|
|
328
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
|
|
329
|
-
);
|
|
330
|
-
let entry = &diff.entries[0];
|
|
331
|
-
assert_eq!(
|
|
332
|
-
entry.after.as_ref().map(|row| row.change_id.as_str()),
|
|
333
|
-
Some("delete")
|
|
334
|
-
);
|
|
335
|
-
assert!(
|
|
336
|
-
entry.after.as_ref().is_some_and(|row| row.deleted),
|
|
337
|
-
"removed diff should preserve the right-side tombstone for merge"
|
|
338
|
-
);
|
|
339
|
-
assert!(entry.before_is_live());
|
|
340
|
-
assert!(!entry.after_is_live());
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
#[tokio::test]
|
|
344
|
-
async fn diff_commits_reports_added_rows_when_left_side_is_tombstone() {
|
|
345
|
-
let (storage, tracked_state) = seed_roots(
|
|
346
|
-
&[tombstone("entity-a", None, "delete")],
|
|
347
|
-
&[row("entity-a", None, "after")],
|
|
348
|
-
)
|
|
349
|
-
.await;
|
|
350
|
-
|
|
351
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
352
|
-
|
|
353
|
-
assert_eq!(
|
|
354
|
-
kinds(&diff),
|
|
355
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
|
|
356
|
-
);
|
|
357
|
-
let entry = &diff.entries[0];
|
|
358
|
-
assert_eq!(
|
|
359
|
-
entry.before.as_ref().map(|row| row.change_id.as_str()),
|
|
360
|
-
Some("delete")
|
|
361
|
-
);
|
|
362
|
-
assert!(
|
|
363
|
-
entry.before.as_ref().is_some_and(|row| row.deleted),
|
|
364
|
-
"added diff should preserve the left-side tombstone for merge"
|
|
365
|
-
);
|
|
366
|
-
assert!(!entry.before_is_live());
|
|
367
|
-
assert!(entry.after_is_live());
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
#[tokio::test]
|
|
371
|
-
async fn diff_commits_reports_modified_rows_for_changed_payload() {
|
|
372
|
-
let (storage, tracked_state) = seed_roots(
|
|
373
|
-
&[row_with_value("entity-a", None, "before", "one")],
|
|
374
|
-
&[row_with_value("entity-a", None, "after", "two")],
|
|
375
|
-
)
|
|
376
|
-
.await;
|
|
377
|
-
|
|
378
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
379
|
-
|
|
380
|
-
assert_eq!(
|
|
381
|
-
kinds(&diff),
|
|
382
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
|
|
383
|
-
);
|
|
384
|
-
assert!(diff.entries[0].before_is_live());
|
|
385
|
-
assert!(diff.entries[0].after_is_live());
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
#[tokio::test]
|
|
389
|
-
async fn diff_commits_omits_unchanged_rows_even_when_metadata_differs_only_by_commit() {
|
|
390
|
-
let (storage, tracked_state) = seed_roots(
|
|
391
|
-
&[row_with_value("entity-a", None, "before", "same")],
|
|
392
|
-
&[row_with_value("entity-a", None, "after", "same")],
|
|
393
|
-
)
|
|
394
|
-
.await;
|
|
395
|
-
|
|
396
|
-
let diff = diff(&storage, &tracked_state).await;
|
|
397
|
-
|
|
398
|
-
assert!(diff.entries.is_empty());
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
#[tokio::test]
|
|
402
|
-
async fn diff_commits_distinguishes_same_entity_with_different_file_id() {
|
|
403
|
-
let (storage, tracked_state) = seed_parent_child_delta(
|
|
404
|
-
&[row("entity-a", Some("file-a"), "before-a")],
|
|
405
|
-
&[row("entity-a", Some("file-b"), "after-b")],
|
|
406
|
-
)
|
|
407
|
-
.await;
|
|
408
|
-
|
|
409
|
-
let read = storage
|
|
410
|
-
.begin_read(StorageReadOptions::default())
|
|
411
|
-
.expect("read should open");
|
|
412
|
-
let diff = tracked_state
|
|
413
|
-
.reader(read)
|
|
414
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
415
|
-
.await
|
|
416
|
-
.expect("diff should load");
|
|
417
|
-
|
|
418
|
-
assert_eq!(diff.entries.len(), 1);
|
|
419
|
-
assert_eq!(diff.entries[0].identity.file_id.as_deref(), Some("file-b"));
|
|
420
|
-
assert_eq!(diff.entries[0].kind, TrackedStateDiffKind::Added);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
#[tokio::test]
|
|
424
|
-
async fn diff_commits_filters_by_schema_entity_and_file_id() {
|
|
425
|
-
let (storage, tracked_state) = seed_roots(
|
|
426
|
-
&[],
|
|
427
|
-
&[
|
|
428
|
-
row_with_schema("entity-a", Some("file-a"), "schema-a", "change-a"),
|
|
429
|
-
row_with_schema("entity-b", Some("file-b"), "schema-b", "change-b"),
|
|
430
|
-
],
|
|
431
|
-
)
|
|
432
|
-
.await;
|
|
433
|
-
let read = storage
|
|
434
|
-
.begin_read(StorageReadOptions::default())
|
|
435
|
-
.expect("read should open");
|
|
436
|
-
let mut reader = tracked_state.reader(read);
|
|
437
|
-
let diff = reader
|
|
438
|
-
.diff_commits(
|
|
439
|
-
"left",
|
|
440
|
-
"right",
|
|
441
|
-
&TrackedStateDiffRequest {
|
|
442
|
-
filter: TrackedStateFilter {
|
|
443
|
-
schema_keys: vec!["schema-b".to_string()],
|
|
444
|
-
entity_pks: vec![crate::entity_pk::EntityPk::single("entity-b")],
|
|
445
|
-
file_ids: vec![NullableKeyFilter::Value("file-b".to_string())],
|
|
446
|
-
..Default::default()
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
)
|
|
450
|
-
.await
|
|
451
|
-
.expect("diff should load");
|
|
452
|
-
|
|
453
|
-
assert_eq!(
|
|
454
|
-
kinds(&diff),
|
|
455
|
-
vec![("entity-b".to_string(), TrackedStateDiffKind::Added)]
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
#[tokio::test]
|
|
460
|
-
async fn diff_validation_rejects_row_identity_that_does_not_match_changelog_change() {
|
|
461
|
-
let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
|
|
462
|
-
let mut diff = diff(&storage, &tracked_state).await;
|
|
463
|
-
diff.entries[0].after.as_mut().expect("after row").entity_pk =
|
|
464
|
-
EntityPk::single("entity-corrupt");
|
|
465
|
-
|
|
466
|
-
let read = storage
|
|
467
|
-
.begin_read(StorageReadOptions::default())
|
|
468
|
-
.expect("read should open");
|
|
469
|
-
let error = tracked_state
|
|
470
|
-
.reader(read)
|
|
471
|
-
.validate_diff_rows_for_commits_against_changelog(&[(
|
|
472
|
-
diff.entries[0].after.as_ref().expect("after row"),
|
|
473
|
-
"right",
|
|
474
|
-
)])
|
|
475
|
-
.await
|
|
476
|
-
.expect_err("identity drift must be rejected");
|
|
477
|
-
|
|
478
|
-
assert!(
|
|
479
|
-
error
|
|
480
|
-
.message
|
|
481
|
-
.contains("does not match changelog change identity")
|
|
482
|
-
|| error.message.contains("changelog commit"),
|
|
483
|
-
"unexpected error: {error}"
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
#[tokio::test]
|
|
488
|
-
async fn diff_validation_rejects_missing_changelog_change() {
|
|
489
|
-
let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
|
|
490
|
-
let mut diff = diff(&storage, &tracked_state).await;
|
|
491
|
-
diff.entries[0].after.as_mut().expect("after row").change_id = "missing-change".to_string();
|
|
492
|
-
|
|
493
|
-
let read = storage
|
|
494
|
-
.begin_read(StorageReadOptions::default())
|
|
495
|
-
.expect("read should open");
|
|
496
|
-
let error = tracked_state
|
|
497
|
-
.reader(read)
|
|
498
|
-
.validate_diff_rows_for_commits_against_changelog(&[(
|
|
499
|
-
diff.entries[0].after.as_ref().expect("after row"),
|
|
500
|
-
"right",
|
|
501
|
-
)])
|
|
502
|
-
.await
|
|
503
|
-
.expect_err("missing change must be rejected");
|
|
504
|
-
|
|
505
|
-
assert!(
|
|
506
|
-
error.message.contains("missing changelog change"),
|
|
507
|
-
"unexpected error: {error}"
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
#[tokio::test]
|
|
512
|
-
async fn diff_validation_rejects_forged_updated_at() {
|
|
513
|
-
let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
|
|
514
|
-
let mut diff = diff(&storage, &tracked_state).await;
|
|
515
|
-
diff.entries[0]
|
|
516
|
-
.after
|
|
517
|
-
.as_mut()
|
|
518
|
-
.expect("after row")
|
|
519
|
-
.updated_at = "2026-01-02T00:00:00Z".to_string();
|
|
520
|
-
|
|
521
|
-
let read = storage
|
|
522
|
-
.begin_read(StorageReadOptions::default())
|
|
523
|
-
.expect("read should open");
|
|
524
|
-
let error = tracked_state
|
|
525
|
-
.reader(read)
|
|
526
|
-
.validate_diff_rows_for_commits_against_changelog(&[(
|
|
527
|
-
diff.entries[0].after.as_ref().expect("after row"),
|
|
528
|
-
"right",
|
|
529
|
-
)])
|
|
530
|
-
.await
|
|
531
|
-
.expect_err("forged updated_at must be rejected");
|
|
532
|
-
|
|
533
|
-
assert!(
|
|
534
|
-
error.message.contains("updated_at does not match"),
|
|
535
|
-
"unexpected error: {error}"
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
#[tokio::test]
|
|
540
|
-
async fn diff_validation_rejects_forged_created_at() {
|
|
541
|
-
let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
|
|
542
|
-
let mut diff = diff(&storage, &tracked_state).await;
|
|
543
|
-
diff.entries[0]
|
|
544
|
-
.after
|
|
545
|
-
.as_mut()
|
|
546
|
-
.expect("after row")
|
|
547
|
-
.created_at = "2025-12-31T00:00:00Z".to_string();
|
|
548
|
-
|
|
549
|
-
let read = storage
|
|
550
|
-
.begin_read(StorageReadOptions::default())
|
|
551
|
-
.expect("read should open");
|
|
552
|
-
let error = tracked_state
|
|
553
|
-
.reader(read)
|
|
554
|
-
.validate_diff_rows_for_commits_against_changelog(&[(
|
|
555
|
-
diff.entries[0].after.as_ref().expect("after row"),
|
|
556
|
-
"right",
|
|
557
|
-
)])
|
|
558
|
-
.await
|
|
559
|
-
.expect_err("forged created_at must be rejected");
|
|
560
|
-
|
|
561
|
-
assert!(
|
|
562
|
-
error.message.contains("created_at"),
|
|
563
|
-
"unexpected error: {error}"
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
#[tokio::test]
|
|
568
|
-
async fn diff_commits_rejects_update_with_arbitrary_forged_created_at() {
|
|
569
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
570
|
-
let tracked_state = TrackedStateContext::new();
|
|
571
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
572
|
-
.await
|
|
573
|
-
.expect("left root should write");
|
|
574
|
-
write_root_committed_for_test(
|
|
575
|
-
&storage,
|
|
576
|
-
&tracked_state,
|
|
577
|
-
"parent",
|
|
578
|
-
None,
|
|
579
|
-
&[row_with_times(
|
|
580
|
-
"entity-a",
|
|
581
|
-
None,
|
|
582
|
-
"parent-change",
|
|
583
|
-
"old",
|
|
584
|
-
"2026-01-01T00:00:00Z",
|
|
585
|
-
"2026-01-01T00:00:00Z",
|
|
586
|
-
)],
|
|
587
|
-
)
|
|
588
|
-
.await
|
|
589
|
-
.expect("parent root should write");
|
|
590
|
-
write_root_committed_for_test(
|
|
591
|
-
&storage,
|
|
592
|
-
&tracked_state,
|
|
593
|
-
"child",
|
|
594
|
-
Some("parent"),
|
|
595
|
-
&[row_with_times(
|
|
596
|
-
"entity-a",
|
|
597
|
-
None,
|
|
598
|
-
"child-change",
|
|
599
|
-
"new",
|
|
600
|
-
"2026-01-02T00:00:00Z",
|
|
601
|
-
"2026-01-02T00:00:00Z",
|
|
602
|
-
)],
|
|
603
|
-
)
|
|
604
|
-
.await
|
|
605
|
-
.expect("child root should write");
|
|
606
|
-
|
|
607
|
-
let read = storage
|
|
608
|
-
.begin_read(StorageReadOptions::default())
|
|
609
|
-
.expect("read should open");
|
|
610
|
-
let valid_diff = tracked_state
|
|
611
|
-
.reader(read)
|
|
612
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
613
|
-
.await
|
|
614
|
-
.expect("valid update should load");
|
|
615
|
-
let row = valid_diff
|
|
616
|
-
.entries
|
|
617
|
-
.iter()
|
|
618
|
-
.find_map(|entry| entry.after.clone())
|
|
619
|
-
.expect("child row should appear");
|
|
620
|
-
let (key, mut value) = row.into_index_entry();
|
|
621
|
-
value.created_at = "2026-01-03T00:00:00Z".to_string();
|
|
622
|
-
let parent_commit_row =
|
|
623
|
-
commit_root_row_entry("parent", "parent:commit", "2026-01-01T00:00:00Z");
|
|
624
|
-
let commit_row = commit_root_row_entry("child", "child:commit", "2026-01-02T00:00:00Z");
|
|
625
|
-
stage_corrupt_commit_root(
|
|
626
|
-
&storage,
|
|
627
|
-
"child",
|
|
628
|
-
vec![(key, value), parent_commit_row, commit_row],
|
|
629
|
-
vec![TrackedStateCommitRootParent {
|
|
630
|
-
commit_id: "parent".to_string(),
|
|
631
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
632
|
-
}],
|
|
633
|
-
)
|
|
634
|
-
.await;
|
|
635
|
-
|
|
636
|
-
let read = storage
|
|
637
|
-
.begin_read(StorageReadOptions::default())
|
|
638
|
-
.expect("read should open");
|
|
639
|
-
let error = tracked_state
|
|
640
|
-
.reader(read)
|
|
641
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
642
|
-
.await
|
|
643
|
-
.expect_err("arbitrary forged created_at must be rejected");
|
|
644
|
-
|
|
645
|
-
assert!(
|
|
646
|
-
error.message.contains("created_at"),
|
|
647
|
-
"unexpected error: {error}"
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
#[tokio::test]
|
|
652
|
-
async fn diff_commits_validates_same_payload_rows_before_classification_drops_them() {
|
|
653
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
654
|
-
let tracked_state = TrackedStateContext::new();
|
|
655
|
-
write_root_committed_for_test(
|
|
656
|
-
&storage,
|
|
657
|
-
&tracked_state,
|
|
658
|
-
"left",
|
|
659
|
-
None,
|
|
660
|
-
&[row_with_value("entity-a", None, "left-a", "same")],
|
|
661
|
-
)
|
|
662
|
-
.await
|
|
663
|
-
.expect("left root should write");
|
|
664
|
-
write_root_committed_for_test(
|
|
665
|
-
&storage,
|
|
666
|
-
&tracked_state,
|
|
667
|
-
"right-valid",
|
|
668
|
-
None,
|
|
669
|
-
&[row_with_value("entity-b", None, "right-b", "same")],
|
|
670
|
-
)
|
|
671
|
-
.await
|
|
672
|
-
.expect("right changelog should write");
|
|
673
|
-
|
|
674
|
-
let read = storage
|
|
675
|
-
.begin_read(StorageReadOptions::default())
|
|
676
|
-
.expect("read should open");
|
|
677
|
-
let valid_diff = tracked_state
|
|
678
|
-
.reader(read)
|
|
679
|
-
.diff_commits("left", "right-valid", &TrackedStateDiffRequest::default())
|
|
680
|
-
.await
|
|
681
|
-
.expect("valid diff should load");
|
|
682
|
-
let source_row = valid_diff
|
|
683
|
-
.entries
|
|
684
|
-
.iter()
|
|
685
|
-
.find_map(|entry| entry.after.clone())
|
|
686
|
-
.expect("right row should appear in valid diff");
|
|
687
|
-
let (_source_key, source_value) = source_row.into_index_entry();
|
|
688
|
-
let corrupt_key = TrackedStateKey {
|
|
689
|
-
schema_key: "test_schema".to_string(),
|
|
690
|
-
file_id: None,
|
|
691
|
-
entity_pk: EntityPk::single("entity-a"),
|
|
692
|
-
};
|
|
693
|
-
let result = {
|
|
694
|
-
let mut read = storage
|
|
695
|
-
.begin_read(StorageReadOptions::default())
|
|
696
|
-
.expect("read should open");
|
|
697
|
-
let mut writes = storage.new_write_set();
|
|
698
|
-
let result = crate::tracked_state::tree::TrackedStateTree::new()
|
|
699
|
-
.apply_mutations(
|
|
700
|
-
&mut read,
|
|
701
|
-
&mut writes,
|
|
702
|
-
None,
|
|
703
|
-
vec![TrackedStateMutation::put_encoded(
|
|
704
|
-
crate::tracked_state::codec::encode_key(&corrupt_key),
|
|
705
|
-
crate::tracked_state::codec::encode_value(&source_value),
|
|
706
|
-
)],
|
|
707
|
-
Some("right-corrupt"),
|
|
708
|
-
)
|
|
709
|
-
.await
|
|
710
|
-
.expect("corrupt root should write");
|
|
711
|
-
crate::tracked_state::storage::stage_commit_root(
|
|
712
|
-
&mut writes,
|
|
713
|
-
&TrackedStateCommitRoot {
|
|
714
|
-
commit_id: "right-corrupt".to_string(),
|
|
715
|
-
root_id: result.root_id.clone(),
|
|
716
|
-
parent_roots: Vec::new(),
|
|
717
|
-
changed_key_count: 1,
|
|
718
|
-
row_count_estimate: result.row_count as u64,
|
|
719
|
-
tree_height: result.tree_height as u32,
|
|
720
|
-
primary_chunk_count: result.chunk_count as u64,
|
|
721
|
-
primary_chunk_bytes: result.chunk_bytes as u64,
|
|
722
|
-
},
|
|
723
|
-
)
|
|
724
|
-
.expect("metadata should encode");
|
|
725
|
-
storage
|
|
726
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
727
|
-
.expect("corrupt root should commit");
|
|
728
|
-
result
|
|
729
|
-
};
|
|
730
|
-
assert_eq!(result.row_count, 1);
|
|
731
|
-
|
|
732
|
-
let read = storage
|
|
733
|
-
.begin_read(StorageReadOptions::default())
|
|
734
|
-
.expect("read should open");
|
|
735
|
-
let error = tracked_state
|
|
736
|
-
.reader(read)
|
|
737
|
-
.diff_commits("left", "right-corrupt", &TrackedStateDiffRequest::default())
|
|
738
|
-
.await
|
|
739
|
-
.expect_err("raw same-payload corruption must be rejected before classification");
|
|
740
|
-
|
|
741
|
-
assert!(
|
|
742
|
-
error
|
|
743
|
-
.message
|
|
744
|
-
.contains("does not match changelog change identity")
|
|
745
|
-
|| error.message.contains("changelog commit"),
|
|
746
|
-
"unexpected error: {error}"
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
#[tokio::test]
|
|
751
|
-
async fn diff_commits_rejects_stale_ancestor_row_that_is_not_root_winner() {
|
|
752
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
753
|
-
let tracked_state = TrackedStateContext::new();
|
|
754
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
755
|
-
.await
|
|
756
|
-
.expect("left root should write");
|
|
757
|
-
write_root_committed_for_test(
|
|
758
|
-
&storage,
|
|
759
|
-
&tracked_state,
|
|
760
|
-
"parent",
|
|
761
|
-
None,
|
|
762
|
-
&[row_with_value("entity-a", None, "parent-change", "old")],
|
|
763
|
-
)
|
|
764
|
-
.await
|
|
765
|
-
.expect("parent root should write");
|
|
766
|
-
write_root_committed_for_test(
|
|
767
|
-
&storage,
|
|
768
|
-
&tracked_state,
|
|
769
|
-
"child",
|
|
770
|
-
Some("parent"),
|
|
771
|
-
&[row_with_value("entity-a", None, "child-change", "new")],
|
|
772
|
-
)
|
|
773
|
-
.await
|
|
774
|
-
.expect("child root should write");
|
|
775
|
-
|
|
776
|
-
let read = storage
|
|
777
|
-
.begin_read(StorageReadOptions::default())
|
|
778
|
-
.expect("read should open");
|
|
779
|
-
let parent_diff = tracked_state
|
|
780
|
-
.reader(read)
|
|
781
|
-
.diff_commits("left", "parent", &TrackedStateDiffRequest::default())
|
|
782
|
-
.await
|
|
783
|
-
.expect("parent diff should load");
|
|
784
|
-
let stale_row = parent_diff
|
|
785
|
-
.entries
|
|
786
|
-
.iter()
|
|
787
|
-
.find_map(|entry| entry.after.clone())
|
|
788
|
-
.expect("parent row should appear");
|
|
789
|
-
let (stale_key, stale_value) = stale_row.into_index_entry();
|
|
790
|
-
stage_corrupt_commit_root(
|
|
791
|
-
&storage,
|
|
792
|
-
"child",
|
|
793
|
-
vec![(stale_key, stale_value)],
|
|
794
|
-
vec![TrackedStateCommitRootParent {
|
|
795
|
-
commit_id: "parent".to_string(),
|
|
796
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
797
|
-
}],
|
|
798
|
-
)
|
|
799
|
-
.await;
|
|
800
|
-
|
|
801
|
-
let read = storage
|
|
802
|
-
.begin_read(StorageReadOptions::default())
|
|
803
|
-
.expect("read should open");
|
|
804
|
-
let error = tracked_state
|
|
805
|
-
.reader(read)
|
|
806
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
807
|
-
.await
|
|
808
|
-
.expect_err("stale ancestor winner must be rejected");
|
|
809
|
-
|
|
810
|
-
assert!(
|
|
811
|
-
is_commit_root_validation_error(&error),
|
|
812
|
-
"unexpected error: {error}"
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
#[tokio::test]
|
|
817
|
-
async fn diff_commits_rejects_valid_change_from_unreachable_commit_root() {
|
|
818
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
819
|
-
let tracked_state = TrackedStateContext::new();
|
|
820
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
821
|
-
.await
|
|
822
|
-
.expect("left root should write");
|
|
823
|
-
write_root_committed_for_test(
|
|
824
|
-
&storage,
|
|
825
|
-
&tracked_state,
|
|
826
|
-
"unrelated",
|
|
827
|
-
None,
|
|
828
|
-
&[row_with_value(
|
|
829
|
-
"entity-a",
|
|
830
|
-
None,
|
|
831
|
-
"unrelated-change",
|
|
832
|
-
"value",
|
|
833
|
-
)],
|
|
834
|
-
)
|
|
835
|
-
.await
|
|
836
|
-
.expect("unrelated changelog should write");
|
|
837
|
-
|
|
838
|
-
let read = storage
|
|
839
|
-
.begin_read(StorageReadOptions::default())
|
|
840
|
-
.expect("read should open");
|
|
841
|
-
let unrelated_diff = tracked_state
|
|
842
|
-
.reader(read)
|
|
843
|
-
.diff_commits("left", "unrelated", &TrackedStateDiffRequest::default())
|
|
844
|
-
.await
|
|
845
|
-
.expect("valid unrelated diff should load");
|
|
846
|
-
let source_row = unrelated_diff
|
|
847
|
-
.entries
|
|
848
|
-
.iter()
|
|
849
|
-
.find_map(|entry| entry.after.clone())
|
|
850
|
-
.expect("unrelated row should appear in valid diff");
|
|
851
|
-
let (source_key, source_value) = source_row.into_index_entry();
|
|
852
|
-
|
|
853
|
-
let result = {
|
|
854
|
-
let mut read = storage
|
|
855
|
-
.begin_read(StorageReadOptions::default())
|
|
856
|
-
.expect("read should open");
|
|
857
|
-
let mut writes = storage.new_write_set();
|
|
858
|
-
crate::test_support::stage_empty_changelog_commit(
|
|
859
|
-
&mut read,
|
|
860
|
-
&mut writes,
|
|
861
|
-
"right-corrupt",
|
|
862
|
-
None,
|
|
863
|
-
)
|
|
864
|
-
.await
|
|
865
|
-
.expect("empty right changelog should write");
|
|
866
|
-
let result = crate::tracked_state::tree::TrackedStateTree::new()
|
|
867
|
-
.apply_mutations(
|
|
868
|
-
&mut read,
|
|
869
|
-
&mut writes,
|
|
870
|
-
None,
|
|
871
|
-
vec![TrackedStateMutation::put_encoded(
|
|
872
|
-
crate::tracked_state::codec::encode_key(&source_key),
|
|
873
|
-
crate::tracked_state::codec::encode_value(&source_value),
|
|
874
|
-
)],
|
|
875
|
-
Some("right-corrupt"),
|
|
876
|
-
)
|
|
877
|
-
.await
|
|
878
|
-
.expect("corrupt root should write");
|
|
879
|
-
crate::tracked_state::storage::stage_commit_root(
|
|
880
|
-
&mut writes,
|
|
881
|
-
&TrackedStateCommitRoot {
|
|
882
|
-
commit_id: "right-corrupt".to_string(),
|
|
883
|
-
root_id: result.root_id.clone(),
|
|
884
|
-
parent_roots: Vec::new(),
|
|
885
|
-
changed_key_count: 1,
|
|
886
|
-
row_count_estimate: result.row_count as u64,
|
|
887
|
-
tree_height: result.tree_height as u32,
|
|
888
|
-
primary_chunk_count: result.chunk_count as u64,
|
|
889
|
-
primary_chunk_bytes: result.chunk_bytes as u64,
|
|
890
|
-
},
|
|
891
|
-
)
|
|
892
|
-
.expect("metadata should encode");
|
|
893
|
-
storage
|
|
894
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
895
|
-
.expect("corrupt root should commit");
|
|
896
|
-
result
|
|
897
|
-
};
|
|
898
|
-
assert_eq!(result.row_count, 1);
|
|
899
|
-
|
|
900
|
-
let read = storage
|
|
901
|
-
.begin_read(StorageReadOptions::default())
|
|
902
|
-
.expect("read should open");
|
|
903
|
-
let error = tracked_state
|
|
904
|
-
.reader(read)
|
|
905
|
-
.diff_commits("left", "right-corrupt", &TrackedStateDiffRequest::default())
|
|
906
|
-
.await
|
|
907
|
-
.expect_err("unreachable valid change must be rejected");
|
|
908
|
-
|
|
909
|
-
assert!(
|
|
910
|
-
is_commit_root_validation_error(&error),
|
|
911
|
-
"unexpected error: {error}"
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
#[tokio::test]
|
|
916
|
-
async fn diff_commits_rejects_second_parent_row_without_commit_root_proof() {
|
|
917
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
918
|
-
let tracked_state = TrackedStateContext::new();
|
|
919
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
920
|
-
.await
|
|
921
|
-
.expect("left root should write");
|
|
922
|
-
write_root_committed_for_test(&storage, &tracked_state, "target", None, &[])
|
|
923
|
-
.await
|
|
924
|
-
.expect("target root should write");
|
|
925
|
-
write_root_committed_for_test(
|
|
926
|
-
&storage,
|
|
927
|
-
&tracked_state,
|
|
928
|
-
"source",
|
|
929
|
-
None,
|
|
930
|
-
&[row_with_value("entity-a", None, "source-change", "value")],
|
|
931
|
-
)
|
|
932
|
-
.await
|
|
933
|
-
.expect("source root should write");
|
|
934
|
-
|
|
935
|
-
let read = storage
|
|
936
|
-
.begin_read(StorageReadOptions::default())
|
|
937
|
-
.expect("read should open");
|
|
938
|
-
let source_diff = tracked_state
|
|
939
|
-
.reader(read)
|
|
940
|
-
.diff_commits("left", "source", &TrackedStateDiffRequest::default())
|
|
941
|
-
.await
|
|
942
|
-
.expect("source diff should load");
|
|
943
|
-
let source_row = source_diff
|
|
944
|
-
.entries
|
|
945
|
-
.iter()
|
|
946
|
-
.find_map(|entry| entry.after.clone())
|
|
947
|
-
.expect("source row should appear");
|
|
948
|
-
let (source_key, source_value) = source_row.into_index_entry();
|
|
949
|
-
|
|
950
|
-
{
|
|
951
|
-
let mut read = storage
|
|
952
|
-
.begin_read(StorageReadOptions::default())
|
|
953
|
-
.expect("read should open");
|
|
954
|
-
let mut writes = storage.new_write_set();
|
|
955
|
-
crate::test_support::stage_empty_changelog_commit_with_parents(
|
|
956
|
-
&mut read,
|
|
957
|
-
&mut writes,
|
|
958
|
-
"merge",
|
|
959
|
-
&["target".to_string(), "source".to_string()],
|
|
960
|
-
)
|
|
961
|
-
.await
|
|
962
|
-
.expect("merge changelog should write");
|
|
963
|
-
storage
|
|
964
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
965
|
-
.expect("merge changelog should commit");
|
|
966
|
-
}
|
|
967
|
-
stage_corrupt_commit_root(
|
|
968
|
-
&storage,
|
|
969
|
-
"merge",
|
|
970
|
-
vec![(source_key, source_value)],
|
|
971
|
-
vec![TrackedStateCommitRootParent {
|
|
972
|
-
commit_id: "target".to_string(),
|
|
973
|
-
root_id: tracked_state_root_id(&storage, "target").await,
|
|
974
|
-
}],
|
|
975
|
-
)
|
|
976
|
-
.await;
|
|
977
|
-
|
|
978
|
-
let read = storage
|
|
979
|
-
.begin_read(StorageReadOptions::default())
|
|
980
|
-
.expect("read should open");
|
|
981
|
-
let error = tracked_state
|
|
982
|
-
.reader(read)
|
|
983
|
-
.diff_commits("left", "merge", &TrackedStateDiffRequest::default())
|
|
984
|
-
.await
|
|
985
|
-
.expect_err("second-parent row without commit-root proof must be rejected");
|
|
986
|
-
|
|
987
|
-
assert!(
|
|
988
|
-
is_commit_root_validation_error(&error),
|
|
989
|
-
"unexpected error: {error}"
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
#[tokio::test]
|
|
994
|
-
async fn diff_commits_rejects_second_parent_row_with_forged_commit_root_parent() {
|
|
995
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
996
|
-
let tracked_state = TrackedStateContext::new();
|
|
997
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
998
|
-
.await
|
|
999
|
-
.expect("left root should write");
|
|
1000
|
-
write_root_committed_for_test(&storage, &tracked_state, "target", None, &[])
|
|
1001
|
-
.await
|
|
1002
|
-
.expect("target root should write");
|
|
1003
|
-
write_root_committed_for_test(
|
|
1004
|
-
&storage,
|
|
1005
|
-
&tracked_state,
|
|
1006
|
-
"source",
|
|
1007
|
-
None,
|
|
1008
|
-
&[row_with_value("entity-a", None, "source-change", "value")],
|
|
1009
|
-
)
|
|
1010
|
-
.await
|
|
1011
|
-
.expect("source root should write");
|
|
1012
|
-
|
|
1013
|
-
let read = storage
|
|
1014
|
-
.begin_read(StorageReadOptions::default())
|
|
1015
|
-
.expect("read should open");
|
|
1016
|
-
let source_diff = tracked_state
|
|
1017
|
-
.reader(read)
|
|
1018
|
-
.diff_commits("left", "source", &TrackedStateDiffRequest::default())
|
|
1019
|
-
.await
|
|
1020
|
-
.expect("source diff should load");
|
|
1021
|
-
let source_row = source_diff
|
|
1022
|
-
.entries
|
|
1023
|
-
.iter()
|
|
1024
|
-
.find_map(|entry| entry.after.clone())
|
|
1025
|
-
.expect("source row should appear");
|
|
1026
|
-
let (source_key, source_value) = source_row.into_index_entry();
|
|
1027
|
-
|
|
1028
|
-
{
|
|
1029
|
-
let mut read = storage
|
|
1030
|
-
.begin_read(StorageReadOptions::default())
|
|
1031
|
-
.expect("read should open");
|
|
1032
|
-
let mut writes = storage.new_write_set();
|
|
1033
|
-
crate::test_support::stage_empty_changelog_commit_with_parents(
|
|
1034
|
-
&mut read,
|
|
1035
|
-
&mut writes,
|
|
1036
|
-
"merge",
|
|
1037
|
-
&["target".to_string(), "source".to_string()],
|
|
1038
|
-
)
|
|
1039
|
-
.await
|
|
1040
|
-
.expect("merge changelog should write");
|
|
1041
|
-
storage
|
|
1042
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1043
|
-
.expect("merge changelog should commit");
|
|
1044
|
-
}
|
|
1045
|
-
stage_corrupt_commit_root(
|
|
1046
|
-
&storage,
|
|
1047
|
-
"merge",
|
|
1048
|
-
vec![(source_key, source_value)],
|
|
1049
|
-
vec![TrackedStateCommitRootParent {
|
|
1050
|
-
commit_id: "source".to_string(),
|
|
1051
|
-
root_id: tracked_state_root_id(&storage, "source").await,
|
|
1052
|
-
}],
|
|
1053
|
-
)
|
|
1054
|
-
.await;
|
|
1055
|
-
|
|
1056
|
-
let read = storage
|
|
1057
|
-
.begin_read(StorageReadOptions::default())
|
|
1058
|
-
.expect("read should open");
|
|
1059
|
-
let error = tracked_state
|
|
1060
|
-
.reader(read)
|
|
1061
|
-
.diff_commits("left", "merge", &TrackedStateDiffRequest::default())
|
|
1062
|
-
.await
|
|
1063
|
-
.expect_err("forged source parent must be rejected");
|
|
1064
|
-
|
|
1065
|
-
assert!(
|
|
1066
|
-
is_commit_root_validation_error(&error),
|
|
1067
|
-
"unexpected error: {error}"
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
#[tokio::test]
|
|
1072
|
-
async fn diff_commits_rejects_unrelated_row_with_forged_commit_root_parent() {
|
|
1073
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1074
|
-
let tracked_state = TrackedStateContext::new();
|
|
1075
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
1076
|
-
.await
|
|
1077
|
-
.expect("left root should write");
|
|
1078
|
-
write_root_committed_for_test(
|
|
1079
|
-
&storage,
|
|
1080
|
-
&tracked_state,
|
|
1081
|
-
"source",
|
|
1082
|
-
None,
|
|
1083
|
-
&[row_with_value("entity-a", None, "source-change", "value")],
|
|
1084
|
-
)
|
|
1085
|
-
.await
|
|
1086
|
-
.expect("source root should write");
|
|
1087
|
-
|
|
1088
|
-
let read = storage
|
|
1089
|
-
.begin_read(StorageReadOptions::default())
|
|
1090
|
-
.expect("read should open");
|
|
1091
|
-
let source_diff = tracked_state
|
|
1092
|
-
.reader(read)
|
|
1093
|
-
.diff_commits("left", "source", &TrackedStateDiffRequest::default())
|
|
1094
|
-
.await
|
|
1095
|
-
.expect("source diff should load");
|
|
1096
|
-
let source_row = source_diff
|
|
1097
|
-
.entries
|
|
1098
|
-
.iter()
|
|
1099
|
-
.find_map(|entry| entry.after.clone())
|
|
1100
|
-
.expect("source row should appear");
|
|
1101
|
-
let (source_key, source_value) = source_row.into_index_entry();
|
|
1102
|
-
|
|
1103
|
-
{
|
|
1104
|
-
let mut read = storage
|
|
1105
|
-
.begin_read(StorageReadOptions::default())
|
|
1106
|
-
.expect("read should open");
|
|
1107
|
-
let mut writes = storage.new_write_set();
|
|
1108
|
-
crate::test_support::stage_empty_changelog_commit(
|
|
1109
|
-
&mut read,
|
|
1110
|
-
&mut writes,
|
|
1111
|
-
"right-corrupt",
|
|
1112
|
-
None,
|
|
1113
|
-
)
|
|
1114
|
-
.await
|
|
1115
|
-
.expect("empty right changelog should write");
|
|
1116
|
-
storage
|
|
1117
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1118
|
-
.expect("right changelog should commit");
|
|
1119
|
-
}
|
|
1120
|
-
stage_corrupt_commit_root(
|
|
1121
|
-
&storage,
|
|
1122
|
-
"right-corrupt",
|
|
1123
|
-
vec![(source_key, source_value)],
|
|
1124
|
-
vec![TrackedStateCommitRootParent {
|
|
1125
|
-
commit_id: "source".to_string(),
|
|
1126
|
-
root_id: tracked_state_root_id(&storage, "source").await,
|
|
1127
|
-
}],
|
|
1128
|
-
)
|
|
1129
|
-
.await;
|
|
1130
|
-
|
|
1131
|
-
let read = storage
|
|
1132
|
-
.begin_read(StorageReadOptions::default())
|
|
1133
|
-
.expect("read should open");
|
|
1134
|
-
let error = tracked_state
|
|
1135
|
-
.reader(read)
|
|
1136
|
-
.diff_commits("left", "right-corrupt", &TrackedStateDiffRequest::default())
|
|
1137
|
-
.await
|
|
1138
|
-
.expect_err("forged unrelated parent must be rejected");
|
|
1139
|
-
|
|
1140
|
-
assert!(
|
|
1141
|
-
is_commit_root_validation_error(&error),
|
|
1142
|
-
"unexpected error: {error}"
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
#[tokio::test]
|
|
1147
|
-
async fn diff_commits_rejects_forged_parent_metadata_even_for_current_winner_rows() {
|
|
1148
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1149
|
-
let tracked_state = TrackedStateContext::new();
|
|
1150
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
1151
|
-
.await
|
|
1152
|
-
.expect("left root should write");
|
|
1153
|
-
write_root_committed_for_test(&storage, &tracked_state, "target", None, &[])
|
|
1154
|
-
.await
|
|
1155
|
-
.expect("target root should write");
|
|
1156
|
-
write_root_committed_for_test(
|
|
1157
|
-
&storage,
|
|
1158
|
-
&tracked_state,
|
|
1159
|
-
"source",
|
|
1160
|
-
None,
|
|
1161
|
-
&[row_with_value("entity-b", None, "source-b", "source")],
|
|
1162
|
-
)
|
|
1163
|
-
.await
|
|
1164
|
-
.expect("source root should write");
|
|
1165
|
-
write_root_committed_for_test(
|
|
1166
|
-
&storage,
|
|
1167
|
-
&tracked_state,
|
|
1168
|
-
"child",
|
|
1169
|
-
Some("target"),
|
|
1170
|
-
&[row_with_value("entity-a", None, "child-a", "current")],
|
|
1171
|
-
)
|
|
1172
|
-
.await
|
|
1173
|
-
.expect("child root should write");
|
|
1174
|
-
|
|
1175
|
-
let read = storage
|
|
1176
|
-
.begin_read(StorageReadOptions::default())
|
|
1177
|
-
.expect("read should open");
|
|
1178
|
-
let child_diff = tracked_state
|
|
1179
|
-
.reader(read)
|
|
1180
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
1181
|
-
.await
|
|
1182
|
-
.expect("child diff should load");
|
|
1183
|
-
let child_row = child_diff
|
|
1184
|
-
.entries
|
|
1185
|
-
.iter()
|
|
1186
|
-
.find_map(|entry| entry.after.clone())
|
|
1187
|
-
.expect("child row should appear");
|
|
1188
|
-
let (child_key, child_value) = child_row.into_index_entry();
|
|
1189
|
-
|
|
1190
|
-
stage_corrupt_commit_root(
|
|
1191
|
-
&storage,
|
|
1192
|
-
"child",
|
|
1193
|
-
vec![(child_key, child_value)],
|
|
1194
|
-
vec![TrackedStateCommitRootParent {
|
|
1195
|
-
commit_id: "source".to_string(),
|
|
1196
|
-
root_id: tracked_state_root_id(&storage, "source").await,
|
|
1197
|
-
}],
|
|
1198
|
-
)
|
|
1199
|
-
.await;
|
|
1200
|
-
|
|
1201
|
-
let read = storage
|
|
1202
|
-
.begin_read(StorageReadOptions::default())
|
|
1203
|
-
.expect("read should open");
|
|
1204
|
-
let error = tracked_state
|
|
1205
|
-
.reader(read)
|
|
1206
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
1207
|
-
.await
|
|
1208
|
-
.expect_err("current winner root metadata must still be validated");
|
|
1209
|
-
|
|
1210
|
-
assert!(
|
|
1211
|
-
is_commit_root_validation_error(&error),
|
|
1212
|
-
"unexpected error: {error}"
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
#[tokio::test]
|
|
1217
|
-
async fn diff_commits_rejects_stale_grandparent_row_with_forged_commit_root_parent() {
|
|
1218
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1219
|
-
let tracked_state = TrackedStateContext::new();
|
|
1220
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
1221
|
-
.await
|
|
1222
|
-
.expect("left root should write");
|
|
1223
|
-
write_root_committed_for_test(
|
|
1224
|
-
&storage,
|
|
1225
|
-
&tracked_state,
|
|
1226
|
-
"grandparent",
|
|
1227
|
-
None,
|
|
1228
|
-
&[row_with_value("entity-a", None, "grandparent-a", "old")],
|
|
1229
|
-
)
|
|
1230
|
-
.await
|
|
1231
|
-
.expect("grandparent root should write");
|
|
1232
|
-
write_root_committed_for_test(
|
|
1233
|
-
&storage,
|
|
1234
|
-
&tracked_state,
|
|
1235
|
-
"parent",
|
|
1236
|
-
Some("grandparent"),
|
|
1237
|
-
&[row_with_value("entity-a", None, "parent-a", "new")],
|
|
1238
|
-
)
|
|
1239
|
-
.await
|
|
1240
|
-
.expect("parent root should write");
|
|
1241
|
-
write_root_committed_for_test(&storage, &tracked_state, "child", Some("parent"), &[])
|
|
1242
|
-
.await
|
|
1243
|
-
.expect("child root should write");
|
|
1244
|
-
|
|
1245
|
-
let read = storage
|
|
1246
|
-
.begin_read(StorageReadOptions::default())
|
|
1247
|
-
.expect("read should open");
|
|
1248
|
-
let stale_diff = tracked_state
|
|
1249
|
-
.reader(read)
|
|
1250
|
-
.diff_commits("left", "grandparent", &TrackedStateDiffRequest::default())
|
|
1251
|
-
.await
|
|
1252
|
-
.expect("grandparent diff should load");
|
|
1253
|
-
let stale_row = stale_diff
|
|
1254
|
-
.entries
|
|
1255
|
-
.iter()
|
|
1256
|
-
.find_map(|entry| entry.after.clone())
|
|
1257
|
-
.expect("grandparent row should appear");
|
|
1258
|
-
let (stale_key, stale_value) = stale_row.into_index_entry();
|
|
1259
|
-
|
|
1260
|
-
stage_corrupt_commit_root(
|
|
1261
|
-
&storage,
|
|
1262
|
-
"child",
|
|
1263
|
-
vec![(stale_key, stale_value)],
|
|
1264
|
-
vec![TrackedStateCommitRootParent {
|
|
1265
|
-
commit_id: "grandparent".to_string(),
|
|
1266
|
-
root_id: tracked_state_root_id(&storage, "grandparent").await,
|
|
1267
|
-
}],
|
|
1268
|
-
)
|
|
1269
|
-
.await;
|
|
1270
|
-
|
|
1271
|
-
let read = storage
|
|
1272
|
-
.begin_read(StorageReadOptions::default())
|
|
1273
|
-
.expect("read should open");
|
|
1274
|
-
let error = tracked_state
|
|
1275
|
-
.reader(read)
|
|
1276
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
1277
|
-
.await
|
|
1278
|
-
.expect_err("forged grandparent parent must be rejected");
|
|
1279
|
-
|
|
1280
|
-
assert!(
|
|
1281
|
-
is_commit_root_validation_error(&error),
|
|
1282
|
-
"unexpected error: {error}"
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
#[tokio::test]
|
|
1287
|
-
async fn diff_commits_allows_rows_reachable_through_parent_commit() {
|
|
1288
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1289
|
-
let tracked_state = TrackedStateContext::new();
|
|
1290
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, &[])
|
|
1291
|
-
.await
|
|
1292
|
-
.expect("left root should write");
|
|
1293
|
-
write_root_committed_for_test(
|
|
1294
|
-
&storage,
|
|
1295
|
-
&tracked_state,
|
|
1296
|
-
"parent",
|
|
1297
|
-
None,
|
|
1298
|
-
&[row_with_value("entity-a", None, "parent-change", "value")],
|
|
1299
|
-
)
|
|
1300
|
-
.await
|
|
1301
|
-
.expect("parent root should write");
|
|
1302
|
-
write_root_committed_for_test(&storage, &tracked_state, "child", Some("parent"), &[])
|
|
1303
|
-
.await
|
|
1304
|
-
.expect("child root should write");
|
|
1305
|
-
|
|
1306
|
-
let read = storage
|
|
1307
|
-
.begin_read(StorageReadOptions::default())
|
|
1308
|
-
.expect("read should open");
|
|
1309
|
-
let diff = tracked_state
|
|
1310
|
-
.reader(read)
|
|
1311
|
-
.diff_commits("left", "child", &TrackedStateDiffRequest::default())
|
|
1312
|
-
.await
|
|
1313
|
-
.expect("ancestor-reachable row should validate");
|
|
1314
|
-
|
|
1315
|
-
assert_eq!(
|
|
1316
|
-
kinds(&diff),
|
|
1317
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
#[tokio::test]
|
|
1322
|
-
async fn diff_commits_allows_source_update_with_source_created_at() {
|
|
1323
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1324
|
-
let tracked_state = TrackedStateContext::new();
|
|
1325
|
-
write_root_committed_for_test(&storage, &tracked_state, "target", None, &[])
|
|
1326
|
-
.await
|
|
1327
|
-
.expect("target root should write");
|
|
1328
|
-
write_root_committed_for_test(
|
|
1329
|
-
&storage,
|
|
1330
|
-
&tracked_state,
|
|
1331
|
-
"source-add",
|
|
1332
|
-
None,
|
|
1333
|
-
&[row_with_times(
|
|
1334
|
-
"entity-a",
|
|
1335
|
-
None,
|
|
1336
|
-
"source-add-a",
|
|
1337
|
-
"old",
|
|
1338
|
-
"2026-01-01T00:00:00Z",
|
|
1339
|
-
"2026-01-01T00:00:00Z",
|
|
1340
|
-
)],
|
|
1341
|
-
)
|
|
1342
|
-
.await
|
|
1343
|
-
.expect("source add root should write");
|
|
1344
|
-
let source_update = row_with_times(
|
|
1345
|
-
"entity-a",
|
|
1346
|
-
None,
|
|
1347
|
-
"source-update-a",
|
|
1348
|
-
"new",
|
|
1349
|
-
"2026-01-01T00:00:00Z",
|
|
1350
|
-
"2026-01-02T00:00:00Z",
|
|
1351
|
-
);
|
|
1352
|
-
write_root_committed_for_test(
|
|
1353
|
-
&storage,
|
|
1354
|
-
&tracked_state,
|
|
1355
|
-
"source-update",
|
|
1356
|
-
Some("source-add"),
|
|
1357
|
-
std::slice::from_ref(&source_update),
|
|
1358
|
-
)
|
|
1359
|
-
.await
|
|
1360
|
-
.expect("source update root should write");
|
|
1361
|
-
{
|
|
1362
|
-
let mut read = storage
|
|
1363
|
-
.begin_read(StorageReadOptions::default())
|
|
1364
|
-
.expect("read should open");
|
|
1365
|
-
let mut writes = storage.new_write_set();
|
|
1366
|
-
crate::test_support::stage_tracked_root_from_materialized_with_parents(
|
|
1367
|
-
&mut read,
|
|
1368
|
-
&mut writes,
|
|
1369
|
-
&tracked_state,
|
|
1370
|
-
"merge",
|
|
1371
|
-
&["target".to_string(), "source-update".to_string()],
|
|
1372
|
-
Some("target"),
|
|
1373
|
-
std::slice::from_ref(&source_update),
|
|
1374
|
-
)
|
|
1375
|
-
.await
|
|
1376
|
-
.expect("merge root should stage");
|
|
1377
|
-
storage
|
|
1378
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1379
|
-
.expect("merge root should commit");
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
let read = storage
|
|
1383
|
-
.begin_read(StorageReadOptions::default())
|
|
1384
|
-
.expect("read should open");
|
|
1385
|
-
let diff = tracked_state
|
|
1386
|
-
.reader(read)
|
|
1387
|
-
.diff_commits("target", "merge", &TrackedStateDiffRequest::default())
|
|
1388
|
-
.await
|
|
1389
|
-
.expect("source update should validate");
|
|
1390
|
-
|
|
1391
|
-
assert_eq!(
|
|
1392
|
-
kinds(&diff),
|
|
1393
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
|
|
1394
|
-
);
|
|
1395
|
-
let row = diff.entries[0].after.as_ref().expect("after row");
|
|
1396
|
-
assert_eq!(row.created_at, "2026-01-01T00:00:00Z");
|
|
1397
|
-
assert_eq!(row.updated_at, "2026-01-02T00:00:00Z");
|
|
1398
|
-
assert_eq!(row.change_id, "source-update-a");
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
#[tokio::test]
|
|
1402
|
-
async fn diff_commits_rejects_omitted_inherited_row_even_when_diff_is_non_empty() {
|
|
1403
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1404
|
-
let tracked_state = TrackedStateContext::new();
|
|
1405
|
-
write_root_committed_for_test(
|
|
1406
|
-
&storage,
|
|
1407
|
-
&tracked_state,
|
|
1408
|
-
"parent",
|
|
1409
|
-
None,
|
|
1410
|
-
&[row_with_value("entity-a", None, "parent-a", "inherited")],
|
|
1411
|
-
)
|
|
1412
|
-
.await
|
|
1413
|
-
.expect("parent root should write");
|
|
1414
|
-
write_root_committed_for_test(
|
|
1415
|
-
&storage,
|
|
1416
|
-
&tracked_state,
|
|
1417
|
-
"child",
|
|
1418
|
-
Some("parent"),
|
|
1419
|
-
&[row_with_value("entity-b", None, "child-b", "unrelated")],
|
|
1420
|
-
)
|
|
1421
|
-
.await
|
|
1422
|
-
.expect("child root should write");
|
|
1423
|
-
|
|
1424
|
-
let read = storage
|
|
1425
|
-
.begin_read(StorageReadOptions::default())
|
|
1426
|
-
.expect("read should open");
|
|
1427
|
-
let valid_diff = tracked_state
|
|
1428
|
-
.reader(read)
|
|
1429
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1430
|
-
.await
|
|
1431
|
-
.expect("valid child diff should load");
|
|
1432
|
-
let unrelated_row = valid_diff
|
|
1433
|
-
.entries
|
|
1434
|
-
.iter()
|
|
1435
|
-
.find_map(|entry| {
|
|
1436
|
-
entry
|
|
1437
|
-
.after
|
|
1438
|
-
.as_ref()
|
|
1439
|
-
.filter(|row| row.change_id == "child-b")
|
|
1440
|
-
.cloned()
|
|
1441
|
-
})
|
|
1442
|
-
.expect("unrelated child row should appear");
|
|
1443
|
-
let (unrelated_key, unrelated_value) = unrelated_row.into_index_entry();
|
|
1444
|
-
stage_corrupt_commit_root(
|
|
1445
|
-
&storage,
|
|
1446
|
-
"child",
|
|
1447
|
-
vec![(unrelated_key, unrelated_value)],
|
|
1448
|
-
vec![TrackedStateCommitRootParent {
|
|
1449
|
-
commit_id: "parent".to_string(),
|
|
1450
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
1451
|
-
}],
|
|
1452
|
-
)
|
|
1453
|
-
.await;
|
|
1454
|
-
|
|
1455
|
-
let read = storage
|
|
1456
|
-
.begin_read(StorageReadOptions::default())
|
|
1457
|
-
.expect("read should open");
|
|
1458
|
-
let error = tracked_state
|
|
1459
|
-
.reader(read)
|
|
1460
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1461
|
-
.await
|
|
1462
|
-
.expect_err("omitted inherited row must be rejected");
|
|
1463
|
-
|
|
1464
|
-
assert!(
|
|
1465
|
-
is_commit_root_validation_error(&error),
|
|
1466
|
-
"unexpected error: {error}"
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
#[tokio::test]
|
|
1471
|
-
async fn diff_commits_rejects_omitted_updated_row_even_when_diff_is_non_empty() {
|
|
1472
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1473
|
-
let tracked_state = TrackedStateContext::new();
|
|
1474
|
-
write_root_committed_for_test(
|
|
1475
|
-
&storage,
|
|
1476
|
-
&tracked_state,
|
|
1477
|
-
"parent",
|
|
1478
|
-
None,
|
|
1479
|
-
&[row_with_value("entity-a", None, "parent-a", "old")],
|
|
1480
|
-
)
|
|
1481
|
-
.await
|
|
1482
|
-
.expect("parent root should write");
|
|
1483
|
-
write_root_committed_for_test(
|
|
1484
|
-
&storage,
|
|
1485
|
-
&tracked_state,
|
|
1486
|
-
"child",
|
|
1487
|
-
Some("parent"),
|
|
1488
|
-
&[
|
|
1489
|
-
row_with_value("entity-a", None, "child-a", "new"),
|
|
1490
|
-
row_with_value("entity-b", None, "child-b", "unrelated"),
|
|
1491
|
-
],
|
|
1492
|
-
)
|
|
1493
|
-
.await
|
|
1494
|
-
.expect("child root should write");
|
|
1495
|
-
|
|
1496
|
-
let read = storage
|
|
1497
|
-
.begin_read(StorageReadOptions::default())
|
|
1498
|
-
.expect("read should open");
|
|
1499
|
-
let valid_diff = tracked_state
|
|
1500
|
-
.reader(read)
|
|
1501
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1502
|
-
.await
|
|
1503
|
-
.expect("valid child diff should load");
|
|
1504
|
-
let unrelated_row = valid_diff
|
|
1505
|
-
.entries
|
|
1506
|
-
.iter()
|
|
1507
|
-
.find_map(|entry| {
|
|
1508
|
-
entry
|
|
1509
|
-
.after
|
|
1510
|
-
.as_ref()
|
|
1511
|
-
.filter(|row| row.change_id == "child-b")
|
|
1512
|
-
.cloned()
|
|
1513
|
-
})
|
|
1514
|
-
.expect("unrelated child row should appear");
|
|
1515
|
-
let (unrelated_key, unrelated_value) = unrelated_row.into_index_entry();
|
|
1516
|
-
stage_corrupt_commit_root(
|
|
1517
|
-
&storage,
|
|
1518
|
-
"child",
|
|
1519
|
-
vec![(unrelated_key, unrelated_value)],
|
|
1520
|
-
vec![TrackedStateCommitRootParent {
|
|
1521
|
-
commit_id: "parent".to_string(),
|
|
1522
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
1523
|
-
}],
|
|
1524
|
-
)
|
|
1525
|
-
.await;
|
|
1526
|
-
|
|
1527
|
-
let read = storage
|
|
1528
|
-
.begin_read(StorageReadOptions::default())
|
|
1529
|
-
.expect("read should open");
|
|
1530
|
-
let error = tracked_state
|
|
1531
|
-
.reader(read)
|
|
1532
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1533
|
-
.await
|
|
1534
|
-
.expect_err("omitted updated row must be rejected");
|
|
1535
|
-
|
|
1536
|
-
assert!(
|
|
1537
|
-
is_commit_root_validation_error(&error),
|
|
1538
|
-
"unexpected error: {error}"
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
#[tokio::test]
|
|
1543
|
-
async fn diff_commits_rejects_shared_omitted_row_even_when_diff_is_non_empty() {
|
|
1544
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1545
|
-
let tracked_state = TrackedStateContext::new();
|
|
1546
|
-
write_root_committed_for_test(
|
|
1547
|
-
&storage,
|
|
1548
|
-
&tracked_state,
|
|
1549
|
-
"parent",
|
|
1550
|
-
None,
|
|
1551
|
-
&[row_with_value("entity-a", None, "parent-a", "shared")],
|
|
1552
|
-
)
|
|
1553
|
-
.await
|
|
1554
|
-
.expect("parent root should write");
|
|
1555
|
-
write_root_committed_for_test(
|
|
1556
|
-
&storage,
|
|
1557
|
-
&tracked_state,
|
|
1558
|
-
"left",
|
|
1559
|
-
Some("parent"),
|
|
1560
|
-
&[row_with_value("entity-b", None, "left-b", "left")],
|
|
1561
|
-
)
|
|
1562
|
-
.await
|
|
1563
|
-
.expect("left root should write");
|
|
1564
|
-
write_root_committed_for_test(
|
|
1565
|
-
&storage,
|
|
1566
|
-
&tracked_state,
|
|
1567
|
-
"right",
|
|
1568
|
-
Some("parent"),
|
|
1569
|
-
&[row_with_value("entity-c", None, "right-c", "right")],
|
|
1570
|
-
)
|
|
1571
|
-
.await
|
|
1572
|
-
.expect("right root should write");
|
|
1573
|
-
|
|
1574
|
-
let read = storage
|
|
1575
|
-
.begin_read(StorageReadOptions::default())
|
|
1576
|
-
.expect("read should open");
|
|
1577
|
-
let left_diff = tracked_state
|
|
1578
|
-
.reader(read)
|
|
1579
|
-
.diff_commits("parent", "left", &TrackedStateDiffRequest::default())
|
|
1580
|
-
.await
|
|
1581
|
-
.expect("left diff should load");
|
|
1582
|
-
let left_row = left_diff
|
|
1583
|
-
.entries
|
|
1584
|
-
.iter()
|
|
1585
|
-
.find_map(|entry| {
|
|
1586
|
-
entry
|
|
1587
|
-
.after
|
|
1588
|
-
.as_ref()
|
|
1589
|
-
.filter(|row| row.change_id == "left-b")
|
|
1590
|
-
.cloned()
|
|
1591
|
-
})
|
|
1592
|
-
.expect("left row should appear");
|
|
1593
|
-
let (left_key, left_value) = left_row.into_index_entry();
|
|
1594
|
-
let read = storage
|
|
1595
|
-
.begin_read(StorageReadOptions::default())
|
|
1596
|
-
.expect("read should open");
|
|
1597
|
-
let right_diff = tracked_state
|
|
1598
|
-
.reader(read)
|
|
1599
|
-
.diff_commits("parent", "right", &TrackedStateDiffRequest::default())
|
|
1600
|
-
.await
|
|
1601
|
-
.expect("right diff should load");
|
|
1602
|
-
let right_row = right_diff
|
|
1603
|
-
.entries
|
|
1604
|
-
.iter()
|
|
1605
|
-
.find_map(|entry| {
|
|
1606
|
-
entry
|
|
1607
|
-
.after
|
|
1608
|
-
.as_ref()
|
|
1609
|
-
.filter(|row| row.change_id == "right-c")
|
|
1610
|
-
.cloned()
|
|
1611
|
-
})
|
|
1612
|
-
.expect("right row should appear");
|
|
1613
|
-
let (right_key, right_value) = right_row.into_index_entry();
|
|
1614
|
-
stage_corrupt_commit_root(
|
|
1615
|
-
&storage,
|
|
1616
|
-
"left",
|
|
1617
|
-
vec![(left_key, left_value)],
|
|
1618
|
-
vec![TrackedStateCommitRootParent {
|
|
1619
|
-
commit_id: "parent".to_string(),
|
|
1620
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
1621
|
-
}],
|
|
1622
|
-
)
|
|
1623
|
-
.await;
|
|
1624
|
-
stage_corrupt_commit_root(
|
|
1625
|
-
&storage,
|
|
1626
|
-
"right",
|
|
1627
|
-
vec![(right_key, right_value)],
|
|
1628
|
-
vec![TrackedStateCommitRootParent {
|
|
1629
|
-
commit_id: "parent".to_string(),
|
|
1630
|
-
root_id: tracked_state_root_id(&storage, "parent").await,
|
|
1631
|
-
}],
|
|
1632
|
-
)
|
|
1633
|
-
.await;
|
|
1634
|
-
|
|
1635
|
-
let read = storage
|
|
1636
|
-
.begin_read(StorageReadOptions::default())
|
|
1637
|
-
.expect("read should open");
|
|
1638
|
-
let error = tracked_state
|
|
1639
|
-
.reader(read)
|
|
1640
|
-
.diff_commits("left", "right", &TrackedStateDiffRequest::default())
|
|
1641
|
-
.await
|
|
1642
|
-
.expect_err("shared hidden omission must be rejected");
|
|
1643
|
-
|
|
1644
|
-
assert!(
|
|
1645
|
-
is_commit_root_validation_error(&error),
|
|
1646
|
-
"unexpected error: {error}"
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
#[tokio::test]
|
|
1651
|
-
async fn diff_commits_validates_roots_even_when_tree_diff_is_empty() {
|
|
1652
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1653
|
-
let tracked_state = TrackedStateContext::new();
|
|
1654
|
-
write_root_committed_for_test(
|
|
1655
|
-
&storage,
|
|
1656
|
-
&tracked_state,
|
|
1657
|
-
"source",
|
|
1658
|
-
None,
|
|
1659
|
-
&[row_with_value("entity-a", None, "source-change", "value")],
|
|
1660
|
-
)
|
|
1661
|
-
.await
|
|
1662
|
-
.expect("source root should write");
|
|
1663
|
-
write_root_committed_for_test(&storage, &tracked_state, "left-corrupt", None, &[])
|
|
1664
|
-
.await
|
|
1665
|
-
.expect("left changelog should write");
|
|
1666
|
-
write_root_committed_for_test(&storage, &tracked_state, "right-corrupt", None, &[])
|
|
1667
|
-
.await
|
|
1668
|
-
.expect("right changelog should write");
|
|
1669
|
-
|
|
1670
|
-
let read = storage
|
|
1671
|
-
.begin_read(StorageReadOptions::default())
|
|
1672
|
-
.expect("read should open");
|
|
1673
|
-
let source_diff = tracked_state
|
|
1674
|
-
.reader(read)
|
|
1675
|
-
.diff_commits(
|
|
1676
|
-
"left-corrupt",
|
|
1677
|
-
"source",
|
|
1678
|
-
&TrackedStateDiffRequest::default(),
|
|
1679
|
-
)
|
|
1680
|
-
.await
|
|
1681
|
-
.expect("source diff should load");
|
|
1682
|
-
let source_row = source_diff
|
|
1683
|
-
.entries
|
|
1684
|
-
.iter()
|
|
1685
|
-
.find_map(|entry| entry.after.clone())
|
|
1686
|
-
.expect("source row should appear");
|
|
1687
|
-
let (source_key, source_value) = source_row.into_index_entry();
|
|
1688
|
-
|
|
1689
|
-
stage_corrupt_commit_root(
|
|
1690
|
-
&storage,
|
|
1691
|
-
"left-corrupt",
|
|
1692
|
-
vec![(source_key.clone(), source_value.clone())],
|
|
1693
|
-
Vec::new(),
|
|
1694
|
-
)
|
|
1695
|
-
.await;
|
|
1696
|
-
stage_corrupt_commit_root(
|
|
1697
|
-
&storage,
|
|
1698
|
-
"right-corrupt",
|
|
1699
|
-
vec![(source_key, source_value)],
|
|
1700
|
-
Vec::new(),
|
|
1701
|
-
)
|
|
1702
|
-
.await;
|
|
1703
|
-
|
|
1704
|
-
let read = storage
|
|
1705
|
-
.begin_read(StorageReadOptions::default())
|
|
1706
|
-
.expect("read should open");
|
|
1707
|
-
let error = tracked_state
|
|
1708
|
-
.reader(read)
|
|
1709
|
-
.diff_commits(
|
|
1710
|
-
"left-corrupt",
|
|
1711
|
-
"right-corrupt",
|
|
1712
|
-
&TrackedStateDiffRequest::default(),
|
|
1713
|
-
)
|
|
1714
|
-
.await
|
|
1715
|
-
.expect_err("identical corrupt roots must still be validated");
|
|
1716
|
-
|
|
1717
|
-
assert!(
|
|
1718
|
-
is_commit_root_validation_error(&error),
|
|
1719
|
-
"unexpected error: {error}"
|
|
1720
|
-
);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
#[tokio::test]
|
|
1724
|
-
async fn diff_commits_between_delta_parent_and_child_reports_suffix_rows() {
|
|
1725
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1726
|
-
let tracked_state = TrackedStateContext::new();
|
|
1727
|
-
let mut read = storage
|
|
1728
|
-
.begin_read(StorageReadOptions::default())
|
|
1729
|
-
.expect("read should open");
|
|
1730
|
-
let mut writes = storage.new_write_set();
|
|
1731
|
-
write_root_for_test(
|
|
1732
|
-
&mut read,
|
|
1733
|
-
&mut writes,
|
|
1734
|
-
&tracked_state,
|
|
1735
|
-
"parent",
|
|
1736
|
-
None,
|
|
1737
|
-
&[
|
|
1738
|
-
row_with_value("entity-a", None, "parent-a", "before"),
|
|
1739
|
-
row_with_value("entity-b", None, "parent-b", "same"),
|
|
1740
|
-
],
|
|
1741
|
-
)
|
|
1742
|
-
.await
|
|
1743
|
-
.expect("parent should write");
|
|
1744
|
-
storage
|
|
1745
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1746
|
-
.expect("parent writes should commit");
|
|
1747
|
-
let mut read = storage
|
|
1748
|
-
.begin_read(StorageReadOptions::default())
|
|
1749
|
-
.expect("child read should open");
|
|
1750
|
-
let mut writes = storage.new_write_set();
|
|
1751
|
-
write_root_for_test(
|
|
1752
|
-
&mut read,
|
|
1753
|
-
&mut writes,
|
|
1754
|
-
&tracked_state,
|
|
1755
|
-
"child",
|
|
1756
|
-
Some("parent"),
|
|
1757
|
-
&[row_with_value("entity-a", None, "child-a", "after")],
|
|
1758
|
-
)
|
|
1759
|
-
.await
|
|
1760
|
-
.expect("child should write");
|
|
1761
|
-
storage
|
|
1762
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1763
|
-
.expect("writes should commit");
|
|
1764
|
-
|
|
1765
|
-
let read = storage
|
|
1766
|
-
.begin_read(StorageReadOptions::default())
|
|
1767
|
-
.expect("read should open");
|
|
1768
|
-
let diff = tracked_state
|
|
1769
|
-
.reader(read)
|
|
1770
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1771
|
-
.await
|
|
1772
|
-
.expect("diff should load");
|
|
1773
|
-
|
|
1774
|
-
assert_eq!(
|
|
1775
|
-
kinds(&diff),
|
|
1776
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
|
|
1777
|
-
);
|
|
1778
|
-
assert_ne!(
|
|
1779
|
-
diff.entries[0]
|
|
1780
|
-
.before
|
|
1781
|
-
.as_ref()
|
|
1782
|
-
.and_then(|row| row.snapshot_ref.as_ref()),
|
|
1783
|
-
diff.entries[0]
|
|
1784
|
-
.after
|
|
1785
|
-
.as_ref()
|
|
1786
|
-
.and_then(|row| row.snapshot_ref.as_ref())
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
#[tokio::test]
|
|
1791
|
-
async fn diff_commits_between_delta_child_and_parent_reports_reverse_suffix_rows() {
|
|
1792
|
-
let (storage, tracked_state) = seed_parent_child_delta(
|
|
1793
|
-
&[
|
|
1794
|
-
row_with_value("entity-a", None, "parent-a", "before"),
|
|
1795
|
-
row_with_value("entity-b", None, "parent-b", "same"),
|
|
1796
|
-
],
|
|
1797
|
-
&[row_with_value("entity-a", None, "child-a", "after")],
|
|
1798
|
-
)
|
|
1799
|
-
.await;
|
|
1800
|
-
|
|
1801
|
-
let read = storage
|
|
1802
|
-
.begin_read(StorageReadOptions::default())
|
|
1803
|
-
.expect("read should open");
|
|
1804
|
-
let diff = tracked_state
|
|
1805
|
-
.reader(read)
|
|
1806
|
-
.diff_commits("child", "parent", &TrackedStateDiffRequest::default())
|
|
1807
|
-
.await
|
|
1808
|
-
.expect("diff should load");
|
|
1809
|
-
|
|
1810
|
-
assert_eq!(
|
|
1811
|
-
kinds(&diff),
|
|
1812
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
|
|
1813
|
-
);
|
|
1814
|
-
assert_ne!(
|
|
1815
|
-
diff.entries[0]
|
|
1816
|
-
.before
|
|
1817
|
-
.as_ref()
|
|
1818
|
-
.and_then(|row| row.snapshot_ref.as_ref()),
|
|
1819
|
-
diff.entries[0]
|
|
1820
|
-
.after
|
|
1821
|
-
.as_ref()
|
|
1822
|
-
.and_then(|row| row.snapshot_ref.as_ref())
|
|
1823
|
-
);
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
#[tokio::test]
|
|
1827
|
-
async fn diff_commits_between_delta_parent_and_child_preserves_suffix_tombstones() {
|
|
1828
|
-
let (storage, tracked_state) = seed_parent_child_delta(
|
|
1829
|
-
&[
|
|
1830
|
-
row_with_value("entity-a", None, "parent-a", "before"),
|
|
1831
|
-
row_with_value("entity-b", None, "parent-b", "same"),
|
|
1832
|
-
],
|
|
1833
|
-
&[tombstone("entity-a", None, "child-delete")],
|
|
1834
|
-
)
|
|
1835
|
-
.await;
|
|
1836
|
-
|
|
1837
|
-
let read = storage
|
|
1838
|
-
.begin_read(StorageReadOptions::default())
|
|
1839
|
-
.expect("read should open");
|
|
1840
|
-
let diff = tracked_state
|
|
1841
|
-
.reader(read)
|
|
1842
|
-
.diff_commits("parent", "child", &TrackedStateDiffRequest::default())
|
|
1843
|
-
.await
|
|
1844
|
-
.expect("diff should load");
|
|
1845
|
-
|
|
1846
|
-
assert_eq!(
|
|
1847
|
-
kinds(&diff),
|
|
1848
|
-
vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
|
|
1849
|
-
);
|
|
1850
|
-
assert!(diff.entries[0].before_is_live());
|
|
1851
|
-
assert!(!diff.entries[0].after_is_live());
|
|
1852
|
-
assert_eq!(
|
|
1853
|
-
diff.entries[0]
|
|
1854
|
-
.after
|
|
1855
|
-
.as_ref()
|
|
1856
|
-
.map(|row| row.change_id.as_str()),
|
|
1857
|
-
Some("child-delete")
|
|
1858
|
-
);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
async fn diff(
|
|
1862
|
-
storage: &StorageContext,
|
|
1863
|
-
tracked_state: &TrackedStateContext,
|
|
1864
|
-
) -> TrackedStateDiff {
|
|
1865
|
-
let read = storage
|
|
1866
|
-
.begin_read(StorageReadOptions::default())
|
|
1867
|
-
.expect("read should open");
|
|
1868
|
-
tracked_state
|
|
1869
|
-
.reader(read)
|
|
1870
|
-
.diff_commits("left", "right", &TrackedStateDiffRequest::default())
|
|
1871
|
-
.await
|
|
1872
|
-
.expect("diff should load")
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
async fn seed_roots(
|
|
1876
|
-
left_rows: &[MaterializedTrackedStateRow],
|
|
1877
|
-
right_rows: &[MaterializedTrackedStateRow],
|
|
1878
|
-
) -> (StorageContext, TrackedStateContext) {
|
|
1879
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1880
|
-
let tracked_state = TrackedStateContext::new();
|
|
1881
|
-
write_root_committed_for_test(&storage, &tracked_state, "left", None, left_rows)
|
|
1882
|
-
.await
|
|
1883
|
-
.expect("left root should write");
|
|
1884
|
-
write_root_committed_for_test(&storage, &tracked_state, "right", None, right_rows)
|
|
1885
|
-
.await
|
|
1886
|
-
.expect("right root should write");
|
|
1887
|
-
(storage, tracked_state)
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
async fn seed_parent_child_delta(
|
|
1891
|
-
parent_rows: &[MaterializedTrackedStateRow],
|
|
1892
|
-
child_rows: &[MaterializedTrackedStateRow],
|
|
1893
|
-
) -> (StorageContext, TrackedStateContext) {
|
|
1894
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1895
|
-
let tracked_state = TrackedStateContext::new();
|
|
1896
|
-
write_root_committed_for_test(&storage, &tracked_state, "parent", None, parent_rows)
|
|
1897
|
-
.await
|
|
1898
|
-
.expect("parent should write");
|
|
1899
|
-
write_root_committed_for_test(
|
|
1900
|
-
&storage,
|
|
1901
|
-
&tracked_state,
|
|
1902
|
-
"child",
|
|
1903
|
-
Some("parent"),
|
|
1904
|
-
child_rows,
|
|
1905
|
-
)
|
|
1906
|
-
.await
|
|
1907
|
-
.expect("child should write");
|
|
1908
|
-
(storage, tracked_state)
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
async fn write_root_committed_for_test(
|
|
1912
|
-
storage: &StorageContext,
|
|
1913
|
-
tracked_state: &TrackedStateContext,
|
|
1914
|
-
commit_id: &str,
|
|
1915
|
-
parent_commit_id: Option<&str>,
|
|
1916
|
-
rows: &[MaterializedTrackedStateRow],
|
|
1917
|
-
) -> Result<(), LixError> {
|
|
1918
|
-
let mut read = storage
|
|
1919
|
-
.begin_read(StorageReadOptions::default())
|
|
1920
|
-
.expect("read should open");
|
|
1921
|
-
let mut writes = storage.new_write_set();
|
|
1922
|
-
write_root_for_test(
|
|
1923
|
-
&mut read,
|
|
1924
|
-
&mut writes,
|
|
1925
|
-
tracked_state,
|
|
1926
|
-
commit_id,
|
|
1927
|
-
parent_commit_id,
|
|
1928
|
-
rows,
|
|
1929
|
-
)
|
|
1930
|
-
.await?;
|
|
1931
|
-
storage.commit_write_set(writes, StorageWriteOptions::default())?;
|
|
1932
|
-
Ok(())
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
async fn write_root_for_test(
|
|
1936
|
-
read: &mut (impl crate::storage::StorageRead + Send + Sync + ?Sized),
|
|
1937
|
-
writes: &mut crate::storage::StorageWriteSet,
|
|
1938
|
-
tracked_state: &TrackedStateContext,
|
|
1939
|
-
commit_id: &str,
|
|
1940
|
-
parent_commit_id: Option<&str>,
|
|
1941
|
-
rows: &[MaterializedTrackedStateRow],
|
|
1942
|
-
) -> Result<(), LixError> {
|
|
1943
|
-
crate::test_support::stage_tracked_root_from_materialized(
|
|
1944
|
-
read,
|
|
1945
|
-
writes,
|
|
1946
|
-
tracked_state,
|
|
1947
|
-
commit_id,
|
|
1948
|
-
parent_commit_id,
|
|
1949
|
-
rows,
|
|
1950
|
-
)
|
|
1951
|
-
.await
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
async fn tracked_state_root_id(
|
|
1955
|
-
storage: &StorageContext,
|
|
1956
|
-
commit_id: &str,
|
|
1957
|
-
) -> TrackedStateRootId {
|
|
1958
|
-
let read = storage
|
|
1959
|
-
.begin_read(StorageReadOptions::default())
|
|
1960
|
-
.expect("read should open");
|
|
1961
|
-
crate::tracked_state::storage::load_root(&read, commit_id)
|
|
1962
|
-
.await
|
|
1963
|
-
.expect("root should load")
|
|
1964
|
-
.expect("root should exist")
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
async fn stage_corrupt_commit_root(
|
|
1968
|
-
storage: &StorageContext,
|
|
1969
|
-
commit_id: &str,
|
|
1970
|
-
entries: Vec<(TrackedStateKey, TrackedStateIndexValue)>,
|
|
1971
|
-
parent_roots: Vec<TrackedStateCommitRootParent>,
|
|
1972
|
-
) {
|
|
1973
|
-
let read = storage
|
|
1974
|
-
.begin_read(StorageReadOptions::default())
|
|
1975
|
-
.expect("read should open");
|
|
1976
|
-
let mut writes = storage.new_write_set();
|
|
1977
|
-
let mutations = entries
|
|
1978
|
-
.into_iter()
|
|
1979
|
-
.map(|(key, value)| {
|
|
1980
|
-
TrackedStateMutation::put_encoded(
|
|
1981
|
-
crate::tracked_state::codec::encode_key(&key),
|
|
1982
|
-
crate::tracked_state::codec::encode_value(&value),
|
|
1983
|
-
)
|
|
1984
|
-
})
|
|
1985
|
-
.collect::<Vec<_>>();
|
|
1986
|
-
let changed_key_count = mutations.len() as u64;
|
|
1987
|
-
let result = crate::tracked_state::tree::TrackedStateTree::new()
|
|
1988
|
-
.apply_mutations(&read, &mut writes, None, mutations, Some(commit_id))
|
|
1989
|
-
.await
|
|
1990
|
-
.expect("corrupt root should write");
|
|
1991
|
-
crate::tracked_state::storage::stage_commit_root(
|
|
1992
|
-
&mut writes,
|
|
1993
|
-
&TrackedStateCommitRoot {
|
|
1994
|
-
commit_id: commit_id.to_string(),
|
|
1995
|
-
root_id: result.root_id,
|
|
1996
|
-
parent_roots,
|
|
1997
|
-
changed_key_count,
|
|
1998
|
-
row_count_estimate: result.row_count as u64,
|
|
1999
|
-
tree_height: result.tree_height as u32,
|
|
2000
|
-
primary_chunk_count: result.chunk_count as u64,
|
|
2001
|
-
primary_chunk_bytes: result.chunk_bytes as u64,
|
|
2002
|
-
},
|
|
2003
|
-
)
|
|
2004
|
-
.expect("metadata should encode");
|
|
2005
|
-
storage
|
|
2006
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2007
|
-
.expect("corrupt root should commit");
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
fn kinds(diff: &TrackedStateDiff) -> Vec<(String, TrackedStateDiffKind)> {
|
|
2011
|
-
diff.entries
|
|
2012
|
-
.iter()
|
|
2013
|
-
.map(|entry| {
|
|
2014
|
-
(
|
|
2015
|
-
entry
|
|
2016
|
-
.identity
|
|
2017
|
-
.entity_pk
|
|
2018
|
-
.as_single_string_owned()
|
|
2019
|
-
.expect("identity"),
|
|
2020
|
-
entry.kind,
|
|
2021
|
-
)
|
|
2022
|
-
})
|
|
2023
|
-
.collect()
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
fn is_commit_root_validation_error(error: &LixError) -> bool {
|
|
2027
|
-
error.message.contains("not the first-parent winner")
|
|
2028
|
-
|| error.message.contains("does not match parent root")
|
|
2029
|
-
|| error
|
|
2030
|
-
.message
|
|
2031
|
-
.contains("does not match changelog first-parent winners")
|
|
2032
|
-
|| error.message.contains("contains non-winner identity")
|
|
2033
|
-
|| error.message.contains("but changelog first parent is")
|
|
2034
|
-
|| error
|
|
2035
|
-
.message
|
|
2036
|
-
.contains("nearest available first-parent root")
|
|
2037
|
-
|| error.message.contains("references unexpected parent")
|
|
2038
|
-
|| error.message.contains("missing changelog winner")
|
|
2039
|
-
|| error.message.contains("has change")
|
|
2040
|
-
|| error.message.contains("omits current changelog change")
|
|
2041
|
-
|| error.message.contains("omits inherited identity")
|
|
2042
|
-
|| error
|
|
2043
|
-
.message
|
|
2044
|
-
.contains("does not preserve inherited identity")
|
|
2045
|
-
|| error.message.contains("but changelog winner is")
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
fn commit_root_row_entry(
|
|
2049
|
-
commit_id: &str,
|
|
2050
|
-
change_id: &str,
|
|
2051
|
-
created_at: &str,
|
|
2052
|
-
) -> (TrackedStateKey, TrackedStateIndexValue) {
|
|
2053
|
-
(
|
|
2054
|
-
TrackedStateKey {
|
|
2055
|
-
schema_key: "lix_commit".to_string(),
|
|
2056
|
-
file_id: None,
|
|
2057
|
-
entity_pk: EntityPk::single(commit_id),
|
|
2058
|
-
},
|
|
2059
|
-
TrackedStateIndexValue {
|
|
2060
|
-
change_id: change_id.to_string(),
|
|
2061
|
-
commit_id: commit_id.to_string(),
|
|
2062
|
-
deleted: false,
|
|
2063
|
-
snapshot_ref: Some(JsonRef::for_content(
|
|
2064
|
-
format!("{{\"id\":\"{commit_id}\"}}").as_bytes(),
|
|
2065
|
-
)),
|
|
2066
|
-
metadata_ref: None,
|
|
2067
|
-
created_at: created_at.to_string(),
|
|
2068
|
-
updated_at: created_at.to_string(),
|
|
2069
|
-
},
|
|
2070
|
-
)
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
fn tombstone(
|
|
2074
|
-
entity_pk: &str,
|
|
2075
|
-
file_id: Option<&str>,
|
|
2076
|
-
change_id: &str,
|
|
2077
|
-
) -> MaterializedTrackedStateRow {
|
|
2078
|
-
let mut row = row(entity_pk, file_id, change_id);
|
|
2079
|
-
row.snapshot_content = None;
|
|
2080
|
-
row.deleted = true;
|
|
2081
|
-
row
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
fn row(entity_pk: &str, file_id: Option<&str>, change_id: &str) -> MaterializedTrackedStateRow {
|
|
2085
|
-
row_with_schema(entity_pk, file_id, "test_schema", change_id)
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
fn row_with_schema(
|
|
2089
|
-
entity_pk: &str,
|
|
2090
|
-
file_id: Option<&str>,
|
|
2091
|
-
schema_key: &str,
|
|
2092
|
-
change_id: &str,
|
|
2093
|
-
) -> MaterializedTrackedStateRow {
|
|
2094
|
-
row_with_schema_and_value(entity_pk, file_id, schema_key, change_id, "value")
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
fn row_with_value(
|
|
2098
|
-
entity_pk: &str,
|
|
2099
|
-
file_id: Option<&str>,
|
|
2100
|
-
change_id: &str,
|
|
2101
|
-
value: &str,
|
|
2102
|
-
) -> MaterializedTrackedStateRow {
|
|
2103
|
-
row_with_schema_and_value(entity_pk, file_id, "test_schema", change_id, value)
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
fn row_with_times(
|
|
2107
|
-
entity_pk: &str,
|
|
2108
|
-
file_id: Option<&str>,
|
|
2109
|
-
change_id: &str,
|
|
2110
|
-
value: &str,
|
|
2111
|
-
created_at: &str,
|
|
2112
|
-
updated_at: &str,
|
|
2113
|
-
) -> MaterializedTrackedStateRow {
|
|
2114
|
-
let mut row = row_with_value(entity_pk, file_id, change_id, value);
|
|
2115
|
-
row.created_at = created_at.to_string();
|
|
2116
|
-
row.updated_at = updated_at.to_string();
|
|
2117
|
-
row
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
fn row_with_schema_and_value(
|
|
2121
|
-
entity_pk: &str,
|
|
2122
|
-
file_id: Option<&str>,
|
|
2123
|
-
schema_key: &str,
|
|
2124
|
-
change_id: &str,
|
|
2125
|
-
value: &str,
|
|
2126
|
-
) -> MaterializedTrackedStateRow {
|
|
2127
|
-
MaterializedTrackedStateRow {
|
|
2128
|
-
entity_pk: EntityPk::single(entity_pk),
|
|
2129
|
-
schema_key: schema_key.to_string(),
|
|
2130
|
-
file_id: file_id.map(str::to_string),
|
|
2131
|
-
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
2132
|
-
metadata: None,
|
|
2133
|
-
deleted: false,
|
|
2134
|
-
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2135
|
-
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2136
|
-
change_id: change_id.to_string(),
|
|
2137
|
-
commit_id: change_id.replace("change", "commit"),
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|