@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKILL.md +65 -64
- package/dist/engine-wasm/index.js +4 -4
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
- package/dist/engine-wasm/wasm/lix_engine.js +130 -118
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
- package/dist/generated/builtin-schemas.d.ts +69 -69
- package/dist/generated/builtin-schemas.js +94 -94
- package/dist/open-lix.d.ts +33 -26
- package/dist/open-lix.js +10 -10
- package/dist/sqlite/index.js +86 -30
- package/dist-engine-src/README.md +3 -3
- package/dist-engine-src/src/backend/capabilities.rs +67 -0
- package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
- package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
- package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
- package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
- package/dist-engine-src/src/backend/conformance/model.rs +28 -0
- package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
- package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
- package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
- package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
- package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
- package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
- package/dist-engine-src/src/backend/conformance/write.rs +16 -0
- package/dist-engine-src/src/backend/error.rs +94 -0
- package/dist-engine-src/src/backend/in_memory.rs +670 -0
- package/dist-engine-src/src/backend/mod.rs +36 -9
- package/dist-engine-src/src/backend/predicate.rs +80 -0
- package/dist-engine-src/src/backend/traits.rs +260 -0
- package/dist-engine-src/src/backend/types.rs +224 -81
- package/dist-engine-src/src/binary_cas/context.rs +8 -8
- package/dist-engine-src/src/binary_cas/kv.rs +234 -259
- package/dist-engine-src/src/{version → branch}/context.rs +12 -12
- package/dist-engine-src/src/branch/lifecycle.rs +221 -0
- package/dist-engine-src/src/branch/mod.rs +13 -0
- package/dist-engine-src/src/branch/refs.rs +321 -0
- package/dist-engine-src/src/branch/stage_rows.rs +67 -0
- package/dist-engine-src/src/branch/types.rs +21 -0
- package/dist-engine-src/src/catalog/context.rs +18 -18
- package/dist-engine-src/src/catalog/snapshot.rs +8 -8
- package/dist-engine-src/src/changelog/bench_support.rs +785 -0
- package/dist-engine-src/src/changelog/change.rs +1 -0
- package/dist-engine-src/src/changelog/codec.rs +497 -0
- package/dist-engine-src/src/changelog/commit.rs +1 -0
- package/dist-engine-src/src/changelog/context.rs +1614 -0
- package/dist-engine-src/src/changelog/mod.rs +29 -0
- package/dist-engine-src/src/changelog/store.rs +163 -0
- package/dist-engine-src/src/changelog/test_support.rs +54 -0
- package/dist-engine-src/src/changelog/types.rs +213 -0
- package/dist-engine-src/src/commit_graph/context.rs +317 -274
- package/dist-engine-src/src/commit_graph/mod.rs +2 -4
- package/dist-engine-src/src/commit_graph/types.rs +22 -42
- package/dist-engine-src/src/commit_graph/walker.rs +133 -103
- package/dist-engine-src/src/common/error.rs +52 -18
- package/dist-engine-src/src/common/identity.rs +2 -2
- package/dist-engine-src/src/common/mod.rs +1 -1
- package/dist-engine-src/src/domain.rs +42 -46
- package/dist-engine-src/src/engine.rs +74 -96
- package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
- package/dist-engine-src/src/functions/context.rs +56 -52
- package/dist-engine-src/src/functions/state.rs +51 -52
- package/dist-engine-src/src/init.rs +288 -154
- package/dist-engine-src/src/json_store/context.rs +15 -266
- package/dist-engine-src/src/json_store/mod.rs +26 -0
- package/dist-engine-src/src/json_store/store.rs +103 -718
- package/dist-engine-src/src/json_store/types.rs +4 -9
- package/dist-engine-src/src/lib.rs +49 -19
- package/dist-engine-src/src/live_state/context.rs +654 -790
- package/dist-engine-src/src/live_state/mod.rs +9 -3
- package/dist-engine-src/src/live_state/overlay.rs +4 -4
- package/dist-engine-src/src/live_state/types.rs +30 -21
- package/dist-engine-src/src/live_state/visibility.rs +514 -71
- package/dist-engine-src/src/plugin/install.rs +48 -48
- package/dist-engine-src/src/plugin/manifest.rs +7 -7
- package/dist-engine-src/src/plugin/materializer.rs +0 -275
- package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
- package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
- package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
- package/dist-engine-src/src/schema/compatibility.rs +11 -11
- package/dist-engine-src/src/schema/definition.json +2 -2
- package/dist-engine-src/src/schema/definition.rs +5 -5
- package/dist-engine-src/src/schema/key.rs +3 -3
- package/dist-engine-src/src/schema/mod.rs +1 -1
- package/dist-engine-src/src/schema/tests.rs +18 -18
- package/dist-engine-src/src/session/context.rs +803 -148
- package/dist-engine-src/src/session/create_branch.rs +94 -0
- package/dist-engine-src/src/session/execute.rs +223 -83
- package/dist-engine-src/src/session/merge/analysis.rs +9 -3
- package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
- package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
- package/dist-engine-src/src/session/merge/mod.rs +5 -6
- package/dist-engine-src/src/session/merge/stats.rs +7 -11
- package/dist-engine-src/src/session/mod.rs +15 -12
- package/dist-engine-src/src/session/switch_branch.rs +113 -0
- package/dist-engine-src/src/session/transaction.rs +495 -14
- package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
- package/dist-engine-src/src/sql2/bind/error.rs +5 -0
- package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
- package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
- package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
- package/dist-engine-src/src/sql2/bind/read.rs +65 -0
- package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
- package/dist-engine-src/src/sql2/bind/table.rs +273 -0
- package/dist-engine-src/src/sql2/bind/write.rs +86 -0
- package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
- package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
- package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
- package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
- package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
- package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
- package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
- package/dist-engine-src/src/sql2/context.rs +36 -30
- package/dist-engine-src/src/sql2/error.rs +1 -1
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
- package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
- package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
- package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
- package/dist-engine-src/src/sql2/exec/write.rs +661 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
- package/dist-engine-src/src/sql2/history_projection.rs +8 -8
- package/dist-engine-src/src/sql2/history_route.rs +35 -31
- package/dist-engine-src/src/sql2/mod.rs +28 -23
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
- package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
- package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
- package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
- package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
- package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
- package/dist-engine-src/src/sql2/plan/write.rs +147 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
- package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
- package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
- package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
- package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
- package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
- package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
- package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
- package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
- package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
- package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
- package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
- package/dist-engine-src/src/sql2/read_only.rs +2 -2
- package/dist-engine-src/src/sql2/session.rs +47 -96
- package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
- package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
- package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
- package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
- package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
- package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
- package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
- package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
- package/dist-engine-src/src/storage/conformance.rs +399 -0
- package/dist-engine-src/src/storage/context.rs +552 -288
- package/dist-engine-src/src/storage/mod.rs +48 -10
- package/dist-engine-src/src/storage/point.rs +440 -0
- package/dist-engine-src/src/storage/read_scope.rs +43 -64
- package/dist-engine-src/src/storage/reader.rs +867 -0
- package/dist-engine-src/src/storage/scan.rs +784 -0
- package/dist-engine-src/src/storage/spaces.rs +236 -0
- package/dist-engine-src/src/storage/stats.rs +80 -0
- package/dist-engine-src/src/storage/write_set.rs +962 -0
- package/dist-engine-src/src/storage_bench.rs +136 -4828
- package/dist-engine-src/src/test_support.rs +360 -138
- package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
- package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
- package/dist-engine-src/src/tracked_state/context.rs +1927 -993
- package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
- package/dist-engine-src/src/tracked_state/merge.rs +74 -88
- package/dist-engine-src/src/tracked_state/mod.rs +19 -16
- package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
- package/dist-engine-src/src/tracked_state/storage.rs +243 -191
- package/dist-engine-src/src/tracked_state/tree.rs +247 -371
- package/dist-engine-src/src/tracked_state/types.rs +49 -42
- package/dist-engine-src/src/transaction/bench_support.rs +407 -0
- package/dist-engine-src/src/transaction/commit.rs +821 -713
- package/dist-engine-src/src/transaction/context.rs +705 -600
- package/dist-engine-src/src/transaction/mod.rs +13 -2
- package/dist-engine-src/src/transaction/normalization.rs +63 -76
- package/dist-engine-src/src/transaction/prep.rs +13 -13
- package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
- package/dist-engine-src/src/transaction/staging.rs +228 -434
- package/dist-engine-src/src/transaction/types.rs +41 -98
- package/dist-engine-src/src/transaction/validation.rs +382 -446
- package/dist-engine-src/src/untracked_state/codec.rs +337 -29
- package/dist-engine-src/src/untracked_state/context.rs +7 -7
- package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
- package/dist-engine-src/src/untracked_state/mod.rs +1 -1
- package/dist-engine-src/src/untracked_state/storage.rs +659 -157
- package/dist-engine-src/src/untracked_state/types.rs +21 -21
- package/package.json +71 -68
- package/dist-engine-src/src/backend/kv.rs +0 -358
- package/dist-engine-src/src/backend/testing.rs +0 -658
- package/dist-engine-src/src/commit_store/codec.rs +0 -887
- package/dist-engine-src/src/commit_store/context.rs +0 -944
- package/dist-engine-src/src/commit_store/materialization.rs +0 -84
- package/dist-engine-src/src/commit_store/mod.rs +0 -16
- package/dist-engine-src/src/commit_store/storage.rs +0 -600
- package/dist-engine-src/src/commit_store/types.rs +0 -215
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
- package/dist-engine-src/src/session/create_version.rs +0 -88
- package/dist-engine-src/src/session/merge/apply.rs +0 -23
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
- package/dist-engine-src/src/session/switch_version.rs +0 -110
- package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
- package/dist-engine-src/src/sql2/execute.rs +0 -3533
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
- package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
- package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
- package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
- package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
- package/dist-engine-src/src/sql2/version_scope.rs +0 -394
- package/dist-engine-src/src/storage/types.rs +0 -501
- package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
- package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
- package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
- package/dist-engine-src/src/version/lifecycle.rs +0 -221
- package/dist-engine-src/src/version/mod.rs +0 -13
- package/dist-engine-src/src/version/refs.rs +0 -330
- package/dist-engine-src/src/version/stage_rows.rs +0 -67
- package/dist-engine-src/src/version/types.rs +0 -21
|
@@ -1,90 +1,96 @@
|
|
|
1
|
-
use std::collections::{BTreeMap,
|
|
1
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
2
2
|
|
|
3
|
-
use crate::
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
use crate::changelog::{
|
|
4
|
+
ChangeLoadRequest, ChangeRecord, ChangelogContext, ChangelogReader, CommitLoadEntry,
|
|
5
|
+
CommitLoadRequest, CommitProjection, CommitRecord,
|
|
6
|
+
};
|
|
7
|
+
use crate::entity_pk::EntityPk;
|
|
8
|
+
use crate::storage::{StorageRead, StorageWriteSet};
|
|
6
9
|
use crate::tracked_state::codec::{encode_key_ref, encode_value_ref};
|
|
7
|
-
use crate::tracked_state::diff::{
|
|
8
|
-
|
|
10
|
+
use crate::tracked_state::diff::{
|
|
11
|
+
diff_commits, diff_commits_with_validation, TrackedStateDiff, TrackedStateDiffRequest,
|
|
12
|
+
TrackedStateDiffRow,
|
|
13
|
+
};
|
|
14
|
+
use crate::tracked_state::materialize_rows_from_index_entries;
|
|
15
|
+
#[cfg(test)]
|
|
9
16
|
use crate::tracked_state::merge::{self, TrackedStateMergePlan};
|
|
10
17
|
use crate::tracked_state::storage;
|
|
11
|
-
use crate::tracked_state::storage::DeltaJsonPackIndexesRef;
|
|
12
18
|
use crate::tracked_state::tree::TrackedStateTree;
|
|
13
19
|
use crate::tracked_state::types::{
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
TrackedStateCommitRoot, TrackedStateCommitRootParent, TrackedStateIndexValue, TrackedStateKey,
|
|
21
|
+
TrackedStateKeyRef, TrackedStateMutation, TrackedStateRootId, TrackedStateTreeScanRequest,
|
|
16
22
|
};
|
|
17
23
|
use crate::tracked_state::{
|
|
18
|
-
MaterializedTrackedStateRow, TrackedStateDeltaRef,
|
|
19
|
-
TrackedStateScanRequest,
|
|
24
|
+
MaterializedTrackedStateRow, TrackedStateDeltaRef, TrackedStateScanRequest,
|
|
20
25
|
};
|
|
21
|
-
use crate::LixError;
|
|
26
|
+
use crate::{LixError, NullableKeyFilter};
|
|
27
|
+
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
29
|
+
struct TrackedStateIdentity {
|
|
30
|
+
schema_key: String,
|
|
31
|
+
file_id: Option<String>,
|
|
32
|
+
entity_pk: EntityPk,
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
/// Factory for tracked-state readers,
|
|
35
|
+
/// Factory for tracked-state readers, root writers, and commit-root rebuilders.
|
|
24
36
|
///
|
|
25
|
-
/// Tracked state is stored as content-addressed roots.
|
|
37
|
+
/// Tracked state is stored as content-addressed roots. Branch refs
|
|
26
38
|
/// choose which commit/root to read; this context only owns root operations.
|
|
27
39
|
#[derive(Clone)]
|
|
28
40
|
pub(crate) struct TrackedStateContext {
|
|
29
41
|
tree: TrackedStateTree,
|
|
30
|
-
commit_store: CommitStoreContext,
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
impl TrackedStateContext {
|
|
34
45
|
pub(crate) fn new() -> Self {
|
|
35
46
|
Self {
|
|
36
47
|
tree: TrackedStateTree::new(),
|
|
37
|
-
commit_store: CommitStoreContext::new(),
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/// Creates a commit-id-addressed tracked-state reader.
|
|
42
52
|
pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
|
|
43
53
|
where
|
|
44
|
-
S:
|
|
54
|
+
S: StorageRead + Send + Sync,
|
|
45
55
|
{
|
|
46
56
|
TrackedStateStoreReader {
|
|
47
57
|
store,
|
|
48
58
|
tree: self.tree.clone(),
|
|
49
|
-
commit_store: self.commit_store,
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/// Creates a tracked-state writer over a caller-owned transaction and write set.
|
|
54
63
|
pub(crate) fn writer<'a, S>(
|
|
55
64
|
&'a self,
|
|
56
|
-
store: &'a
|
|
65
|
+
store: &'a S,
|
|
57
66
|
writes: &'a mut StorageWriteSet,
|
|
58
67
|
) -> TrackedStateWriter<'a, S>
|
|
59
68
|
where
|
|
60
|
-
S:
|
|
69
|
+
S: StorageRead + Send + Sync + ?Sized,
|
|
61
70
|
{
|
|
62
71
|
TrackedStateWriter {
|
|
72
|
+
chunk_overlay: storage::TrackedStateChunkOverlay::new(),
|
|
73
|
+
staged_roots: BTreeMap::new(),
|
|
63
74
|
tree: self.tree.clone(),
|
|
64
75
|
store,
|
|
65
76
|
writes,
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
/// Creates an explicit tracked-state
|
|
80
|
+
/// Creates an explicit tracked-state commit-root rebuilder.
|
|
70
81
|
///
|
|
71
|
-
/// Normal commits
|
|
72
|
-
///
|
|
73
|
-
pub(crate) fn
|
|
82
|
+
/// Normal commits stage commit roots directly. This rebuilder reconstructs
|
|
83
|
+
/// a missing root from changelog facts as an explicit maintenance path.
|
|
84
|
+
pub(crate) fn root_rebuilder<'a, S>(
|
|
74
85
|
&'a self,
|
|
75
|
-
store: &'a
|
|
86
|
+
store: &'a S,
|
|
76
87
|
writes: &'a mut StorageWriteSet,
|
|
77
|
-
|
|
78
|
-
) -> TrackedStateMaterializer<'a, S>
|
|
88
|
+
) -> TrackedStateRootRebuilder<'a, S>
|
|
79
89
|
where
|
|
80
|
-
S:
|
|
90
|
+
S: StorageRead + Send + Sync + ?Sized,
|
|
81
91
|
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
store,
|
|
85
|
-
writes,
|
|
86
|
-
commit_store,
|
|
87
|
-
}
|
|
92
|
+
let _ = self;
|
|
93
|
+
TrackedStateRootRebuilder { store, writes }
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -92,52 +98,53 @@ impl TrackedStateContext {
|
|
|
92
98
|
pub(crate) struct TrackedStateStoreReader<S> {
|
|
93
99
|
store: S,
|
|
94
100
|
tree: TrackedStateTree,
|
|
95
|
-
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
struct DiffCommitRootValidationCache {
|
|
104
|
+
commit_ref_winners: HashMap<String, HashMap<TrackedStateIdentity, String>>,
|
|
105
|
+
commit_root_metadata: HashMap<String, TrackedStateCommitRoot>,
|
|
106
|
+
commit_roots: HashMap<String, TrackedStateRootId>,
|
|
107
|
+
tree_values: HashMap<(TrackedStateRootId, TrackedStateKey), Option<TrackedStateIndexValue>>,
|
|
108
|
+
changelog_first_parents: HashMap<String, Option<String>>,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl DiffCommitRootValidationCache {
|
|
112
|
+
fn new() -> Self {
|
|
113
|
+
Self {
|
|
114
|
+
commit_ref_winners: HashMap::new(),
|
|
115
|
+
commit_root_metadata: HashMap::new(),
|
|
116
|
+
commit_roots: HashMap::new(),
|
|
117
|
+
tree_values: HashMap::new(),
|
|
118
|
+
changelog_first_parents: HashMap::new(),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
impl<S> TrackedStateStoreReader<S>
|
|
99
124
|
where
|
|
100
|
-
S:
|
|
125
|
+
S: StorageRead + Send + Sync,
|
|
101
126
|
{
|
|
102
127
|
pub(crate) async fn scan_rows_at_commit(
|
|
103
128
|
&mut self,
|
|
104
129
|
commit_id: &str,
|
|
105
130
|
request: &TrackedStateScanRequest,
|
|
106
131
|
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
107
|
-
let root_id = self.tree.load_root(&mut self.store, commit_id).await
|
|
108
|
-
|
|
109
|
-
if ByFileIndex::should_use(request) {
|
|
110
|
-
if let Some(by_file_root_id) =
|
|
111
|
-
storage::load_by_file_root(&mut self.store, commit_id).await?
|
|
112
|
-
{
|
|
113
|
-
self.scan_rows_at_commit_by_file_index(&root_id, &by_file_root_id, request)
|
|
114
|
-
.await?
|
|
115
|
-
} else {
|
|
116
|
-
self.tree
|
|
117
|
-
.scan(
|
|
118
|
-
&mut self.store,
|
|
119
|
-
&root_id,
|
|
120
|
-
&tree_scan_request_from_tracked(request),
|
|
121
|
-
)
|
|
122
|
-
.await?
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
self.tree
|
|
126
|
-
.scan(
|
|
127
|
-
&mut self.store,
|
|
128
|
-
&root_id,
|
|
129
|
-
&tree_scan_request_from_tracked(request),
|
|
130
|
-
)
|
|
131
|
-
.await?
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
self.projection_entries_at_commit(commit_id, &tree_scan_request_from_tracked(request))
|
|
135
|
-
.await?
|
|
132
|
+
let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
|
|
133
|
+
return Err(missing_commit_root_error(commit_id));
|
|
136
134
|
};
|
|
137
|
-
let
|
|
138
|
-
|
|
135
|
+
let rows = self
|
|
136
|
+
.tree
|
|
137
|
+
.scan(
|
|
138
|
+
&mut self.store,
|
|
139
|
+
&root_id,
|
|
140
|
+
&tree_scan_request_from_tracked(request),
|
|
141
|
+
)
|
|
142
|
+
.await?;
|
|
143
|
+
let materialization = crate::tracked_state::TrackedRowMaterialization::from_columns(
|
|
144
|
+
&request.read_columns.columns,
|
|
139
145
|
);
|
|
140
|
-
let mut rows =
|
|
146
|
+
let mut rows =
|
|
147
|
+
materialize_rows_from_index_entries(&mut self.store, rows, &materialization).await?;
|
|
141
148
|
if !request.filter.include_tombstones {
|
|
142
149
|
rows.retain(|row| !row.deleted);
|
|
143
150
|
}
|
|
@@ -147,36 +154,31 @@ where
|
|
|
147
154
|
Ok(rows)
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
#[cfg(any(test, feature = "storage-benches"))]
|
|
150
158
|
pub(crate) async fn load_rows_at_commit(
|
|
151
159
|
&mut self,
|
|
152
160
|
commit_id: &str,
|
|
153
|
-
|
|
161
|
+
keys: &[TrackedStateKey],
|
|
154
162
|
) -> Result<Vec<Option<MaterializedTrackedStateRow>>, LixError> {
|
|
155
|
-
if
|
|
163
|
+
if keys.is_empty() {
|
|
156
164
|
return Ok(Vec::new());
|
|
157
165
|
}
|
|
158
|
-
let
|
|
159
|
-
.iter()
|
|
160
|
-
.map(tracked_key_from_request)
|
|
161
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
162
|
-
let values = self
|
|
163
|
-
.projection_values_at_commit_for_keys(commit_id, &keys)
|
|
164
|
-
.await?;
|
|
166
|
+
let values = self.commit_root_values_for_keys(commit_id, &keys).await?;
|
|
165
167
|
let mut entry_indices = Vec::new();
|
|
166
168
|
let mut entries = Vec::new();
|
|
167
|
-
for (index, (key, value)) in keys.
|
|
169
|
+
for (index, (key, value)) in keys.iter().cloned().zip(values).enumerate() {
|
|
168
170
|
if let Some(value) = value {
|
|
169
171
|
entry_indices.push(index);
|
|
170
172
|
entries.push((key, value));
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
|
-
let materialized =
|
|
175
|
+
let materialized = materialize_rows_from_index_entries(
|
|
174
176
|
&mut self.store,
|
|
175
177
|
entries,
|
|
176
|
-
&crate::tracked_state::
|
|
178
|
+
&crate::tracked_state::TrackedRowMaterialization::full(),
|
|
177
179
|
)
|
|
178
180
|
.await?;
|
|
179
|
-
let mut rows = vec![None;
|
|
181
|
+
let mut rows = vec![None; keys.len()];
|
|
180
182
|
for (index, row) in entry_indices.into_iter().zip(materialized) {
|
|
181
183
|
rows[index] = Some(row);
|
|
182
184
|
}
|
|
@@ -192,500 +194,623 @@ where
|
|
|
192
194
|
diff_commits(self, left_commit_id, right_commit_id, request).await
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
pub(crate) async fn
|
|
197
|
+
pub(crate) async fn diff_commits_with_validation(
|
|
196
198
|
&mut self,
|
|
197
199
|
left_commit_id: &str,
|
|
198
200
|
right_commit_id: &str,
|
|
199
|
-
request: &
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.tree
|
|
213
|
-
.diff(
|
|
214
|
-
&mut self.store,
|
|
215
|
-
left_root.as_ref(),
|
|
216
|
-
right_root.as_ref(),
|
|
217
|
-
request,
|
|
218
|
-
)
|
|
219
|
-
.await?;
|
|
220
|
-
return Ok(entries);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if let Some(entries) = self
|
|
224
|
-
.diff_pending_delta_suffix(left_commit_id, right_commit_id, request)
|
|
225
|
-
.await?
|
|
226
|
-
{
|
|
227
|
-
return Ok(entries);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let left = self
|
|
231
|
-
.projection_entries_at_commit(left_commit_id, request)
|
|
232
|
-
.await?
|
|
233
|
-
.into_iter()
|
|
234
|
-
.collect::<BTreeMap<_, _>>();
|
|
235
|
-
let right = self
|
|
236
|
-
.projection_entries_at_commit(right_commit_id, request)
|
|
237
|
-
.await?
|
|
238
|
-
.into_iter()
|
|
239
|
-
.collect::<BTreeMap<_, _>>();
|
|
240
|
-
let keys = left
|
|
241
|
-
.keys()
|
|
242
|
-
.chain(right.keys())
|
|
243
|
-
.cloned()
|
|
244
|
-
.collect::<BTreeSet<_>>();
|
|
245
|
-
let entries = keys
|
|
246
|
-
.into_iter()
|
|
247
|
-
.filter_map(|key| {
|
|
248
|
-
let before = left.get(&key).cloned().map(|value| (key.clone(), value));
|
|
249
|
-
let after = right.get(&key).cloned().map(|value| (key, value));
|
|
250
|
-
if before == after {
|
|
251
|
-
None
|
|
252
|
-
} else {
|
|
253
|
-
Some(TrackedStateTreeDiffEntry { before, after })
|
|
254
|
-
}
|
|
255
|
-
})
|
|
256
|
-
.collect();
|
|
257
|
-
Ok(entries)
|
|
201
|
+
request: &TrackedStateDiffRequest,
|
|
202
|
+
validate_left_root: bool,
|
|
203
|
+
validate_right_root: bool,
|
|
204
|
+
) -> Result<TrackedStateDiff, LixError> {
|
|
205
|
+
diff_commits_with_validation(
|
|
206
|
+
self,
|
|
207
|
+
left_commit_id,
|
|
208
|
+
right_commit_id,
|
|
209
|
+
request,
|
|
210
|
+
validate_left_root,
|
|
211
|
+
validate_right_root,
|
|
212
|
+
)
|
|
213
|
+
.await
|
|
258
214
|
}
|
|
259
215
|
|
|
260
|
-
async fn
|
|
216
|
+
pub(crate) async fn validate_diff_rows_for_commits_against_changelog(
|
|
261
217
|
&mut self,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
let left_delta_ids = self
|
|
267
|
-
.delta_commit_ids_since_projection_root(left_commit_id)
|
|
268
|
-
.await?;
|
|
269
|
-
let right_delta_ids = self
|
|
270
|
-
.delta_commit_ids_since_projection_root(right_commit_id)
|
|
271
|
-
.await?;
|
|
272
|
-
let left_base_commit_id = self
|
|
273
|
-
.projection_base_commit_id(left_commit_id, &left_delta_ids)
|
|
274
|
-
.await?;
|
|
275
|
-
let right_base_commit_id = self
|
|
276
|
-
.projection_base_commit_id(right_commit_id, &right_delta_ids)
|
|
277
|
-
.await?;
|
|
278
|
-
if left_base_commit_id != right_base_commit_id {
|
|
279
|
-
return Ok(None);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if right_delta_ids.starts_with(&left_delta_ids) {
|
|
283
|
-
let suffix = &right_delta_ids[left_delta_ids.len()..];
|
|
284
|
-
return self
|
|
285
|
-
.diff_pending_delta_suffix_from_base(left_commit_id, suffix, request, true)
|
|
286
|
-
.await
|
|
287
|
-
.map(Some);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if left_delta_ids.starts_with(&right_delta_ids) {
|
|
291
|
-
let suffix = &left_delta_ids[right_delta_ids.len()..];
|
|
292
|
-
return self
|
|
293
|
-
.diff_pending_delta_suffix_from_base(right_commit_id, suffix, request, false)
|
|
294
|
-
.await
|
|
295
|
-
.map(Some);
|
|
218
|
+
rows: &[(&TrackedStateDiffRow, &str)],
|
|
219
|
+
) -> Result<(), LixError> {
|
|
220
|
+
if rows.is_empty() {
|
|
221
|
+
return Ok(());
|
|
296
222
|
}
|
|
297
223
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
request: &TrackedStateTreeScanRequest,
|
|
306
|
-
suffix_is_after: bool,
|
|
307
|
-
) -> Result<Vec<TrackedStateTreeDiffEntry>, LixError> {
|
|
308
|
-
if suffix_commit_ids.is_empty() {
|
|
309
|
-
return Ok(Vec::new());
|
|
310
|
-
}
|
|
224
|
+
let mut change_ids = rows
|
|
225
|
+
.iter()
|
|
226
|
+
.filter(|(row, _)| row.schema_key != "lix_commit")
|
|
227
|
+
.map(|(row, _)| row.change_id.clone())
|
|
228
|
+
.collect::<Vec<_>>();
|
|
229
|
+
change_ids.sort();
|
|
230
|
+
change_ids.dedup();
|
|
311
231
|
|
|
312
|
-
let mut
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
232
|
+
let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
|
|
233
|
+
let loaded_changes = changelog_reader
|
|
234
|
+
.load_changes(ChangeLoadRequest {
|
|
235
|
+
change_ids: &change_ids,
|
|
236
|
+
})
|
|
237
|
+
.await?;
|
|
238
|
+
let mut changes = HashMap::new();
|
|
239
|
+
for (change_id, loaded) in change_ids.into_iter().zip(loaded_changes.entries) {
|
|
240
|
+
let Some(change) = loaded else {
|
|
241
|
+
return Err(LixError::unknown(format!(
|
|
242
|
+
"tracked-state diff row references missing changelog change '{change_id}'"
|
|
243
|
+
)));
|
|
317
244
|
};
|
|
318
|
-
|
|
319
|
-
if request.matches_key(&delta.key) {
|
|
320
|
-
changed.insert(delta.key, delta.value);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
245
|
+
changes.insert(change_id, change);
|
|
323
246
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
247
|
+
let commit_ids = rows
|
|
248
|
+
.iter()
|
|
249
|
+
.filter(|(row, _)| row.schema_key == "lix_commit")
|
|
250
|
+
.map(|(row, _)| row.commit_id.clone())
|
|
251
|
+
.collect::<Vec<_>>();
|
|
252
|
+
if !commit_ids.is_empty() {
|
|
253
|
+
let batch = changelog_reader
|
|
254
|
+
.load_commits(CommitLoadRequest {
|
|
255
|
+
commit_ids: &commit_ids,
|
|
256
|
+
projection: CommitProjection::Record,
|
|
257
|
+
})
|
|
258
|
+
.await?;
|
|
259
|
+
for (commit_id, entry) in commit_ids.into_iter().zip(batch.entries) {
|
|
260
|
+
let Some(CommitLoadEntry::Record(commit)) = entry else {
|
|
261
|
+
return Err(LixError::unknown(format!(
|
|
262
|
+
"tracked-state diff row references missing changelog commit '{commit_id}'"
|
|
263
|
+
)));
|
|
264
|
+
};
|
|
265
|
+
changes.insert(
|
|
266
|
+
commit.change_id.clone(),
|
|
267
|
+
change_record_from_commit_record(&commit)?,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
327
270
|
}
|
|
328
271
|
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
272
|
+
let mut validation_cache = DiffCommitRootValidationCache::new();
|
|
273
|
+
for (row, expected_commit_id) in rows {
|
|
274
|
+
validate_diff_row_against_changelog(row, &changes)?;
|
|
275
|
+
let change_created_at = changes
|
|
276
|
+
.get(&row.change_id)
|
|
277
|
+
.map(|change| change.created_at.as_str())
|
|
278
|
+
.ok_or_else(|| {
|
|
279
|
+
LixError::unknown(format!(
|
|
280
|
+
"tracked-state diff row references missing changelog change '{}'",
|
|
281
|
+
row.change_id
|
|
282
|
+
))
|
|
283
|
+
})?;
|
|
284
|
+
self.validate_diff_row_commit_root_membership(
|
|
285
|
+
row,
|
|
286
|
+
expected_commit_id,
|
|
287
|
+
change_created_at,
|
|
288
|
+
&mut validation_cache,
|
|
289
|
+
)
|
|
332
290
|
.await?;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.zip(base_values)
|
|
336
|
-
.filter_map(|(key, base_value)| {
|
|
337
|
-
let changed_value = changed.get(&key).cloned();
|
|
338
|
-
let (before_value, after_value) = if suffix_is_after {
|
|
339
|
-
(base_value, changed_value)
|
|
340
|
-
} else {
|
|
341
|
-
(changed_value, base_value)
|
|
342
|
-
};
|
|
343
|
-
if before_value == after_value {
|
|
344
|
-
return None;
|
|
345
|
-
}
|
|
346
|
-
Some(TrackedStateTreeDiffEntry {
|
|
347
|
-
before: before_value.map(|value| (key.clone(), value)),
|
|
348
|
-
after: after_value.map(|value| (key, value)),
|
|
349
|
-
})
|
|
350
|
-
})
|
|
351
|
-
.collect();
|
|
352
|
-
Ok(entries)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
pub(crate) async fn materialize_tree_values(
|
|
356
|
-
&mut self,
|
|
357
|
-
entries: Vec<(TrackedStateKey, TrackedStateIndexValue)>,
|
|
358
|
-
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
359
|
-
materialize_index_entries(
|
|
360
|
-
&mut self.store,
|
|
361
|
-
entries,
|
|
362
|
-
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
363
|
-
)
|
|
364
|
-
.await
|
|
291
|
+
}
|
|
292
|
+
Ok(())
|
|
365
293
|
}
|
|
366
294
|
|
|
367
|
-
async fn
|
|
295
|
+
async fn validate_diff_row_commit_root_membership(
|
|
368
296
|
&mut self,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
let
|
|
375
|
-
|
|
376
|
-
.
|
|
377
|
-
.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
297
|
+
row: &TrackedStateDiffRow,
|
|
298
|
+
root_commit_id: &str,
|
|
299
|
+
change_created_at: &str,
|
|
300
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
301
|
+
) -> Result<(), LixError> {
|
|
302
|
+
let identity = tracked_state_identity_from_diff_row(row)?;
|
|
303
|
+
let key = TrackedStateKey {
|
|
304
|
+
schema_key: row.schema_key.clone(),
|
|
305
|
+
file_id: row.file_id.clone(),
|
|
306
|
+
entity_pk: row.entity_pk.clone(),
|
|
307
|
+
};
|
|
308
|
+
let root_metadata = self
|
|
309
|
+
.load_cached_commit_root_metadata(root_commit_id, cache)
|
|
381
310
|
.await?;
|
|
382
|
-
|
|
383
|
-
let rows = self
|
|
384
|
-
.tree
|
|
385
|
-
.scan(
|
|
386
|
-
&mut self.store,
|
|
387
|
-
primary_root_id,
|
|
388
|
-
&tree_scan_request_from_tracked(request),
|
|
389
|
-
)
|
|
390
|
-
.await?;
|
|
391
|
-
return Ok(rows);
|
|
392
|
-
}
|
|
393
|
-
let index_rows = self
|
|
394
|
-
.tree
|
|
395
|
-
.scan(&mut self.store, by_file_root_id, &by_file_request)
|
|
311
|
+
self.validate_commit_root_parent_matches_changelog(root_commit_id, &root_metadata, cache)
|
|
396
312
|
.await?;
|
|
397
|
-
let
|
|
398
|
-
let
|
|
399
|
-
let
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
313
|
+
let (_, row_value) = row.clone().into_index_entry();
|
|
314
|
+
let mut current_commit_id = root_commit_id.to_string();
|
|
315
|
+
let mut seen = HashSet::new();
|
|
316
|
+
loop {
|
|
317
|
+
if !seen.insert(current_commit_id.clone()) {
|
|
318
|
+
return Err(LixError::unknown(format!(
|
|
319
|
+
"tracked-state commit-root parent chain contains cycle at commit '{current_commit_id}'"
|
|
320
|
+
)));
|
|
406
321
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.
|
|
322
|
+
|
|
323
|
+
let winners = self
|
|
324
|
+
.load_cached_commit_ref_winners(¤t_commit_id, cache)
|
|
410
325
|
.await?;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
326
|
+
if let Some(winner_change_id) = winners.get(&identity) {
|
|
327
|
+
if winner_change_id != &row.change_id {
|
|
328
|
+
return Err(LixError::unknown(format!(
|
|
329
|
+
"tracked-state diff row references changelog change '{}' that is not the first-parent winner for commit '{}' and identity {:?}",
|
|
330
|
+
row.change_id, root_commit_id, identity
|
|
331
|
+
)));
|
|
417
332
|
}
|
|
418
|
-
|
|
333
|
+
self.validate_diff_row_created_at(row, &key, ¤t_commit_id, change_created_at)
|
|
334
|
+
.await?;
|
|
335
|
+
return Ok(());
|
|
419
336
|
}
|
|
420
|
-
return Ok(rows);
|
|
421
|
-
}
|
|
422
337
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
338
|
+
let metadata = self
|
|
339
|
+
.load_cached_commit_root_metadata(¤t_commit_id, cache)
|
|
340
|
+
.await?;
|
|
341
|
+
self.validate_commit_root_parent_matches_changelog(
|
|
342
|
+
¤t_commit_id,
|
|
343
|
+
&metadata,
|
|
344
|
+
cache,
|
|
345
|
+
)
|
|
346
|
+
.await?;
|
|
347
|
+
let Some(parent) = metadata.parent_roots.first() else {
|
|
348
|
+
return Err(LixError::unknown(format!(
|
|
349
|
+
"tracked-state diff row references changelog change '{}' that is not the first-parent winner for commit '{}' and identity {:?}",
|
|
350
|
+
row.change_id, root_commit_id, identity
|
|
351
|
+
)));
|
|
426
352
|
};
|
|
427
|
-
let
|
|
428
|
-
|
|
429
|
-
|
|
353
|
+
let parent_value = self
|
|
354
|
+
.load_cached_tree_value(&parent.root_id, &key, cache)
|
|
355
|
+
.await?;
|
|
356
|
+
if parent_value.as_ref() != Some(&row_value) {
|
|
357
|
+
return Err(LixError::unknown(format!(
|
|
358
|
+
"tracked-state commit-root row for commit '{}' does not match parent root '{}' for inherited identity {:?}",
|
|
359
|
+
root_commit_id, parent.commit_id, identity
|
|
360
|
+
)));
|
|
430
361
|
}
|
|
362
|
+
current_commit_id = parent.commit_id.clone();
|
|
431
363
|
}
|
|
432
|
-
Ok(rows)
|
|
433
364
|
}
|
|
434
365
|
|
|
435
|
-
async fn
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
366
|
+
async fn validate_commit_root_parent_matches_changelog(
|
|
367
|
+
&mut self,
|
|
368
|
+
commit_id: &str,
|
|
369
|
+
metadata: &TrackedStateCommitRoot,
|
|
370
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
371
|
+
) -> Result<(), LixError> {
|
|
372
|
+
let changelog_first_parent = self
|
|
373
|
+
.load_cached_changelog_first_parent(commit_id, cache)
|
|
374
|
+
.await?;
|
|
375
|
+
let expected_parent = match changelog_first_parent.as_deref() {
|
|
376
|
+
Some(first_parent_id) => {
|
|
377
|
+
self.nearest_available_commit_root_parent(first_parent_id, cache)
|
|
378
|
+
.await?
|
|
379
|
+
}
|
|
380
|
+
None => None,
|
|
381
|
+
};
|
|
382
|
+
match (expected_parent, metadata.parent_roots.first()) {
|
|
383
|
+
(None, None) => Ok(()),
|
|
384
|
+
(Some((expected_parent_id, expected_root)), Some(parent))
|
|
385
|
+
if parent.commit_id == expected_parent_id && parent.root_id == expected_root =>
|
|
386
|
+
{
|
|
387
|
+
Ok(())
|
|
388
|
+
}
|
|
389
|
+
(Some((expected_parent_id, expected_root)), Some(parent))
|
|
390
|
+
if parent.commit_id == expected_parent_id =>
|
|
391
|
+
{
|
|
392
|
+
let _ = expected_root;
|
|
393
|
+
Err(LixError::unknown(format!(
|
|
394
|
+
"tracked-state commit-root metadata for commit '{}' references stale root for commit-root parent '{}'",
|
|
395
|
+
commit_id, expected_parent_id
|
|
396
|
+
)))
|
|
397
|
+
}
|
|
398
|
+
(Some((expected_parent_id, _)), Some(parent)) => Err(LixError::unknown(format!(
|
|
399
|
+
"tracked-state commit-root metadata for commit '{}' references parent '{}' but nearest available first-parent root is '{}'",
|
|
400
|
+
commit_id, parent.commit_id, expected_parent_id
|
|
401
|
+
))),
|
|
402
|
+
(Some((expected_parent_id, _)), None) => Err(LixError::unknown(format!(
|
|
403
|
+
"tracked-state commit-root metadata for commit '{}' is missing commit-root parent '{}'",
|
|
404
|
+
commit_id, expected_parent_id
|
|
405
|
+
))),
|
|
406
|
+
(None, Some(parent)) => Err(LixError::unknown(format!(
|
|
407
|
+
"tracked-state commit-root metadata for root commit '{}' references unexpected parent '{}'",
|
|
408
|
+
commit_id, parent.commit_id
|
|
409
|
+
))),
|
|
410
|
+
}
|
|
441
411
|
}
|
|
442
412
|
|
|
443
|
-
async fn
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
413
|
+
async fn nearest_available_commit_root_parent(
|
|
414
|
+
&mut self,
|
|
415
|
+
start_commit_id: &str,
|
|
416
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
417
|
+
) -> Result<Option<(String, TrackedStateRootId)>, LixError> {
|
|
418
|
+
let mut current = Some(start_commit_id.to_string());
|
|
419
|
+
let mut seen = HashSet::new();
|
|
420
|
+
while let Some(commit_id) = current {
|
|
421
|
+
if !seen.insert(commit_id.clone()) {
|
|
422
|
+
return Err(LixError::unknown(format!(
|
|
423
|
+
"tracked-state commit-root parent chain contains cycle at commit '{commit_id}'"
|
|
424
|
+
)));
|
|
425
|
+
}
|
|
426
|
+
if let Some(root_id) = self
|
|
427
|
+
.load_cached_commit_root_optional(&commit_id, cache)
|
|
428
|
+
.await?
|
|
429
|
+
{
|
|
430
|
+
return Ok(Some((commit_id, root_id)));
|
|
431
|
+
}
|
|
432
|
+
current = self
|
|
433
|
+
.load_cached_changelog_first_parent(&commit_id, cache)
|
|
434
|
+
.await?;
|
|
435
|
+
}
|
|
436
|
+
Ok(None)
|
|
448
437
|
}
|
|
449
438
|
|
|
450
|
-
async fn
|
|
439
|
+
async fn load_cached_commit_ref_winners(
|
|
451
440
|
&mut self,
|
|
452
441
|
commit_id: &str,
|
|
453
|
-
|
|
454
|
-
) -> Result<
|
|
455
|
-
let
|
|
456
|
-
.
|
|
457
|
-
.await?;
|
|
458
|
-
let base_commit_id = self
|
|
459
|
-
.projection_base_commit_id(commit_id, &delta_commit_ids)
|
|
460
|
-
.await?;
|
|
461
|
-
if base_commit_id.is_none() && delta_commit_ids.len() == 1 {
|
|
462
|
-
return self
|
|
463
|
-
.single_delta_pack_entries(&delta_commit_ids[0], request)
|
|
464
|
-
.await;
|
|
442
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
443
|
+
) -> Result<HashMap<TrackedStateIdentity, String>, LixError> {
|
|
444
|
+
if let Some(winners) = cache.commit_ref_winners.get(commit_id) {
|
|
445
|
+
return Ok(winners.clone());
|
|
465
446
|
}
|
|
466
|
-
let
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
474
|
-
format!(
|
|
475
|
-
"tracked_state projection base root '{base_commit_id}' disappeared"
|
|
476
|
-
),
|
|
477
|
-
)
|
|
478
|
-
})?;
|
|
479
|
-
self.tree
|
|
480
|
-
.scan(&mut self.store, &root_id, request)
|
|
481
|
-
.await?
|
|
482
|
-
.into_iter()
|
|
483
|
-
.collect::<BTreeMap<_, _>>()
|
|
484
|
-
} else {
|
|
485
|
-
BTreeMap::new()
|
|
486
|
-
};
|
|
487
|
-
self.apply_delta_packs_to_entries(&delta_commit_ids, Some(request), &mut entries)
|
|
447
|
+
let commit_ids = [commit_id.to_string()];
|
|
448
|
+
let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
|
|
449
|
+
let batch = changelog_reader
|
|
450
|
+
.load_commits(CommitLoadRequest {
|
|
451
|
+
commit_ids: &commit_ids,
|
|
452
|
+
projection: CommitProjection::Full,
|
|
453
|
+
})
|
|
488
454
|
.await?;
|
|
489
|
-
|
|
455
|
+
let Some(entry) = batch.entries.into_iter().next().flatten() else {
|
|
456
|
+
return Err(LixError::unknown(format!(
|
|
457
|
+
"changelog commit '{commit_id}' is missing while validating tracked-state commit-root rows"
|
|
458
|
+
)));
|
|
459
|
+
};
|
|
460
|
+
let CommitLoadEntry::Full {
|
|
461
|
+
record,
|
|
462
|
+
change_ref_chunks: chunks,
|
|
463
|
+
} = entry
|
|
464
|
+
else {
|
|
465
|
+
return Err(LixError::unknown(format!(
|
|
466
|
+
"changelog commit '{commit_id}' did not return full commit"
|
|
467
|
+
)));
|
|
468
|
+
};
|
|
469
|
+
let mut winners = HashMap::new();
|
|
470
|
+
winners.insert(
|
|
471
|
+
TrackedStateIdentity {
|
|
472
|
+
schema_key: "lix_commit".to_string(),
|
|
473
|
+
file_id: None,
|
|
474
|
+
entity_pk: EntityPk::single(&record.commit_id),
|
|
475
|
+
},
|
|
476
|
+
record.change_id,
|
|
477
|
+
);
|
|
478
|
+
for change_ref in chunks.into_iter().flat_map(|chunk| chunk.entries) {
|
|
479
|
+
winners.insert(
|
|
480
|
+
TrackedStateIdentity {
|
|
481
|
+
schema_key: change_ref.schema_key,
|
|
482
|
+
file_id: change_ref.file_id,
|
|
483
|
+
entity_pk: change_ref.entity_pk,
|
|
484
|
+
},
|
|
485
|
+
change_ref.change_id,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
cache
|
|
489
|
+
.commit_ref_winners
|
|
490
|
+
.insert(commit_id.to_string(), winners.clone());
|
|
491
|
+
Ok(winners)
|
|
490
492
|
}
|
|
491
493
|
|
|
492
|
-
async fn
|
|
494
|
+
async fn load_cached_commit_root_metadata(
|
|
493
495
|
&mut self,
|
|
494
496
|
commit_id: &str,
|
|
495
|
-
|
|
496
|
-
) -> Result<
|
|
497
|
-
let Some(
|
|
498
|
-
|
|
499
|
-
return Ok(Vec::new());
|
|
500
|
-
};
|
|
501
|
-
let mut rows = delta_entries
|
|
502
|
-
.into_iter()
|
|
503
|
-
.enumerate()
|
|
504
|
-
.filter_map(|(ordinal, delta)| {
|
|
505
|
-
request
|
|
506
|
-
.matches_key(&delta.key)
|
|
507
|
-
.then_some((ordinal, delta.key, delta.value))
|
|
508
|
-
})
|
|
509
|
-
.collect::<Vec<_>>();
|
|
510
|
-
rows.sort_by(|left, right| left.1.cmp(&right.1).then(left.0.cmp(&right.0)));
|
|
511
|
-
|
|
512
|
-
let mut out = Vec::new();
|
|
513
|
-
let mut rows = rows.into_iter().peekable();
|
|
514
|
-
while let Some((_, key, mut value)) = rows.next() {
|
|
515
|
-
while rows.peek().is_some_and(|(_, next_key, _)| next_key == &key) {
|
|
516
|
-
let (_, _, next_value) = rows
|
|
517
|
-
.next()
|
|
518
|
-
.expect("peek confirmed duplicate delta entry exists");
|
|
519
|
-
value = next_value;
|
|
520
|
-
}
|
|
521
|
-
if !request.include_tombstones && value.deleted {
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
out.push((key, value));
|
|
497
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
498
|
+
) -> Result<TrackedStateCommitRoot, LixError> {
|
|
499
|
+
if let Some(metadata) = cache.commit_root_metadata.get(commit_id) {
|
|
500
|
+
return Ok(metadata.clone());
|
|
525
501
|
}
|
|
526
|
-
|
|
502
|
+
let metadata = storage::load_commit_root(&mut self.store, commit_id)
|
|
503
|
+
.await?
|
|
504
|
+
.ok_or_else(|| missing_commit_root_error(commit_id))?;
|
|
505
|
+
cache
|
|
506
|
+
.commit_root_metadata
|
|
507
|
+
.insert(commit_id.to_string(), metadata.clone());
|
|
508
|
+
Ok(metadata)
|
|
527
509
|
}
|
|
528
510
|
|
|
529
|
-
async fn
|
|
511
|
+
async fn load_cached_commit_root_optional(
|
|
530
512
|
&mut self,
|
|
531
513
|
commit_id: &str,
|
|
532
|
-
|
|
533
|
-
) -> Result<
|
|
534
|
-
let
|
|
535
|
-
.
|
|
536
|
-
|
|
537
|
-
let
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
Ok(keys.iter().map(|key| entries.get(key).cloned()).collect())
|
|
514
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
515
|
+
) -> Result<Option<TrackedStateRootId>, LixError> {
|
|
516
|
+
if let Some(root_id) = cache.commit_roots.get(commit_id) {
|
|
517
|
+
return Ok(Some(root_id.clone()));
|
|
518
|
+
}
|
|
519
|
+
let root_id = storage::load_root(&self.store, commit_id).await?;
|
|
520
|
+
if let Some(root_id) = &root_id {
|
|
521
|
+
cache
|
|
522
|
+
.commit_roots
|
|
523
|
+
.insert(commit_id.to_string(), root_id.clone());
|
|
524
|
+
}
|
|
525
|
+
Ok(root_id)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async fn load_cached_tree_value(
|
|
529
|
+
&mut self,
|
|
530
|
+
root_id: &TrackedStateRootId,
|
|
531
|
+
key: &TrackedStateKey,
|
|
532
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
533
|
+
) -> Result<Option<TrackedStateIndexValue>, LixError> {
|
|
534
|
+
let cache_key = (root_id.clone(), key.clone());
|
|
535
|
+
if let Some(value) = cache.tree_values.get(&cache_key) {
|
|
536
|
+
return Ok(value.clone());
|
|
537
|
+
}
|
|
538
|
+
let value = self
|
|
539
|
+
.tree
|
|
540
|
+
.get_many(&mut self.store, root_id, std::slice::from_ref(key))
|
|
541
|
+
.await?
|
|
542
|
+
.into_iter()
|
|
543
|
+
.next()
|
|
544
|
+
.flatten();
|
|
545
|
+
cache.tree_values.insert(cache_key, value.clone());
|
|
546
|
+
Ok(value)
|
|
566
547
|
}
|
|
567
548
|
|
|
568
|
-
async fn
|
|
549
|
+
async fn load_cached_changelog_first_parent(
|
|
569
550
|
&mut self,
|
|
570
551
|
commit_id: &str,
|
|
571
|
-
|
|
552
|
+
cache: &mut DiffCommitRootValidationCache,
|
|
572
553
|
) -> Result<Option<String>, LixError> {
|
|
573
|
-
if
|
|
574
|
-
return Ok(
|
|
575
|
-
Some(commit_id.to_string())
|
|
576
|
-
} else {
|
|
577
|
-
None
|
|
578
|
-
});
|
|
554
|
+
if let Some(parent_id) = cache.changelog_first_parents.get(commit_id) {
|
|
555
|
+
return Ok(parent_id.clone());
|
|
579
556
|
}
|
|
580
|
-
let
|
|
581
|
-
|
|
557
|
+
let commit_ids = [commit_id.to_string()];
|
|
558
|
+
let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
|
|
559
|
+
let batch = changelog_reader
|
|
560
|
+
.load_commits(CommitLoadRequest {
|
|
561
|
+
commit_ids: &commit_ids,
|
|
562
|
+
projection: CommitProjection::Record,
|
|
563
|
+
})
|
|
564
|
+
.await?;
|
|
565
|
+
let Some(entry) = batch.entries.into_iter().next().flatten() else {
|
|
566
|
+
return Err(LixError::unknown(format!(
|
|
567
|
+
"changelog commit '{commit_id}' is missing while validating tracked-state commit-root metadata"
|
|
568
|
+
)));
|
|
582
569
|
};
|
|
583
|
-
let
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
.ok_or_else(|| missing_commit_error(first_delta_commit_id))?;
|
|
588
|
-
let Some(parent_id) = commit.parent_ids.first() else {
|
|
589
|
-
return Ok(None);
|
|
570
|
+
let CommitLoadEntry::Record(record) = entry else {
|
|
571
|
+
return Err(LixError::unknown(format!(
|
|
572
|
+
"changelog commit '{commit_id}' did not return a commit record"
|
|
573
|
+
)));
|
|
590
574
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
575
|
+
let parent_id = record.parent_commit_ids.first().cloned();
|
|
576
|
+
cache
|
|
577
|
+
.changelog_first_parents
|
|
578
|
+
.insert(commit_id.to_string(), parent_id.clone());
|
|
579
|
+
Ok(parent_id)
|
|
596
580
|
}
|
|
597
581
|
|
|
598
|
-
async fn
|
|
582
|
+
async fn validate_diff_row_created_at(
|
|
599
583
|
&mut self,
|
|
584
|
+
row: &TrackedStateDiffRow,
|
|
585
|
+
key: &TrackedStateKey,
|
|
600
586
|
commit_id: &str,
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
let mut
|
|
604
|
-
let
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
format!("tracked_state projection found first-parent cycle at '{current_id}'"),
|
|
610
|
-
));
|
|
611
|
-
}
|
|
612
|
-
if self
|
|
587
|
+
change_created_at: &str,
|
|
588
|
+
) -> Result<(), LixError> {
|
|
589
|
+
let mut expected_created_at = change_created_at.to_string();
|
|
590
|
+
let Some(metadata) = storage::load_commit_root(&mut self.store, commit_id).await? else {
|
|
591
|
+
return Err(missing_commit_root_error(commit_id));
|
|
592
|
+
};
|
|
593
|
+
if let Some(parent) = metadata.parent_roots.first() {
|
|
594
|
+
let parent_value = self
|
|
613
595
|
.tree
|
|
614
|
-
.
|
|
596
|
+
.get_many(&mut self.store, &parent.root_id, std::slice::from_ref(key))
|
|
615
597
|
.await?
|
|
616
|
-
.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
out.push(current_id.clone());
|
|
598
|
+
.into_iter()
|
|
599
|
+
.next()
|
|
600
|
+
.flatten();
|
|
601
|
+
if let Some(parent_value) = parent_value {
|
|
602
|
+
expected_created_at = parent_value.created_at;
|
|
622
603
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
604
|
+
}
|
|
605
|
+
if expected_created_at == change_created_at {
|
|
606
|
+
if let Some(merge_parent_created_at) = self
|
|
607
|
+
.load_merge_parent_created_at_for_row(commit_id, row, key)
|
|
626
608
|
.await?
|
|
627
|
-
|
|
628
|
-
|
|
609
|
+
{
|
|
610
|
+
expected_created_at = merge_parent_created_at;
|
|
611
|
+
}
|
|
629
612
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
&mut self,
|
|
636
|
-
commit_ids: &[String],
|
|
637
|
-
request: Option<&TrackedStateTreeScanRequest>,
|
|
638
|
-
entries: &mut BTreeMap<TrackedStateKey, TrackedStateIndexValue>,
|
|
639
|
-
) -> Result<(), LixError> {
|
|
640
|
-
for commit_id in commit_ids {
|
|
641
|
-
let Some(delta_entries) = storage::load_delta_pack(&mut self.store, commit_id).await?
|
|
642
|
-
else {
|
|
643
|
-
continue;
|
|
644
|
-
};
|
|
645
|
-
for delta in delta_entries {
|
|
646
|
-
if let Some(request) = request {
|
|
647
|
-
if !request.matches_key(&delta.key) {
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
if !request.include_tombstones && delta.value.deleted {
|
|
651
|
-
entries.remove(&delta.key);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
entries.insert(delta.key, delta.value);
|
|
655
|
-
} else {
|
|
656
|
-
entries.insert(delta.key, delta.value);
|
|
657
|
-
}
|
|
613
|
+
if expected_created_at == change_created_at && row.commit_id != commit_id {
|
|
614
|
+
if let Some(source_created_at) =
|
|
615
|
+
self.load_parent_created_at_for_row_commit(row, key).await?
|
|
616
|
+
{
|
|
617
|
+
expected_created_at = source_created_at;
|
|
658
618
|
}
|
|
659
619
|
}
|
|
660
|
-
|
|
620
|
+
if row.created_at == expected_created_at {
|
|
621
|
+
return Ok(());
|
|
622
|
+
}
|
|
623
|
+
Err(LixError::unknown(format!(
|
|
624
|
+
"tracked-state diff row for change '{}' created_at '{}' does not match first ancestry timestamp '{}'",
|
|
625
|
+
row.change_id, row.created_at, expected_created_at
|
|
626
|
+
)))
|
|
661
627
|
}
|
|
662
628
|
|
|
663
|
-
async fn
|
|
629
|
+
async fn load_merge_parent_created_at_for_row(
|
|
664
630
|
&mut self,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
) -> Result<
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
631
|
+
commit_id: &str,
|
|
632
|
+
row: &TrackedStateDiffRow,
|
|
633
|
+
key: &TrackedStateKey,
|
|
634
|
+
) -> Result<Option<String>, LixError> {
|
|
635
|
+
let commit_ids = [commit_id.to_string()];
|
|
636
|
+
let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
|
|
637
|
+
let batch = changelog_reader
|
|
638
|
+
.load_commits(CommitLoadRequest {
|
|
639
|
+
commit_ids: &commit_ids,
|
|
640
|
+
projection: CommitProjection::Record,
|
|
641
|
+
})
|
|
642
|
+
.await?;
|
|
643
|
+
let Some(CommitLoadEntry::Record(commit)) = batch.entries.into_iter().next().flatten()
|
|
644
|
+
else {
|
|
645
|
+
return Ok(None);
|
|
646
|
+
};
|
|
647
|
+
for parent_id in commit.parent_commit_ids.iter().skip(1) {
|
|
648
|
+
let Some(parent_root) = storage::load_root(&self.store, parent_id).await? else {
|
|
672
649
|
continue;
|
|
673
650
|
};
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
651
|
+
let parent_value = self
|
|
652
|
+
.tree
|
|
653
|
+
.get_many(&mut self.store, &parent_root, std::slice::from_ref(key))
|
|
654
|
+
.await?
|
|
655
|
+
.into_iter()
|
|
656
|
+
.next()
|
|
657
|
+
.flatten();
|
|
658
|
+
if let Some(parent_value) = parent_value {
|
|
659
|
+
if parent_value.change_id == row.change_id {
|
|
660
|
+
return Ok(Some(parent_value.created_at));
|
|
677
661
|
}
|
|
678
662
|
}
|
|
679
663
|
}
|
|
680
|
-
Ok(
|
|
664
|
+
Ok(None)
|
|
681
665
|
}
|
|
682
666
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
667
|
+
async fn load_parent_created_at_for_row_commit(
|
|
668
|
+
&mut self,
|
|
669
|
+
row: &TrackedStateDiffRow,
|
|
670
|
+
key: &TrackedStateKey,
|
|
671
|
+
) -> Result<Option<String>, LixError> {
|
|
672
|
+
let Some(metadata) = storage::load_commit_root(&mut self.store, &row.commit_id).await?
|
|
673
|
+
else {
|
|
674
|
+
return Ok(None);
|
|
675
|
+
};
|
|
676
|
+
let Some(parent) = metadata.parent_roots.first() else {
|
|
677
|
+
return Ok(None);
|
|
678
|
+
};
|
|
679
|
+
let parent_value = self
|
|
680
|
+
.tree
|
|
681
|
+
.get_many(&mut self.store, &parent.root_id, std::slice::from_ref(key))
|
|
682
|
+
.await?
|
|
683
|
+
.into_iter()
|
|
684
|
+
.next()
|
|
685
|
+
.flatten();
|
|
686
|
+
Ok(parent_value.map(|value| value.created_at))
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
pub(crate) async fn validate_tree_rows_at_commit_against_changelog(
|
|
690
|
+
&mut self,
|
|
691
|
+
commit_id: &str,
|
|
692
|
+
request: &TrackedStateTreeScanRequest,
|
|
693
|
+
) -> Result<(), LixError> {
|
|
694
|
+
let root = self.load_ensured_root(commit_id).await?;
|
|
695
|
+
let rows = self.tree.scan(&mut self.store, &root, request).await?;
|
|
696
|
+
self.validate_commit_root_coverage(commit_id, request, &rows)
|
|
697
|
+
.await?;
|
|
698
|
+
let rows = rows
|
|
699
|
+
.into_iter()
|
|
700
|
+
.map(|(key, value)| TrackedStateDiffRow::from_tree_entry(key, value))
|
|
701
|
+
.collect::<Vec<_>>();
|
|
702
|
+
let row_refs = rows.iter().map(|row| (row, commit_id)).collect::<Vec<_>>();
|
|
703
|
+
self.validate_diff_rows_for_commits_against_changelog(&row_refs)
|
|
704
|
+
.await
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async fn validate_commit_root_coverage(
|
|
708
|
+
&mut self,
|
|
709
|
+
commit_id: &str,
|
|
710
|
+
request: &TrackedStateTreeScanRequest,
|
|
711
|
+
rows: &[(TrackedStateKey, TrackedStateIndexValue)],
|
|
712
|
+
) -> Result<(), LixError> {
|
|
713
|
+
let row_map = rows
|
|
714
|
+
.iter()
|
|
715
|
+
.map(|(key, value)| (tracked_state_identity_from_key(key), value))
|
|
716
|
+
.collect::<HashMap<_, _>>();
|
|
717
|
+
let mut cache = DiffCommitRootValidationCache::new();
|
|
718
|
+
let winners = self
|
|
719
|
+
.load_cached_commit_ref_winners(commit_id, &mut cache)
|
|
720
|
+
.await?;
|
|
721
|
+
for (identity, change_id) in &winners {
|
|
722
|
+
if !tracked_state_identity_matches_tree_request(identity, request) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
let Some(value) = row_map.get(identity) else {
|
|
726
|
+
return Err(LixError::unknown(format!(
|
|
727
|
+
"tracked-state commit-root for commit '{commit_id}' omits current changelog change '{change_id}' for identity {:?}",
|
|
728
|
+
identity
|
|
729
|
+
)));
|
|
730
|
+
};
|
|
731
|
+
if &value.change_id != change_id {
|
|
732
|
+
return Err(LixError::unknown(format!(
|
|
733
|
+
"tracked-state commit-root for commit '{commit_id}' stores change '{}' but changelog winner is '{}' for identity {:?}",
|
|
734
|
+
value.change_id, change_id, identity
|
|
735
|
+
)));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let metadata = self
|
|
740
|
+
.load_cached_commit_root_metadata(commit_id, &mut cache)
|
|
741
|
+
.await?;
|
|
742
|
+
let Some(parent) = metadata.parent_roots.first() else {
|
|
743
|
+
return Ok(());
|
|
744
|
+
};
|
|
745
|
+
let parent_rows = self
|
|
746
|
+
.tree
|
|
747
|
+
.scan(&mut self.store, &parent.root_id, request)
|
|
748
|
+
.await?;
|
|
749
|
+
for (parent_key, parent_value) in parent_rows {
|
|
750
|
+
let identity = tracked_state_identity_from_key(&parent_key);
|
|
751
|
+
if winners.contains_key(&identity) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
let Some(value) = row_map.get(&identity) else {
|
|
755
|
+
return Err(LixError::unknown(format!(
|
|
756
|
+
"tracked-state commit-root for commit '{commit_id}' omits inherited identity {:?} from parent '{}'",
|
|
757
|
+
identity, parent.commit_id
|
|
758
|
+
)));
|
|
759
|
+
};
|
|
760
|
+
if *value != &parent_value {
|
|
761
|
+
return Err(LixError::unknown(format!(
|
|
762
|
+
"tracked-state commit-root for commit '{commit_id}' does not preserve inherited identity {:?} from parent '{}'",
|
|
763
|
+
identity, parent.commit_id
|
|
764
|
+
)));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
Ok(())
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
pub(crate) async fn diff_tree_entries_at_commits(
|
|
771
|
+
&mut self,
|
|
772
|
+
left_commit_id: &str,
|
|
773
|
+
right_commit_id: &str,
|
|
774
|
+
request: &TrackedStateTreeScanRequest,
|
|
775
|
+
) -> Result<Vec<crate::tracked_state::types::TrackedStateTreeDiffEntry>, LixError> {
|
|
776
|
+
let left_root = self.load_ensured_root(left_commit_id).await?;
|
|
777
|
+
let right_root = self.load_ensured_root(right_commit_id).await?;
|
|
778
|
+
self.tree
|
|
779
|
+
.diff(
|
|
780
|
+
&mut self.store,
|
|
781
|
+
Some(&left_root),
|
|
782
|
+
Some(&right_root),
|
|
783
|
+
request,
|
|
784
|
+
)
|
|
785
|
+
.await
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async fn load_ensured_root(
|
|
789
|
+
&mut self,
|
|
790
|
+
commit_id: &str,
|
|
791
|
+
) -> Result<crate::tracked_state::types::TrackedStateRootId, LixError> {
|
|
792
|
+
self.tree
|
|
793
|
+
.load_root(&mut self.store, commit_id)
|
|
794
|
+
.await?
|
|
795
|
+
.ok_or_else(|| missing_commit_root_error(commit_id))
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
#[cfg(any(test, feature = "storage-benches"))]
|
|
799
|
+
async fn commit_root_values_for_keys(
|
|
800
|
+
&mut self,
|
|
801
|
+
commit_id: &str,
|
|
802
|
+
keys: &[TrackedStateKey],
|
|
803
|
+
) -> Result<Vec<Option<TrackedStateIndexValue>>, LixError> {
|
|
804
|
+
let root_id = self.load_ensured_root(commit_id).await?;
|
|
805
|
+
self.tree.get_many(&mut self.store, &root_id, keys).await
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/// Plans a three-way merge by diffing both heads against the same base.
|
|
809
|
+
///
|
|
810
|
+
/// `target_commit_id` is the destination root that should keep its own
|
|
811
|
+
/// changes. `source_commit_id` is the incoming root whose non-conflicting
|
|
687
812
|
/// changes should be applied.
|
|
688
|
-
#[
|
|
813
|
+
#[cfg(test)]
|
|
689
814
|
pub(crate) async fn plan_merge(
|
|
690
815
|
&mut self,
|
|
691
816
|
base_commit_id: &str,
|
|
@@ -703,75 +828,38 @@ where
|
|
|
703
828
|
}
|
|
704
829
|
}
|
|
705
830
|
|
|
706
|
-
/// Writer for
|
|
831
|
+
/// Writer for changelog-backed tracked-state commit roots.
|
|
707
832
|
pub(crate) struct TrackedStateWriter<'a, S: ?Sized> {
|
|
833
|
+
chunk_overlay: storage::TrackedStateChunkOverlay,
|
|
834
|
+
staged_roots: BTreeMap<String, crate::tracked_state::types::TrackedStateRootId>,
|
|
708
835
|
tree: TrackedStateTree,
|
|
709
|
-
store: &'a
|
|
836
|
+
store: &'a S,
|
|
710
837
|
writes: &'a mut StorageWriteSet,
|
|
711
838
|
}
|
|
712
839
|
|
|
713
|
-
/// Explicit
|
|
714
|
-
pub(crate) struct
|
|
715
|
-
pub(super)
|
|
716
|
-
pub(super) store: &'a mut S,
|
|
840
|
+
/// Explicit commit-root rebuilder created by `TrackedStateContext`.
|
|
841
|
+
pub(crate) struct TrackedStateRootRebuilder<'a, S: ?Sized> {
|
|
842
|
+
pub(super) store: &'a S,
|
|
717
843
|
pub(super) writes: &'a mut StorageWriteSet,
|
|
718
|
-
pub(super) commit_store: &'a CommitStoreContext,
|
|
719
844
|
}
|
|
720
845
|
|
|
721
|
-
impl<S>
|
|
846
|
+
impl<S> TrackedStateRootRebuilder<'_, S>
|
|
722
847
|
where
|
|
723
|
-
S:
|
|
848
|
+
S: StorageRead + Send + Sync + ?Sized,
|
|
724
849
|
{
|
|
725
|
-
pub(crate) async fn
|
|
850
|
+
pub(crate) async fn rebuild_commit_root_at(
|
|
726
851
|
&mut self,
|
|
727
852
|
commit_id: &str,
|
|
728
853
|
) -> Result<TrackedStateWriteReport, LixError> {
|
|
729
|
-
crate::tracked_state::
|
|
854
|
+
crate::tracked_state::commit_root_rebuild::rebuild_commit_root_at(self, commit_id).await
|
|
730
855
|
}
|
|
731
856
|
}
|
|
732
857
|
|
|
733
858
|
impl<S> TrackedStateWriter<'_, S>
|
|
734
859
|
where
|
|
735
|
-
S:
|
|
860
|
+
S: StorageRead + Send + Sync + ?Sized,
|
|
736
861
|
{
|
|
737
|
-
|
|
738
|
-
pub(crate) async fn stage_delta(
|
|
739
|
-
&mut self,
|
|
740
|
-
commit_id: &str,
|
|
741
|
-
_parent_commit_id: Option<&str>,
|
|
742
|
-
deltas: &[TrackedStateDeltaRef<'_>],
|
|
743
|
-
) -> Result<TrackedStateWriteReport, LixError> {
|
|
744
|
-
storage::stage_delta_pack_refs(self.writes, commit_id, deltas)?;
|
|
745
|
-
Ok(TrackedStateWriteReport {
|
|
746
|
-
commit_id: commit_id.to_string(),
|
|
747
|
-
changed_rows: deltas.len(),
|
|
748
|
-
primary_chunk_puts: 0,
|
|
749
|
-
by_file_chunk_puts: 0,
|
|
750
|
-
})
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
pub(crate) async fn stage_delta_with_json_pack_indexes(
|
|
754
|
-
&mut self,
|
|
755
|
-
commit_id: &str,
|
|
756
|
-
_parent_commit_id: Option<&str>,
|
|
757
|
-
deltas: &[TrackedStateDeltaRef<'_>],
|
|
758
|
-
json_pack_indexes: DeltaJsonPackIndexesRef<'_>,
|
|
759
|
-
) -> Result<TrackedStateWriteReport, LixError> {
|
|
760
|
-
storage::stage_delta_pack_refs_with_json_pack_indexes(
|
|
761
|
-
self.writes,
|
|
762
|
-
commit_id,
|
|
763
|
-
deltas,
|
|
764
|
-
json_pack_indexes,
|
|
765
|
-
)?;
|
|
766
|
-
Ok(TrackedStateWriteReport {
|
|
767
|
-
commit_id: commit_id.to_string(),
|
|
768
|
-
changed_rows: deltas.len(),
|
|
769
|
-
primary_chunk_puts: 0,
|
|
770
|
-
by_file_chunk_puts: 0,
|
|
771
|
-
})
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
pub(crate) async fn stage_projection_root<'a, I>(
|
|
862
|
+
pub(crate) async fn stage_commit_root<'a, I>(
|
|
775
863
|
&mut self,
|
|
776
864
|
commit_id: &str,
|
|
777
865
|
parent_commit_id: Option<&str>,
|
|
@@ -783,7 +871,11 @@ where
|
|
|
783
871
|
let deltas = deltas.into_iter().collect::<Vec<_>>();
|
|
784
872
|
let base_root = match parent_commit_id {
|
|
785
873
|
Some(parent_commit_id) => {
|
|
786
|
-
let
|
|
874
|
+
let root = match self.staged_roots.get(parent_commit_id) {
|
|
875
|
+
Some(root) => Some(root.clone()),
|
|
876
|
+
None => self.tree.load_root(self.store, parent_commit_id).await?,
|
|
877
|
+
};
|
|
878
|
+
let Some(root) = root else {
|
|
787
879
|
return Err(LixError::new(
|
|
788
880
|
"LIX_ERROR_UNKNOWN",
|
|
789
881
|
format!(
|
|
@@ -795,19 +887,37 @@ where
|
|
|
795
887
|
}
|
|
796
888
|
None => None,
|
|
797
889
|
};
|
|
890
|
+
let parent_values = if let Some(base_root) = base_root.as_ref() {
|
|
891
|
+
let keys = deltas
|
|
892
|
+
.iter()
|
|
893
|
+
.map(|delta| TrackedStateKey {
|
|
894
|
+
schema_key: delta.schema_key.to_string(),
|
|
895
|
+
file_id: delta.file_id.map(str::to_string),
|
|
896
|
+
entity_pk: delta.entity_pk.clone(),
|
|
897
|
+
})
|
|
898
|
+
.collect::<Vec<_>>();
|
|
899
|
+
self.tree.get_many(self.store, base_root, &keys).await?
|
|
900
|
+
} else {
|
|
901
|
+
vec![None; deltas.len()]
|
|
902
|
+
};
|
|
798
903
|
let mut mutations = Vec::with_capacity(deltas.len());
|
|
799
|
-
for delta in
|
|
904
|
+
for (delta, parent_value) in deltas.iter().zip(parent_values.iter()) {
|
|
905
|
+
let created_at = parent_value
|
|
906
|
+
.as_ref()
|
|
907
|
+
.map(|value| value.created_at.as_str())
|
|
908
|
+
.unwrap_or(delta.created_at);
|
|
800
909
|
let key = TrackedStateKeyRef {
|
|
801
|
-
schema_key: delta.
|
|
802
|
-
file_id: delta.
|
|
803
|
-
|
|
910
|
+
schema_key: delta.schema_key,
|
|
911
|
+
file_id: delta.file_id,
|
|
912
|
+
entity_pk: delta.entity_pk,
|
|
804
913
|
};
|
|
805
914
|
let value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
915
|
+
change_id: delta.change_id,
|
|
916
|
+
commit_id: delta.commit_id,
|
|
917
|
+
deleted: delta.deleted,
|
|
918
|
+
snapshot_ref: delta.snapshot_ref,
|
|
919
|
+
metadata_ref: delta.metadata_ref,
|
|
920
|
+
created_at,
|
|
811
921
|
updated_at: delta.updated_at,
|
|
812
922
|
};
|
|
813
923
|
mutations.push(TrackedStateMutation::put_encoded(
|
|
@@ -817,69 +927,69 @@ where
|
|
|
817
927
|
}
|
|
818
928
|
let result = self
|
|
819
929
|
.tree
|
|
820
|
-
.
|
|
930
|
+
.apply_mutations_with_overlay(
|
|
821
931
|
self.store,
|
|
822
932
|
self.writes,
|
|
933
|
+
&mut self.chunk_overlay,
|
|
823
934
|
base_root.as_ref(),
|
|
824
935
|
mutations,
|
|
825
936
|
Some(commit_id),
|
|
826
937
|
)
|
|
827
938
|
.await?;
|
|
939
|
+
self.staged_roots
|
|
940
|
+
.insert(commit_id.to_string(), result.root_id.clone());
|
|
941
|
+
storage::stage_commit_root(
|
|
942
|
+
self.writes,
|
|
943
|
+
&TrackedStateCommitRoot {
|
|
944
|
+
commit_id: commit_id.to_string(),
|
|
945
|
+
root_id: result.root_id.clone(),
|
|
946
|
+
parent_roots: parent_commit_id
|
|
947
|
+
.zip(base_root.as_ref())
|
|
948
|
+
.map(|(parent_commit_id, root_id)| {
|
|
949
|
+
vec![TrackedStateCommitRootParent {
|
|
950
|
+
commit_id: parent_commit_id.to_string(),
|
|
951
|
+
root_id: root_id.clone(),
|
|
952
|
+
}]
|
|
953
|
+
})
|
|
954
|
+
.unwrap_or_default(),
|
|
955
|
+
changed_key_count: u64::try_from(deltas.len()).map_err(|_| {
|
|
956
|
+
LixError::new(
|
|
957
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
958
|
+
"tracked_state commit_root changed key count exceeds u64",
|
|
959
|
+
)
|
|
960
|
+
})?,
|
|
961
|
+
row_count_estimate: u64::try_from(result.row_count).map_err(|_| {
|
|
962
|
+
LixError::new(
|
|
963
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
964
|
+
"tracked_state commit_root row count exceeds u64",
|
|
965
|
+
)
|
|
966
|
+
})?,
|
|
967
|
+
tree_height: u32::try_from(result.tree_height).map_err(|_| {
|
|
968
|
+
LixError::new(
|
|
969
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
970
|
+
"tracked_state commit_root tree height exceeds u32",
|
|
971
|
+
)
|
|
972
|
+
})?,
|
|
973
|
+
primary_chunk_count: u64::try_from(result.chunk_count).map_err(|_| {
|
|
974
|
+
LixError::new(
|
|
975
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
976
|
+
"tracked_state commit_root chunk count exceeds u64",
|
|
977
|
+
)
|
|
978
|
+
})?,
|
|
979
|
+
primary_chunk_bytes: u64::try_from(result.chunk_bytes).map_err(|_| {
|
|
980
|
+
LixError::new(
|
|
981
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
982
|
+
"tracked_state commit_root chunk bytes exceeds u64",
|
|
983
|
+
)
|
|
984
|
+
})?,
|
|
985
|
+
},
|
|
986
|
+
)?;
|
|
828
987
|
|
|
829
|
-
let by_file_base_root = match parent_commit_id {
|
|
830
|
-
Some(parent_commit_id) => {
|
|
831
|
-
storage::load_by_file_root(self.store, parent_commit_id).await?
|
|
832
|
-
}
|
|
833
|
-
None => None,
|
|
834
|
-
};
|
|
835
|
-
let concrete_file_deltas = deltas
|
|
836
|
-
.iter()
|
|
837
|
-
.filter(|delta| delta.change.file_id.is_some())
|
|
838
|
-
.collect::<Vec<_>>();
|
|
839
|
-
let by_file_chunk_puts = if concrete_file_deltas.is_empty() {
|
|
840
|
-
if let Some(by_file_base_root) = by_file_base_root.as_ref() {
|
|
841
|
-
storage::stage_by_file_root(self.writes, commit_id, by_file_base_root);
|
|
842
|
-
}
|
|
843
|
-
0
|
|
844
|
-
} else {
|
|
845
|
-
let mut by_file_mutations = Vec::with_capacity(concrete_file_deltas.len());
|
|
846
|
-
for delta in concrete_file_deltas {
|
|
847
|
-
let key = TrackedStateKeyRef {
|
|
848
|
-
schema_key: delta.change.schema_key,
|
|
849
|
-
file_id: delta.change.file_id,
|
|
850
|
-
entity_id: delta.change.entity_id,
|
|
851
|
-
};
|
|
852
|
-
let header_value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
853
|
-
change_locator: delta.locator,
|
|
854
|
-
deleted: delta.change.snapshot_ref.is_none(),
|
|
855
|
-
snapshot_ref: None,
|
|
856
|
-
metadata_ref: None,
|
|
857
|
-
created_at: delta.created_at,
|
|
858
|
-
updated_at: delta.updated_at,
|
|
859
|
-
};
|
|
860
|
-
by_file_mutations.push(TrackedStateMutation::put_encoded(
|
|
861
|
-
ByFileIndex::encode_key_ref(key),
|
|
862
|
-
ByFileIndex::encode_header_value_ref(header_value),
|
|
863
|
-
));
|
|
864
|
-
}
|
|
865
|
-
let by_file_result = self
|
|
866
|
-
.tree
|
|
867
|
-
.apply_mutations(
|
|
868
|
-
self.store,
|
|
869
|
-
self.writes,
|
|
870
|
-
by_file_base_root.as_ref(),
|
|
871
|
-
by_file_mutations,
|
|
872
|
-
None,
|
|
873
|
-
)
|
|
874
|
-
.await?;
|
|
875
|
-
storage::stage_by_file_root(self.writes, commit_id, &by_file_result.root_id);
|
|
876
|
-
by_file_result.chunk_count
|
|
877
|
-
};
|
|
878
988
|
Ok(TrackedStateWriteReport {
|
|
879
989
|
commit_id: commit_id.to_string(),
|
|
990
|
+
root_id: result.root_id,
|
|
880
991
|
changed_rows: deltas.len(),
|
|
881
992
|
primary_chunk_puts: result.chunk_count,
|
|
882
|
-
by_file_chunk_puts,
|
|
883
993
|
})
|
|
884
994
|
}
|
|
885
995
|
}
|
|
@@ -887,15 +997,17 @@ where
|
|
|
887
997
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
888
998
|
pub(crate) struct TrackedStateWriteReport {
|
|
889
999
|
pub(crate) commit_id: String,
|
|
1000
|
+
pub(crate) root_id: TrackedStateRootId,
|
|
890
1001
|
pub(crate) changed_rows: usize,
|
|
891
1002
|
pub(crate) primary_chunk_puts: usize,
|
|
892
|
-
pub(crate) by_file_chunk_puts: usize,
|
|
893
1003
|
}
|
|
894
1004
|
|
|
895
|
-
fn
|
|
1005
|
+
fn missing_commit_root_error(commit_id: &str) -> LixError {
|
|
896
1006
|
LixError::new(
|
|
897
|
-
|
|
898
|
-
|
|
1007
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
1008
|
+
format!(
|
|
1009
|
+
"tracked_state commit_root is missing for commit '{commit_id}'; run explicit commit_root rebuild before structural diff"
|
|
1010
|
+
),
|
|
899
1011
|
)
|
|
900
1012
|
}
|
|
901
1013
|
|
|
@@ -904,7 +1016,7 @@ fn tree_scan_request_from_tracked(
|
|
|
904
1016
|
) -> TrackedStateTreeScanRequest {
|
|
905
1017
|
TrackedStateTreeScanRequest {
|
|
906
1018
|
schema_keys: request.filter.schema_keys.clone(),
|
|
907
|
-
|
|
1019
|
+
entity_pks: request.filter.entity_pks.clone(),
|
|
908
1020
|
file_ids: request.filter.file_ids.clone(),
|
|
909
1021
|
include_tombstones: request.filter.include_tombstones,
|
|
910
1022
|
// User limits belong above delta overlay and tombstone visibility.
|
|
@@ -914,63 +1026,207 @@ fn tree_scan_request_from_tracked(
|
|
|
914
1026
|
}
|
|
915
1027
|
}
|
|
916
1028
|
|
|
917
|
-
fn
|
|
918
|
-
|
|
919
|
-
|
|
1029
|
+
fn validate_diff_row_against_changelog(
|
|
1030
|
+
row: &TrackedStateDiffRow,
|
|
1031
|
+
changes: &HashMap<String, ChangeRecord>,
|
|
1032
|
+
) -> Result<(), LixError> {
|
|
1033
|
+
let Some(change) = changes.get(&row.change_id) else {
|
|
1034
|
+
return Err(LixError::unknown(format!(
|
|
1035
|
+
"tracked-state diff row references missing changelog change '{}'",
|
|
1036
|
+
row.change_id
|
|
1037
|
+
)));
|
|
1038
|
+
};
|
|
1039
|
+
if change.schema_key != row.schema_key
|
|
1040
|
+
|| change.file_id != row.file_id
|
|
1041
|
+
|| change.entity_pk != row.entity_pk
|
|
1042
|
+
{
|
|
1043
|
+
return Err(LixError::unknown(format!(
|
|
1044
|
+
"tracked-state diff row for change '{}' does not match changelog change identity",
|
|
1045
|
+
row.change_id
|
|
1046
|
+
)));
|
|
1047
|
+
}
|
|
1048
|
+
if row.deleted != change.snapshot_ref.is_none() {
|
|
1049
|
+
return Err(LixError::unknown(format!(
|
|
1050
|
+
"tracked-state diff row for change '{}' deleted flag does not match changelog snapshot",
|
|
1051
|
+
row.change_id
|
|
1052
|
+
)));
|
|
1053
|
+
}
|
|
1054
|
+
if row.snapshot_ref != change.snapshot_ref || row.metadata_ref != change.metadata_ref {
|
|
1055
|
+
return Err(LixError::unknown(format!(
|
|
1056
|
+
"tracked-state diff row for change '{}' payload refs do not match changelog change",
|
|
1057
|
+
row.change_id
|
|
1058
|
+
)));
|
|
1059
|
+
}
|
|
1060
|
+
if row.updated_at != change.created_at {
|
|
1061
|
+
return Err(LixError::unknown(format!(
|
|
1062
|
+
"tracked-state diff row for change '{}' updated_at does not match changelog change timestamp",
|
|
1063
|
+
row.change_id
|
|
1064
|
+
)));
|
|
920
1065
|
}
|
|
921
|
-
|
|
922
|
-
.projection
|
|
923
|
-
.columns
|
|
924
|
-
.iter()
|
|
925
|
-
.any(|column| column == "snapshot_content" || column == "metadata")
|
|
1066
|
+
Ok(())
|
|
926
1067
|
}
|
|
927
1068
|
|
|
928
|
-
fn
|
|
929
|
-
let
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
file_id,
|
|
942
|
-
entity_id: request.entity_id.clone(),
|
|
1069
|
+
fn change_record_from_commit_record(commit: &CommitRecord) -> Result<ChangeRecord, LixError> {
|
|
1070
|
+
let snapshot_content = commit_row_snapshot_content(&commit.commit_id)?;
|
|
1071
|
+
Ok(ChangeRecord {
|
|
1072
|
+
format_version: 1,
|
|
1073
|
+
change_id: commit.change_id.clone(),
|
|
1074
|
+
schema_key: "lix_commit".to_string(),
|
|
1075
|
+
entity_pk: EntityPk::single(&commit.commit_id),
|
|
1076
|
+
file_id: None,
|
|
1077
|
+
snapshot_ref: Some(crate::json_store::JsonRef::for_content(
|
|
1078
|
+
snapshot_content.as_bytes(),
|
|
1079
|
+
)),
|
|
1080
|
+
metadata_ref: None,
|
|
1081
|
+
created_at: commit.created_at.clone(),
|
|
943
1082
|
})
|
|
944
1083
|
}
|
|
945
1084
|
|
|
1085
|
+
fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
|
|
1086
|
+
serde_json::to_string(&serde_json::json!({
|
|
1087
|
+
"id": commit_id,
|
|
1088
|
+
}))
|
|
1089
|
+
.map_err(|error| {
|
|
1090
|
+
LixError::new(
|
|
1091
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
1092
|
+
format!("failed to encode lix_commit snapshot: {error}"),
|
|
1093
|
+
)
|
|
1094
|
+
})
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
fn tracked_state_identity_from_diff_row(
|
|
1098
|
+
row: &TrackedStateDiffRow,
|
|
1099
|
+
) -> Result<TrackedStateIdentity, LixError> {
|
|
1100
|
+
Ok(TrackedStateIdentity {
|
|
1101
|
+
schema_key: row.schema_key.clone(),
|
|
1102
|
+
file_id: row.file_id.clone(),
|
|
1103
|
+
entity_pk: row.entity_pk.clone(),
|
|
1104
|
+
})
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
fn tracked_state_identity_from_key(key: &TrackedStateKey) -> TrackedStateIdentity {
|
|
1108
|
+
TrackedStateIdentity {
|
|
1109
|
+
schema_key: key.schema_key.clone(),
|
|
1110
|
+
file_id: key.file_id.clone(),
|
|
1111
|
+
entity_pk: key.entity_pk.clone(),
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
fn tracked_state_identity_matches_tree_request(
|
|
1116
|
+
identity: &TrackedStateIdentity,
|
|
1117
|
+
request: &TrackedStateTreeScanRequest,
|
|
1118
|
+
) -> bool {
|
|
1119
|
+
if !request.schema_keys.is_empty() && !request.schema_keys.contains(&identity.schema_key) {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
if !request.entity_pks.is_empty() && !request.entity_pks.contains(&identity.entity_pk) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
nullable_key_filter_allows(&request.file_ids, identity.file_id.as_deref())
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
fn nullable_key_filter_allows(filters: &[NullableKeyFilter<String>], value: Option<&str>) -> bool {
|
|
1129
|
+
filters.is_empty()
|
|
1130
|
+
|| filters.iter().any(|filter| match (filter, value) {
|
|
1131
|
+
(NullableKeyFilter::Any, _) => true,
|
|
1132
|
+
(NullableKeyFilter::Null, None) => true,
|
|
1133
|
+
(NullableKeyFilter::Value(expected), Some(value)) => expected == value,
|
|
1134
|
+
_ => false,
|
|
1135
|
+
})
|
|
1136
|
+
}
|
|
1137
|
+
|
|
946
1138
|
#[cfg(test)]
|
|
947
1139
|
mod tests {
|
|
948
|
-
use std::sync::Arc;
|
|
949
|
-
|
|
950
1140
|
use super::*;
|
|
951
|
-
use crate::
|
|
952
|
-
use crate::storage::{
|
|
1141
|
+
use crate::storage::StorageContext;
|
|
1142
|
+
use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
|
|
953
1143
|
use crate::NullableKeyFilter;
|
|
954
1144
|
|
|
955
1145
|
#[tokio::test]
|
|
956
|
-
async fn
|
|
957
|
-
let
|
|
958
|
-
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1146
|
+
async fn stage_commit_root_requires_parent_commit_root() {
|
|
1147
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
959
1148
|
let tracked_state = TrackedStateContext::new();
|
|
960
|
-
|
|
961
|
-
|
|
1149
|
+
{
|
|
1150
|
+
let mut read = storage
|
|
1151
|
+
.begin_read(StorageReadOptions::default())
|
|
1152
|
+
.expect("parent read should open");
|
|
1153
|
+
let mut writes = storage.new_write_set();
|
|
1154
|
+
crate::test_support::stage_empty_changelog_commit(
|
|
1155
|
+
&mut read,
|
|
1156
|
+
&mut writes,
|
|
1157
|
+
"missing-parent",
|
|
1158
|
+
None,
|
|
1159
|
+
)
|
|
962
1160
|
.await
|
|
963
|
-
.expect("
|
|
1161
|
+
.expect("parent changelog commit should stage");
|
|
1162
|
+
storage
|
|
1163
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1164
|
+
.expect("parent changelog commit should commit");
|
|
1165
|
+
}
|
|
964
1166
|
|
|
965
1167
|
write_root_for_test(
|
|
966
|
-
|
|
1168
|
+
&storage,
|
|
967
1169
|
&tracked_state,
|
|
968
1170
|
"commit-child",
|
|
969
1171
|
Some("missing-parent"),
|
|
970
1172
|
&[row("entity-child", "change-child", "commit-child")],
|
|
971
1173
|
)
|
|
972
1174
|
.await
|
|
973
|
-
.
|
|
1175
|
+
.expect_err("root staging should require a parent commit root");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
#[tokio::test]
|
|
1179
|
+
async fn stage_commit_root_writes_commit_root_metadata() {
|
|
1180
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1181
|
+
let tracked_state = TrackedStateContext::new();
|
|
1182
|
+
write_root_for_test(
|
|
1183
|
+
&storage,
|
|
1184
|
+
&tracked_state,
|
|
1185
|
+
"parent",
|
|
1186
|
+
None,
|
|
1187
|
+
&[row("entity-a", "change-parent", "parent")],
|
|
1188
|
+
)
|
|
1189
|
+
.await
|
|
1190
|
+
.expect("parent root should write");
|
|
1191
|
+
write_root_for_test(
|
|
1192
|
+
&storage,
|
|
1193
|
+
&tracked_state,
|
|
1194
|
+
"child",
|
|
1195
|
+
Some("parent"),
|
|
1196
|
+
&[
|
|
1197
|
+
row("entity-a", "change-child-a", "child"),
|
|
1198
|
+
row("entity-b", "change-child-b", "child"),
|
|
1199
|
+
],
|
|
1200
|
+
)
|
|
1201
|
+
.await
|
|
1202
|
+
.expect("child root should write");
|
|
1203
|
+
|
|
1204
|
+
let read = storage
|
|
1205
|
+
.begin_read(StorageReadOptions::default())
|
|
1206
|
+
.expect("read should open");
|
|
1207
|
+
let parent_root = storage::load_root(&read, "parent")
|
|
1208
|
+
.await
|
|
1209
|
+
.expect("parent root should load")
|
|
1210
|
+
.expect("parent root should exist");
|
|
1211
|
+
let child_root = storage::load_root(&read, "child")
|
|
1212
|
+
.await
|
|
1213
|
+
.expect("child root should load")
|
|
1214
|
+
.expect("child root should exist");
|
|
1215
|
+
let metadata = storage::load_commit_root(&read, "child")
|
|
1216
|
+
.await
|
|
1217
|
+
.expect("metadata should load")
|
|
1218
|
+
.expect("metadata should exist");
|
|
1219
|
+
|
|
1220
|
+
assert_eq!(metadata.commit_id, "child");
|
|
1221
|
+
assert_eq!(metadata.root_id, child_root);
|
|
1222
|
+
assert_eq!(metadata.parent_roots.len(), 1);
|
|
1223
|
+
assert_eq!(metadata.parent_roots[0].commit_id, "parent");
|
|
1224
|
+
assert_eq!(metadata.parent_roots[0].root_id, parent_root);
|
|
1225
|
+
assert_eq!(metadata.changed_key_count, 3);
|
|
1226
|
+
assert_eq!(metadata.row_count_estimate, 4);
|
|
1227
|
+
assert!(metadata.tree_height >= 1);
|
|
1228
|
+
assert!(metadata.primary_chunk_count >= 1);
|
|
1229
|
+
assert!(metadata.primary_chunk_bytes > 0);
|
|
974
1230
|
}
|
|
975
1231
|
|
|
976
1232
|
#[tokio::test]
|
|
@@ -988,7 +1244,11 @@ mod tests {
|
|
|
988
1244
|
.await;
|
|
989
1245
|
|
|
990
1246
|
let plan = tracked_state
|
|
991
|
-
.reader(
|
|
1247
|
+
.reader(
|
|
1248
|
+
storage
|
|
1249
|
+
.begin_read(StorageReadOptions::default())
|
|
1250
|
+
.expect("read should open"),
|
|
1251
|
+
)
|
|
992
1252
|
.plan_merge(
|
|
993
1253
|
"base",
|
|
994
1254
|
"target",
|
|
@@ -998,7 +1258,7 @@ mod tests {
|
|
|
998
1258
|
.await
|
|
999
1259
|
.expect("merge should plan");
|
|
1000
1260
|
|
|
1001
|
-
assert_eq!(
|
|
1261
|
+
assert_eq!(merge_pick_ids(&plan), vec!["entity-a"]);
|
|
1002
1262
|
assert!(plan.conflicts.is_empty());
|
|
1003
1263
|
}
|
|
1004
1264
|
|
|
@@ -1009,98 +1269,661 @@ mod tests {
|
|
|
1009
1269
|
&[row("entity-a", "change-target", "target")],
|
|
1010
1270
|
&[row("entity-a", "change-base", "base")],
|
|
1011
1271
|
)
|
|
1012
|
-
.await;
|
|
1272
|
+
.await;
|
|
1273
|
+
|
|
1274
|
+
let plan = tracked_state
|
|
1275
|
+
.reader(
|
|
1276
|
+
storage
|
|
1277
|
+
.begin_read(StorageReadOptions::default())
|
|
1278
|
+
.expect("read should open"),
|
|
1279
|
+
)
|
|
1280
|
+
.plan_merge(
|
|
1281
|
+
"base",
|
|
1282
|
+
"target",
|
|
1283
|
+
"source",
|
|
1284
|
+
&TrackedStateDiffRequest::default(),
|
|
1285
|
+
)
|
|
1286
|
+
.await
|
|
1287
|
+
.expect("merge should plan");
|
|
1288
|
+
|
|
1289
|
+
assert!(plan.picks.is_empty());
|
|
1290
|
+
assert!(plan.conflicts.is_empty());
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
#[tokio::test]
|
|
1294
|
+
async fn plan_merge_from_roots_reports_divergent_modification_conflict() {
|
|
1295
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
1296
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1297
|
+
&[row_with_value(
|
|
1298
|
+
"entity-a",
|
|
1299
|
+
"change-target",
|
|
1300
|
+
"target",
|
|
1301
|
+
"target",
|
|
1302
|
+
)],
|
|
1303
|
+
&[row_with_value(
|
|
1304
|
+
"entity-a",
|
|
1305
|
+
"change-source",
|
|
1306
|
+
"source",
|
|
1307
|
+
"source",
|
|
1308
|
+
)],
|
|
1309
|
+
)
|
|
1310
|
+
.await;
|
|
1311
|
+
|
|
1312
|
+
let plan = tracked_state
|
|
1313
|
+
.reader(
|
|
1314
|
+
storage
|
|
1315
|
+
.begin_read(StorageReadOptions::default())
|
|
1316
|
+
.expect("read should open"),
|
|
1317
|
+
)
|
|
1318
|
+
.plan_merge(
|
|
1319
|
+
"base",
|
|
1320
|
+
"target",
|
|
1321
|
+
"source",
|
|
1322
|
+
&TrackedStateDiffRequest::default(),
|
|
1323
|
+
)
|
|
1324
|
+
.await
|
|
1325
|
+
.expect("merge should plan");
|
|
1326
|
+
|
|
1327
|
+
assert!(plan.picks.is_empty());
|
|
1328
|
+
assert_eq!(merge_conflict_ids(&plan), vec!["entity-a"]);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
#[tokio::test]
|
|
1332
|
+
async fn plan_merge_from_roots_applies_source_tombstone() {
|
|
1333
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
1334
|
+
&[row("entity-a", "change-base", "base")],
|
|
1335
|
+
&[row("entity-a", "change-base", "base")],
|
|
1336
|
+
&[tombstone("entity-a", "change-source-delete", "source")],
|
|
1337
|
+
)
|
|
1338
|
+
.await;
|
|
1339
|
+
|
|
1340
|
+
let plan = tracked_state
|
|
1341
|
+
.reader(
|
|
1342
|
+
storage
|
|
1343
|
+
.begin_read(StorageReadOptions::default())
|
|
1344
|
+
.expect("read should open"),
|
|
1345
|
+
)
|
|
1346
|
+
.plan_merge(
|
|
1347
|
+
"base",
|
|
1348
|
+
"target",
|
|
1349
|
+
"source",
|
|
1350
|
+
&TrackedStateDiffRequest::default(),
|
|
1351
|
+
)
|
|
1352
|
+
.await
|
|
1353
|
+
.expect("merge should plan");
|
|
1354
|
+
|
|
1355
|
+
assert_eq!(merge_pick_ids(&plan), vec!["entity-a"]);
|
|
1356
|
+
assert!(plan.picks[0].source_row().deleted);
|
|
1357
|
+
assert_eq!(plan.picks[0].source_change_id(), "change-source-delete");
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
#[tokio::test]
|
|
1361
|
+
async fn explicit_rebuild_repairs_missing_child_root_from_nearest_parent() {
|
|
1362
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1363
|
+
let tracked_state = TrackedStateContext::new();
|
|
1364
|
+
write_root_for_test(
|
|
1365
|
+
&storage,
|
|
1366
|
+
&tracked_state,
|
|
1367
|
+
"base",
|
|
1368
|
+
None,
|
|
1369
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1370
|
+
)
|
|
1371
|
+
.await
|
|
1372
|
+
.expect("base root should write");
|
|
1373
|
+
write_root_for_test(
|
|
1374
|
+
&storage,
|
|
1375
|
+
&tracked_state,
|
|
1376
|
+
"child",
|
|
1377
|
+
Some("base"),
|
|
1378
|
+
&[row_with_value("entity-a", "change-child", "child", "child")],
|
|
1379
|
+
)
|
|
1380
|
+
.await
|
|
1381
|
+
.expect("child root should write");
|
|
1382
|
+
{
|
|
1383
|
+
let mut writes = storage.new_write_set();
|
|
1384
|
+
writes.delete(
|
|
1385
|
+
storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
|
|
1386
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"child")),
|
|
1387
|
+
);
|
|
1388
|
+
storage
|
|
1389
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1390
|
+
.expect("child commit_root delete should commit");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
tracked_state
|
|
1394
|
+
.reader(
|
|
1395
|
+
storage
|
|
1396
|
+
.begin_read(StorageReadOptions::default())
|
|
1397
|
+
.expect("read should open"),
|
|
1398
|
+
)
|
|
1399
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1400
|
+
.await
|
|
1401
|
+
.expect_err("diff should require durable roots before repair");
|
|
1402
|
+
|
|
1403
|
+
let mut read = storage
|
|
1404
|
+
.begin_read(StorageReadOptions::default())
|
|
1405
|
+
.expect("read should open");
|
|
1406
|
+
let mut writes = storage.new_write_set();
|
|
1407
|
+
tracked_state
|
|
1408
|
+
.root_rebuilder(&mut read, &mut writes)
|
|
1409
|
+
.rebuild_commit_root_at("child")
|
|
1410
|
+
.await
|
|
1411
|
+
.expect("child root should repair");
|
|
1412
|
+
storage
|
|
1413
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1414
|
+
.expect("repaired root should commit");
|
|
1415
|
+
|
|
1416
|
+
let diff = tracked_state
|
|
1417
|
+
.reader(
|
|
1418
|
+
storage
|
|
1419
|
+
.begin_read(StorageReadOptions::default())
|
|
1420
|
+
.expect("read should open"),
|
|
1421
|
+
)
|
|
1422
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1423
|
+
.await
|
|
1424
|
+
.expect("diff should use repaired root");
|
|
1425
|
+
|
|
1426
|
+
assert_eq!(diff.entries.len(), 1);
|
|
1427
|
+
assert_eq!(
|
|
1428
|
+
diff.entries[0].kind,
|
|
1429
|
+
crate::tracked_state::TrackedStateDiffKind::Modified
|
|
1430
|
+
);
|
|
1431
|
+
assert_eq!(
|
|
1432
|
+
diff.entries[0]
|
|
1433
|
+
.after
|
|
1434
|
+
.as_ref()
|
|
1435
|
+
.map(|row| row.change_id.as_str()),
|
|
1436
|
+
Some("change-child")
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
#[tokio::test]
|
|
1441
|
+
async fn diff_allows_repaired_root_with_rebuilt_ancestor_chain() {
|
|
1442
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1443
|
+
let tracked_state = TrackedStateContext::new();
|
|
1444
|
+
write_root_for_test(
|
|
1445
|
+
&storage,
|
|
1446
|
+
&tracked_state,
|
|
1447
|
+
"base",
|
|
1448
|
+
None,
|
|
1449
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1450
|
+
)
|
|
1451
|
+
.await
|
|
1452
|
+
.expect("base root should write");
|
|
1453
|
+
write_root_for_test(
|
|
1454
|
+
&storage,
|
|
1455
|
+
&tracked_state,
|
|
1456
|
+
"middle",
|
|
1457
|
+
Some("base"),
|
|
1458
|
+
&[row_with_value(
|
|
1459
|
+
"entity-a",
|
|
1460
|
+
"change-middle",
|
|
1461
|
+
"middle",
|
|
1462
|
+
"middle",
|
|
1463
|
+
)],
|
|
1464
|
+
)
|
|
1465
|
+
.await
|
|
1466
|
+
.expect("middle root should write");
|
|
1467
|
+
write_root_for_test(
|
|
1468
|
+
&storage,
|
|
1469
|
+
&tracked_state,
|
|
1470
|
+
"child",
|
|
1471
|
+
Some("middle"),
|
|
1472
|
+
&[row_with_value("entity-a", "change-child", "child", "child")],
|
|
1473
|
+
)
|
|
1474
|
+
.await
|
|
1475
|
+
.expect("child root should write");
|
|
1476
|
+
{
|
|
1477
|
+
let mut writes = storage.new_write_set();
|
|
1478
|
+
for commit_id in ["middle", "child"] {
|
|
1479
|
+
writes.delete(
|
|
1480
|
+
storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
|
|
1481
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(commit_id.as_bytes())),
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
storage
|
|
1485
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1486
|
+
.expect("commit_root deletes should commit");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
let mut read = storage
|
|
1490
|
+
.begin_read(StorageReadOptions::default())
|
|
1491
|
+
.expect("read should open");
|
|
1492
|
+
let mut writes = storage.new_write_set();
|
|
1493
|
+
tracked_state
|
|
1494
|
+
.root_rebuilder(&mut read, &mut writes)
|
|
1495
|
+
.rebuild_commit_root_at("child")
|
|
1496
|
+
.await
|
|
1497
|
+
.expect("child root should repair");
|
|
1498
|
+
storage
|
|
1499
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1500
|
+
.expect("repaired root should commit");
|
|
1501
|
+
|
|
1502
|
+
let diff = tracked_state
|
|
1503
|
+
.reader(
|
|
1504
|
+
storage
|
|
1505
|
+
.begin_read(StorageReadOptions::default())
|
|
1506
|
+
.expect("read should open"),
|
|
1507
|
+
)
|
|
1508
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1509
|
+
.await
|
|
1510
|
+
.expect("diff should accept repaired nearest-ancestor parent metadata");
|
|
1511
|
+
|
|
1512
|
+
assert_eq!(diff.entries.len(), 1);
|
|
1513
|
+
assert_eq!(
|
|
1514
|
+
diff.entries[0]
|
|
1515
|
+
.after
|
|
1516
|
+
.as_ref()
|
|
1517
|
+
.map(|row| row.change_id.as_str()),
|
|
1518
|
+
Some("change-child")
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
#[tokio::test]
|
|
1523
|
+
async fn explicit_rebuild_repairs_missing_ancestor_chain() {
|
|
1524
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1525
|
+
let tracked_state = TrackedStateContext::new();
|
|
1526
|
+
write_root_for_test(
|
|
1527
|
+
&storage,
|
|
1528
|
+
&tracked_state,
|
|
1529
|
+
"base",
|
|
1530
|
+
None,
|
|
1531
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1532
|
+
)
|
|
1533
|
+
.await
|
|
1534
|
+
.expect("base root should write");
|
|
1535
|
+
write_root_for_test(
|
|
1536
|
+
&storage,
|
|
1537
|
+
&tracked_state,
|
|
1538
|
+
"middle",
|
|
1539
|
+
Some("base"),
|
|
1540
|
+
&[row_with_value(
|
|
1541
|
+
"entity-a",
|
|
1542
|
+
"change-middle",
|
|
1543
|
+
"middle",
|
|
1544
|
+
"middle",
|
|
1545
|
+
)],
|
|
1546
|
+
)
|
|
1547
|
+
.await
|
|
1548
|
+
.expect("middle root should write");
|
|
1549
|
+
write_root_for_test(
|
|
1550
|
+
&storage,
|
|
1551
|
+
&tracked_state,
|
|
1552
|
+
"child",
|
|
1553
|
+
Some("middle"),
|
|
1554
|
+
&[row_with_value("entity-a", "change-child", "child", "child")],
|
|
1555
|
+
)
|
|
1556
|
+
.await
|
|
1557
|
+
.expect("child root should write");
|
|
1558
|
+
{
|
|
1559
|
+
let mut writes = storage.new_write_set();
|
|
1560
|
+
for commit_id in ["middle", "child"] {
|
|
1561
|
+
writes.delete(
|
|
1562
|
+
storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
|
|
1563
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(commit_id.as_bytes())),
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
storage
|
|
1567
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1568
|
+
.expect("commit_root deletes should commit");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
let read = storage
|
|
1572
|
+
.begin_read(StorageReadOptions::default())
|
|
1573
|
+
.expect("read should open");
|
|
1574
|
+
let mut writes = storage.new_write_set();
|
|
1575
|
+
tracked_state
|
|
1576
|
+
.root_rebuilder(&read, &mut writes)
|
|
1577
|
+
.rebuild_commit_root_at("child")
|
|
1578
|
+
.await
|
|
1579
|
+
.expect("explicit rebuild should repair missing ancestor chain");
|
|
1580
|
+
storage
|
|
1581
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1582
|
+
.expect("repaired roots should commit");
|
|
1583
|
+
|
|
1584
|
+
let diff = tracked_state
|
|
1585
|
+
.reader(
|
|
1586
|
+
storage
|
|
1587
|
+
.begin_read(StorageReadOptions::default())
|
|
1588
|
+
.expect("read should open"),
|
|
1589
|
+
)
|
|
1590
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1591
|
+
.await
|
|
1592
|
+
.expect("diff should accept explicitly rebuilt chain");
|
|
1593
|
+
|
|
1594
|
+
assert_eq!(diff.entries.len(), 1);
|
|
1595
|
+
assert_eq!(
|
|
1596
|
+
diff.entries[0]
|
|
1597
|
+
.after
|
|
1598
|
+
.as_ref()
|
|
1599
|
+
.map(|row| row.change_id.as_str()),
|
|
1600
|
+
Some("change-child")
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
#[tokio::test]
|
|
1605
|
+
async fn explicit_rebuild_errors_on_first_parent_cycle() {
|
|
1606
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1607
|
+
let tracked_state = TrackedStateContext::new();
|
|
1608
|
+
{
|
|
1609
|
+
let mut read = storage
|
|
1610
|
+
.begin_read(StorageReadOptions::default())
|
|
1611
|
+
.expect("read should open");
|
|
1612
|
+
let mut writes = storage.new_write_set();
|
|
1613
|
+
crate::test_support::stage_empty_changelog_commit(
|
|
1614
|
+
&mut read,
|
|
1615
|
+
&mut writes,
|
|
1616
|
+
"commit-a",
|
|
1617
|
+
None,
|
|
1618
|
+
)
|
|
1619
|
+
.await
|
|
1620
|
+
.expect("commit-a should stage");
|
|
1621
|
+
storage
|
|
1622
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1623
|
+
.expect("commit-a should commit");
|
|
1624
|
+
}
|
|
1625
|
+
{
|
|
1626
|
+
let mut read = storage
|
|
1627
|
+
.begin_read(StorageReadOptions::default())
|
|
1628
|
+
.expect("read should open");
|
|
1629
|
+
let mut writes = storage.new_write_set();
|
|
1630
|
+
crate::test_support::stage_empty_changelog_commit_with_parents(
|
|
1631
|
+
&mut read,
|
|
1632
|
+
&mut writes,
|
|
1633
|
+
"commit-b",
|
|
1634
|
+
&["commit-a".to_string()],
|
|
1635
|
+
)
|
|
1636
|
+
.await
|
|
1637
|
+
.expect("commit-b should stage");
|
|
1638
|
+
storage
|
|
1639
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1640
|
+
.expect("commit-b should commit");
|
|
1641
|
+
}
|
|
1642
|
+
{
|
|
1643
|
+
let mut writes = storage.new_write_set();
|
|
1644
|
+
writes.put(
|
|
1645
|
+
crate::changelog::COMMIT_SPACE,
|
|
1646
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"commit-a")),
|
|
1647
|
+
crate::changelog::encode_commit_record(&crate::changelog::CommitRecord {
|
|
1648
|
+
format_version: 1,
|
|
1649
|
+
commit_id: "commit-a".to_string(),
|
|
1650
|
+
parent_commit_ids: vec!["commit-b".to_string()],
|
|
1651
|
+
change_id: "commit-a:commit".to_string(),
|
|
1652
|
+
author_account_ids: Vec::new(),
|
|
1653
|
+
created_at: "1970-01-01T00:00:00.000Z".to_string(),
|
|
1654
|
+
})
|
|
1655
|
+
.expect("corrupt cycle commit should encode"),
|
|
1656
|
+
);
|
|
1657
|
+
storage
|
|
1658
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1659
|
+
.expect("cycle corruption should commit");
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
let read = storage
|
|
1663
|
+
.begin_read(StorageReadOptions::default())
|
|
1664
|
+
.expect("read should open");
|
|
1665
|
+
let mut writes = storage.new_write_set();
|
|
1666
|
+
let error = tracked_state
|
|
1667
|
+
.root_rebuilder(&read, &mut writes)
|
|
1668
|
+
.rebuild_commit_root_at("commit-a")
|
|
1669
|
+
.await
|
|
1670
|
+
.expect_err("first-parent cycle should not rebuild forever");
|
|
1671
|
+
|
|
1672
|
+
assert_eq!(error.code, LixError::CODE_INTERNAL_ERROR);
|
|
1673
|
+
assert!(
|
|
1674
|
+
error.message.contains("first-parent cycle"),
|
|
1675
|
+
"unexpected error message: {}",
|
|
1676
|
+
error.message
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
#[tokio::test]
|
|
1681
|
+
async fn explicit_rebuild_repairs_missing_head_root_chunk() {
|
|
1682
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1683
|
+
let tracked_state = TrackedStateContext::new();
|
|
1684
|
+
write_root_for_test(
|
|
1685
|
+
&storage,
|
|
1686
|
+
&tracked_state,
|
|
1687
|
+
"base",
|
|
1688
|
+
None,
|
|
1689
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1690
|
+
)
|
|
1691
|
+
.await
|
|
1692
|
+
.expect("base root should write");
|
|
1693
|
+
write_root_for_test(
|
|
1694
|
+
&storage,
|
|
1695
|
+
&tracked_state,
|
|
1696
|
+
"child",
|
|
1697
|
+
Some("base"),
|
|
1698
|
+
&[row_with_value("entity-a", "change-child", "child", "child")],
|
|
1699
|
+
)
|
|
1700
|
+
.await
|
|
1701
|
+
.expect("child root should write");
|
|
1702
|
+
delete_root_chunk_for_test(&storage, "child").await;
|
|
1703
|
+
|
|
1704
|
+
tracked_state
|
|
1705
|
+
.reader(
|
|
1706
|
+
storage
|
|
1707
|
+
.begin_read(StorageReadOptions::default())
|
|
1708
|
+
.expect("read should open"),
|
|
1709
|
+
)
|
|
1710
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1711
|
+
.await
|
|
1712
|
+
.expect_err("diff should fail before missing root chunk repair");
|
|
1713
|
+
|
|
1714
|
+
let read = storage
|
|
1715
|
+
.begin_read(StorageReadOptions::default())
|
|
1716
|
+
.expect("read should open");
|
|
1717
|
+
let mut writes = storage.new_write_set();
|
|
1718
|
+
tracked_state
|
|
1719
|
+
.root_rebuilder(&read, &mut writes)
|
|
1720
|
+
.rebuild_commit_root_at("child")
|
|
1721
|
+
.await
|
|
1722
|
+
.expect("child root chunk should repair");
|
|
1723
|
+
storage
|
|
1724
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1725
|
+
.expect("repaired root should commit");
|
|
1726
|
+
|
|
1727
|
+
let diff = tracked_state
|
|
1728
|
+
.reader(
|
|
1729
|
+
storage
|
|
1730
|
+
.begin_read(StorageReadOptions::default())
|
|
1731
|
+
.expect("read should open"),
|
|
1732
|
+
)
|
|
1733
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1734
|
+
.await
|
|
1735
|
+
.expect("diff should use repaired root chunk");
|
|
1736
|
+
|
|
1737
|
+
assert_eq!(diff.entries.len(), 1);
|
|
1738
|
+
assert_eq!(
|
|
1739
|
+
diff.entries[0]
|
|
1740
|
+
.after
|
|
1741
|
+
.as_ref()
|
|
1742
|
+
.map(|row| row.change_id.as_str()),
|
|
1743
|
+
Some("change-child")
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
#[tokio::test]
|
|
1748
|
+
async fn explicit_rebuild_repairs_corrupt_head_root_chunk() {
|
|
1749
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1750
|
+
let tracked_state = TrackedStateContext::new();
|
|
1751
|
+
write_root_for_test(
|
|
1752
|
+
&storage,
|
|
1753
|
+
&tracked_state,
|
|
1754
|
+
"base",
|
|
1755
|
+
None,
|
|
1756
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1757
|
+
)
|
|
1758
|
+
.await
|
|
1759
|
+
.expect("base root should write");
|
|
1760
|
+
write_root_for_test(
|
|
1761
|
+
&storage,
|
|
1762
|
+
&tracked_state,
|
|
1763
|
+
"child",
|
|
1764
|
+
Some("base"),
|
|
1765
|
+
&[row_with_value("entity-a", "change-child", "child", "child")],
|
|
1766
|
+
)
|
|
1767
|
+
.await
|
|
1768
|
+
.expect("child root should write");
|
|
1769
|
+
corrupt_root_chunk_for_test(&storage, "child").await;
|
|
1013
1770
|
|
|
1014
|
-
let
|
|
1015
|
-
.
|
|
1016
|
-
.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1771
|
+
let read = storage
|
|
1772
|
+
.begin_read(StorageReadOptions::default())
|
|
1773
|
+
.expect("read should open");
|
|
1774
|
+
let mut writes = storage.new_write_set();
|
|
1775
|
+
tracked_state
|
|
1776
|
+
.root_rebuilder(&read, &mut writes)
|
|
1777
|
+
.rebuild_commit_root_at("child")
|
|
1778
|
+
.await
|
|
1779
|
+
.expect("corrupt child root chunk should repair");
|
|
1780
|
+
storage
|
|
1781
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1782
|
+
.expect("repaired root should commit");
|
|
1783
|
+
|
|
1784
|
+
let diff = tracked_state
|
|
1785
|
+
.reader(
|
|
1786
|
+
storage
|
|
1787
|
+
.begin_read(StorageReadOptions::default())
|
|
1788
|
+
.expect("read should open"),
|
|
1021
1789
|
)
|
|
1790
|
+
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1022
1791
|
.await
|
|
1023
|
-
.expect("
|
|
1792
|
+
.expect("diff should use repaired root chunk");
|
|
1024
1793
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1794
|
+
assert_eq!(diff.entries.len(), 1);
|
|
1795
|
+
assert_eq!(
|
|
1796
|
+
diff.entries[0]
|
|
1797
|
+
.after
|
|
1798
|
+
.as_ref()
|
|
1799
|
+
.map(|row| row.change_id.as_str()),
|
|
1800
|
+
Some("change-child")
|
|
1801
|
+
);
|
|
1027
1802
|
}
|
|
1028
1803
|
|
|
1029
1804
|
#[tokio::test]
|
|
1030
|
-
async fn
|
|
1031
|
-
let
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
&
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1805
|
+
async fn explicit_rebuild_repairs_stale_root_missing_commit_row() {
|
|
1806
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1807
|
+
let tracked_state = TrackedStateContext::new();
|
|
1808
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", "value");
|
|
1809
|
+
write_root_for_test(
|
|
1810
|
+
&storage,
|
|
1811
|
+
&tracked_state,
|
|
1812
|
+
"commit-1",
|
|
1813
|
+
None,
|
|
1814
|
+
std::slice::from_ref(&row),
|
|
1815
|
+
)
|
|
1816
|
+
.await
|
|
1817
|
+
.expect("root should write");
|
|
1818
|
+
overwrite_root_without_commit_row_for_test(
|
|
1819
|
+
&storage,
|
|
1820
|
+
"commit-1",
|
|
1821
|
+
std::slice::from_ref(&row),
|
|
1045
1822
|
)
|
|
1046
1823
|
.await;
|
|
1047
1824
|
|
|
1048
|
-
let
|
|
1049
|
-
.
|
|
1050
|
-
.
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
)
|
|
1825
|
+
let read = storage
|
|
1826
|
+
.begin_read(StorageReadOptions::default())
|
|
1827
|
+
.expect("read should open");
|
|
1828
|
+
let mut writes = storage.new_write_set();
|
|
1829
|
+
tracked_state
|
|
1830
|
+
.root_rebuilder(&read, &mut writes)
|
|
1831
|
+
.rebuild_commit_root_at("commit-1")
|
|
1056
1832
|
.await
|
|
1057
|
-
.expect("
|
|
1833
|
+
.expect("stale root should repair");
|
|
1834
|
+
storage
|
|
1835
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1836
|
+
.expect("repaired root should commit");
|
|
1058
1837
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1838
|
+
let rows = tracked_state
|
|
1839
|
+
.reader(
|
|
1840
|
+
storage
|
|
1841
|
+
.begin_read(StorageReadOptions::default())
|
|
1842
|
+
.expect("read should open"),
|
|
1843
|
+
)
|
|
1844
|
+
.scan_rows_at_commit(
|
|
1845
|
+
"commit-1",
|
|
1846
|
+
&TrackedStateScanRequest {
|
|
1847
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1848
|
+
schema_keys: vec!["lix_commit".to_string()],
|
|
1849
|
+
..Default::default()
|
|
1850
|
+
},
|
|
1851
|
+
..Default::default()
|
|
1852
|
+
},
|
|
1853
|
+
)
|
|
1854
|
+
.await
|
|
1855
|
+
.expect("repaired root should scan");
|
|
1856
|
+
assert_eq!(rows.len(), 1);
|
|
1857
|
+
assert_eq!(rows[0].schema_key, "lix_commit");
|
|
1061
1858
|
}
|
|
1062
1859
|
|
|
1063
1860
|
#[tokio::test]
|
|
1064
|
-
async fn
|
|
1065
|
-
let
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1861
|
+
async fn explicit_rebuild_repairs_stale_root_missing_inherited_row() {
|
|
1862
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1863
|
+
let tracked_state = TrackedStateContext::new();
|
|
1864
|
+
let inherited = row_with_value("entity-a", "change-base", "base", "base");
|
|
1865
|
+
let child = row_with_value("entity-b", "change-child", "child", "child");
|
|
1866
|
+
write_root_for_test(
|
|
1867
|
+
&storage,
|
|
1868
|
+
&tracked_state,
|
|
1869
|
+
"base",
|
|
1870
|
+
None,
|
|
1871
|
+
std::slice::from_ref(&inherited),
|
|
1069
1872
|
)
|
|
1070
|
-
.await
|
|
1873
|
+
.await
|
|
1874
|
+
.expect("base root should write");
|
|
1875
|
+
write_root_for_test(
|
|
1876
|
+
&storage,
|
|
1877
|
+
&tracked_state,
|
|
1878
|
+
"child",
|
|
1879
|
+
Some("base"),
|
|
1880
|
+
std::slice::from_ref(&child),
|
|
1881
|
+
)
|
|
1882
|
+
.await
|
|
1883
|
+
.expect("child root should write");
|
|
1884
|
+
overwrite_root_without_commit_row_for_test(&storage, "child", std::slice::from_ref(&child))
|
|
1885
|
+
.await;
|
|
1071
1886
|
|
|
1072
|
-
let
|
|
1073
|
-
.
|
|
1074
|
-
.
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
)
|
|
1887
|
+
let read = storage
|
|
1888
|
+
.begin_read(StorageReadOptions::default())
|
|
1889
|
+
.expect("read should open");
|
|
1890
|
+
let mut writes = storage.new_write_set();
|
|
1891
|
+
tracked_state
|
|
1892
|
+
.root_rebuilder(&read, &mut writes)
|
|
1893
|
+
.rebuild_commit_root_at("child")
|
|
1080
1894
|
.await
|
|
1081
|
-
.expect("
|
|
1895
|
+
.expect("stale child root should repair");
|
|
1896
|
+
storage
|
|
1897
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1898
|
+
.expect("repaired root should commit");
|
|
1082
1899
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1900
|
+
let rows = tracked_state
|
|
1901
|
+
.reader(
|
|
1902
|
+
storage
|
|
1903
|
+
.begin_read(StorageReadOptions::default())
|
|
1904
|
+
.expect("read should open"),
|
|
1905
|
+
)
|
|
1906
|
+
.scan_rows_at_commit("child", &test_schema_scan_request())
|
|
1907
|
+
.await
|
|
1908
|
+
.expect("repaired child root should scan");
|
|
1909
|
+
assert_eq!(
|
|
1910
|
+
rows.iter()
|
|
1911
|
+
.map(|row| row.change_id.as_str())
|
|
1912
|
+
.collect::<Vec<_>>(),
|
|
1913
|
+
vec!["change-base", "change-child"]
|
|
1914
|
+
);
|
|
1086
1915
|
}
|
|
1087
1916
|
|
|
1088
1917
|
#[tokio::test]
|
|
1089
|
-
async fn
|
|
1090
|
-
let
|
|
1091
|
-
let storage = StorageContext::new(backend.clone());
|
|
1918
|
+
async fn scan_rows_filters_by_file() {
|
|
1919
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1092
1920
|
let tracked_state = TrackedStateContext::new();
|
|
1093
1921
|
let mut file_a = row("entity-a", "change-a", "commit-1");
|
|
1094
1922
|
file_a.file_id = Some("file-a.json".to_string());
|
|
1095
1923
|
let mut file_b = row("entity-b", "change-b", "commit-1");
|
|
1096
1924
|
file_b.file_id = Some("file-b.json".to_string());
|
|
1097
|
-
|
|
1098
|
-
let mut transaction = storage
|
|
1099
|
-
.begin_write_transaction()
|
|
1100
|
-
.await
|
|
1101
|
-
.expect("transaction should open");
|
|
1102
1925
|
write_root_for_test(
|
|
1103
|
-
|
|
1926
|
+
&storage,
|
|
1104
1927
|
&tracked_state,
|
|
1105
1928
|
"commit-1",
|
|
1106
1929
|
None,
|
|
@@ -1108,13 +1931,13 @@ mod tests {
|
|
|
1108
1931
|
)
|
|
1109
1932
|
.await
|
|
1110
1933
|
.expect("root should write");
|
|
1111
|
-
transaction
|
|
1112
|
-
.commit()
|
|
1113
|
-
.await
|
|
1114
|
-
.expect("transaction should commit");
|
|
1115
1934
|
|
|
1116
1935
|
let rows = tracked_state
|
|
1117
|
-
.reader(
|
|
1936
|
+
.reader(
|
|
1937
|
+
storage
|
|
1938
|
+
.begin_read(StorageReadOptions::default())
|
|
1939
|
+
.expect("read should open"),
|
|
1940
|
+
)
|
|
1118
1941
|
.scan_rows_at_commit(
|
|
1119
1942
|
"commit-1",
|
|
1120
1943
|
&TrackedStateScanRequest {
|
|
@@ -1126,34 +1949,28 @@ mod tests {
|
|
|
1126
1949
|
},
|
|
1127
1950
|
)
|
|
1128
1951
|
.await
|
|
1129
|
-
.expect("file scan should
|
|
1952
|
+
.expect("file scan should use primary root");
|
|
1130
1953
|
|
|
1131
1954
|
assert_eq!(rows.len(), 1);
|
|
1132
1955
|
assert_eq!(
|
|
1133
1956
|
rows[0]
|
|
1134
|
-
.
|
|
1957
|
+
.entity_pk
|
|
1135
1958
|
.as_single_string_owned()
|
|
1136
|
-
.expect("entity
|
|
1959
|
+
.expect("entity pk"),
|
|
1137
1960
|
"entity-a"
|
|
1138
1961
|
);
|
|
1139
1962
|
assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
|
|
1140
1963
|
}
|
|
1141
1964
|
|
|
1142
1965
|
#[tokio::test]
|
|
1143
|
-
async fn
|
|
1144
|
-
let
|
|
1145
|
-
let storage = StorageContext::new(backend.clone());
|
|
1966
|
+
async fn file_filtered_header_scan_fetches_primary_payload_only_when_requested() {
|
|
1967
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1146
1968
|
let tracked_state = TrackedStateContext::new();
|
|
1147
1969
|
let mut row = row("entity-a", "change-a", "commit-1");
|
|
1148
1970
|
row.file_id = Some("file-a.json".to_string());
|
|
1149
1971
|
let expected_snapshot = row.snapshot_content.clone();
|
|
1150
|
-
|
|
1151
|
-
let mut transaction = storage
|
|
1152
|
-
.begin_write_transaction()
|
|
1153
|
-
.await
|
|
1154
|
-
.expect("transaction should open");
|
|
1155
1972
|
write_root_for_test(
|
|
1156
|
-
|
|
1973
|
+
&storage,
|
|
1157
1974
|
&tracked_state,
|
|
1158
1975
|
"commit-1",
|
|
1159
1976
|
None,
|
|
@@ -1161,12 +1978,12 @@ mod tests {
|
|
|
1161
1978
|
)
|
|
1162
1979
|
.await
|
|
1163
1980
|
.expect("root should write");
|
|
1164
|
-
transaction
|
|
1165
|
-
.commit()
|
|
1166
|
-
.await
|
|
1167
|
-
.expect("transaction should commit");
|
|
1168
1981
|
|
|
1169
|
-
let mut reader = tracked_state.reader(
|
|
1982
|
+
let mut reader = tracked_state.reader(
|
|
1983
|
+
storage
|
|
1984
|
+
.begin_read(StorageReadOptions::default())
|
|
1985
|
+
.expect("read should open"),
|
|
1986
|
+
);
|
|
1170
1987
|
let header_rows = reader
|
|
1171
1988
|
.scan_rows_at_commit(
|
|
1172
1989
|
"commit-1",
|
|
@@ -1175,14 +1992,14 @@ mod tests {
|
|
|
1175
1992
|
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1176
1993
|
..Default::default()
|
|
1177
1994
|
},
|
|
1178
|
-
|
|
1179
|
-
columns: vec!["
|
|
1995
|
+
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
1996
|
+
columns: vec!["entity_pk".to_string()],
|
|
1180
1997
|
},
|
|
1181
1998
|
..Default::default()
|
|
1182
1999
|
},
|
|
1183
2000
|
)
|
|
1184
2001
|
.await
|
|
1185
|
-
.expect("header scan should
|
|
2002
|
+
.expect("header scan should use primary root");
|
|
1186
2003
|
let full_rows = reader
|
|
1187
2004
|
.scan_rows_at_commit(
|
|
1188
2005
|
"commit-1",
|
|
@@ -1202,18 +2019,12 @@ mod tests {
|
|
|
1202
2019
|
}
|
|
1203
2020
|
|
|
1204
2021
|
#[tokio::test]
|
|
1205
|
-
async fn
|
|
1206
|
-
let
|
|
1207
|
-
let storage = StorageContext::new(backend.clone());
|
|
2022
|
+
async fn null_file_rows_match_null_file_filter() {
|
|
2023
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1208
2024
|
let tracked_state = TrackedStateContext::new();
|
|
1209
2025
|
let row = row("entity-a", "change-a", "commit-1");
|
|
1210
|
-
|
|
1211
|
-
let mut transaction = storage
|
|
1212
|
-
.begin_write_transaction()
|
|
1213
|
-
.await
|
|
1214
|
-
.expect("transaction should open");
|
|
1215
2026
|
write_root_for_test(
|
|
1216
|
-
|
|
2027
|
+
&storage,
|
|
1217
2028
|
&tracked_state,
|
|
1218
2029
|
"commit-1",
|
|
1219
2030
|
None,
|
|
@@ -1221,22 +2032,18 @@ mod tests {
|
|
|
1221
2032
|
)
|
|
1222
2033
|
.await
|
|
1223
2034
|
.expect("root should write");
|
|
1224
|
-
transaction
|
|
1225
|
-
.commit()
|
|
1226
|
-
.await
|
|
1227
|
-
.expect("transaction should commit");
|
|
1228
|
-
|
|
1229
|
-
let by_file_root = storage::load_by_file_root(&mut storage.clone(), "commit-1")
|
|
1230
|
-
.await
|
|
1231
|
-
.expect("by-file root lookup should load");
|
|
1232
|
-
assert!(by_file_root.is_none());
|
|
1233
2035
|
|
|
1234
2036
|
let rows = tracked_state
|
|
1235
|
-
.reader(
|
|
2037
|
+
.reader(
|
|
2038
|
+
storage
|
|
2039
|
+
.begin_read(StorageReadOptions::default())
|
|
2040
|
+
.expect("read should open"),
|
|
2041
|
+
)
|
|
1236
2042
|
.scan_rows_at_commit(
|
|
1237
2043
|
"commit-1",
|
|
1238
2044
|
&TrackedStateScanRequest {
|
|
1239
2045
|
filter: crate::tracked_state::TrackedStateFilter {
|
|
2046
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
1240
2047
|
file_ids: vec![NullableKeyFilter::Null],
|
|
1241
2048
|
..Default::default()
|
|
1242
2049
|
},
|
|
@@ -1244,33 +2051,27 @@ mod tests {
|
|
|
1244
2051
|
},
|
|
1245
2052
|
)
|
|
1246
2053
|
.await
|
|
1247
|
-
.expect("null file scan should
|
|
2054
|
+
.expect("null file scan should use primary tree");
|
|
1248
2055
|
|
|
1249
2056
|
assert_eq!(rows.len(), 1);
|
|
1250
2057
|
assert_eq!(
|
|
1251
2058
|
rows[0]
|
|
1252
|
-
.
|
|
2059
|
+
.entity_pk
|
|
1253
2060
|
.as_single_string_owned()
|
|
1254
|
-
.expect("entity
|
|
2061
|
+
.expect("entity pk"),
|
|
1255
2062
|
"entity-a"
|
|
1256
2063
|
);
|
|
1257
2064
|
}
|
|
1258
2065
|
|
|
1259
2066
|
#[tokio::test]
|
|
1260
2067
|
async fn mixed_null_and_concrete_file_scan_uses_primary_tree() {
|
|
1261
|
-
let
|
|
1262
|
-
let storage = StorageContext::new(backend.clone());
|
|
2068
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1263
2069
|
let tracked_state = TrackedStateContext::new();
|
|
1264
2070
|
let null_row = row("entity-null", "change-null", "commit-1");
|
|
1265
2071
|
let mut file_row = row("entity-file", "change-file", "commit-2");
|
|
1266
2072
|
file_row.file_id = Some("file-a.json".to_string());
|
|
1267
|
-
|
|
1268
|
-
let mut transaction = storage
|
|
1269
|
-
.begin_write_transaction()
|
|
1270
|
-
.await
|
|
1271
|
-
.expect("transaction should open");
|
|
1272
2073
|
write_root_for_test(
|
|
1273
|
-
|
|
2074
|
+
&storage,
|
|
1274
2075
|
&tracked_state,
|
|
1275
2076
|
"commit-1",
|
|
1276
2077
|
None,
|
|
@@ -1279,7 +2080,7 @@ mod tests {
|
|
|
1279
2080
|
.await
|
|
1280
2081
|
.expect("parent root should write");
|
|
1281
2082
|
write_root_for_test(
|
|
1282
|
-
|
|
2083
|
+
&storage,
|
|
1283
2084
|
&tracked_state,
|
|
1284
2085
|
"commit-2",
|
|
1285
2086
|
Some("commit-1"),
|
|
@@ -1287,17 +2088,18 @@ mod tests {
|
|
|
1287
2088
|
)
|
|
1288
2089
|
.await
|
|
1289
2090
|
.expect("child root should write");
|
|
1290
|
-
transaction
|
|
1291
|
-
.commit()
|
|
1292
|
-
.await
|
|
1293
|
-
.expect("transaction should commit");
|
|
1294
2091
|
|
|
1295
2092
|
let rows = tracked_state
|
|
1296
|
-
.reader(
|
|
2093
|
+
.reader(
|
|
2094
|
+
storage
|
|
2095
|
+
.begin_read(StorageReadOptions::default())
|
|
2096
|
+
.expect("read should open"),
|
|
2097
|
+
)
|
|
1297
2098
|
.scan_rows_at_commit(
|
|
1298
2099
|
"commit-2",
|
|
1299
2100
|
&TrackedStateScanRequest {
|
|
1300
2101
|
filter: crate::tracked_state::TrackedStateFilter {
|
|
2102
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
1301
2103
|
file_ids: vec![
|
|
1302
2104
|
NullableKeyFilter::Null,
|
|
1303
2105
|
NullableKeyFilter::Value("file-a.json".to_string()),
|
|
@@ -1310,44 +2112,32 @@ mod tests {
|
|
|
1310
2112
|
.await
|
|
1311
2113
|
.expect("mixed scan should use primary tree");
|
|
1312
2114
|
|
|
1313
|
-
let mut
|
|
2115
|
+
let mut entity_pks = rows
|
|
1314
2116
|
.iter()
|
|
1315
|
-
.map(|row| row.
|
|
2117
|
+
.map(|row| row.entity_pk.as_single_string_owned().expect("entity pk"))
|
|
1316
2118
|
.collect::<Vec<_>>();
|
|
1317
|
-
|
|
1318
|
-
assert_eq!(
|
|
2119
|
+
entity_pks.sort();
|
|
2120
|
+
assert_eq!(entity_pks, vec!["entity-file", "entity-null"]);
|
|
1319
2121
|
}
|
|
1320
2122
|
|
|
1321
2123
|
#[tokio::test]
|
|
1322
|
-
async fn
|
|
1323
|
-
let
|
|
1324
|
-
let storage = StorageContext::new(backend.clone());
|
|
2124
|
+
async fn file_filtered_header_scan_filters_tombstones_without_payload_sentinel() {
|
|
2125
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1325
2126
|
let tracked_state = TrackedStateContext::new();
|
|
1326
2127
|
let mut live = row("entity-live", "change-live", "commit-1");
|
|
1327
2128
|
live.file_id = Some("file-a.json".to_string());
|
|
1328
2129
|
let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
|
|
1329
2130
|
deleted.file_id = Some("file-a.json".to_string());
|
|
1330
|
-
|
|
1331
|
-
let mut transaction = storage
|
|
1332
|
-
.begin_write_transaction()
|
|
1333
|
-
.await
|
|
1334
|
-
.expect("transaction should open");
|
|
1335
|
-
write_root_for_test(
|
|
1336
|
-
transaction.as_mut(),
|
|
1337
|
-
&tracked_state,
|
|
1338
|
-
"commit-1",
|
|
1339
|
-
None,
|
|
1340
|
-
&[live, deleted],
|
|
1341
|
-
)
|
|
1342
|
-
.await
|
|
1343
|
-
.expect("root should write");
|
|
1344
|
-
transaction
|
|
1345
|
-
.commit()
|
|
2131
|
+
write_root_for_test(&storage, &tracked_state, "commit-1", None, &[live, deleted])
|
|
1346
2132
|
.await
|
|
1347
|
-
.expect("
|
|
2133
|
+
.expect("root should write");
|
|
1348
2134
|
|
|
1349
2135
|
let rows = tracked_state
|
|
1350
|
-
.reader(
|
|
2136
|
+
.reader(
|
|
2137
|
+
storage
|
|
2138
|
+
.begin_read(StorageReadOptions::default())
|
|
2139
|
+
.expect("read should open"),
|
|
2140
|
+
)
|
|
1351
2141
|
.scan_rows_at_commit(
|
|
1352
2142
|
"commit-1",
|
|
1353
2143
|
&TrackedStateScanRequest {
|
|
@@ -1355,107 +2145,81 @@ mod tests {
|
|
|
1355
2145
|
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1356
2146
|
..Default::default()
|
|
1357
2147
|
},
|
|
1358
|
-
|
|
1359
|
-
columns: vec!["
|
|
2148
|
+
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
2149
|
+
columns: vec!["entity_pk".to_string()],
|
|
1360
2150
|
},
|
|
1361
2151
|
..Default::default()
|
|
1362
2152
|
},
|
|
1363
2153
|
)
|
|
1364
2154
|
.await
|
|
1365
|
-
.expect("file scan should
|
|
2155
|
+
.expect("file scan should use primary root");
|
|
1366
2156
|
|
|
1367
2157
|
assert_eq!(rows.len(), 1);
|
|
1368
2158
|
assert_eq!(
|
|
1369
2159
|
rows[0]
|
|
1370
|
-
.
|
|
2160
|
+
.entity_pk
|
|
1371
2161
|
.as_single_string_owned()
|
|
1372
|
-
.expect("entity
|
|
2162
|
+
.expect("entity pk"),
|
|
1373
2163
|
"entity-live"
|
|
1374
2164
|
);
|
|
1375
2165
|
}
|
|
1376
2166
|
|
|
1377
2167
|
#[tokio::test]
|
|
1378
|
-
async fn
|
|
1379
|
-
let
|
|
1380
|
-
let storage = StorageContext::new(backend.clone());
|
|
2168
|
+
async fn child_root_tombstone_hides_materialized_base_row() {
|
|
2169
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1381
2170
|
let tracked_state = TrackedStateContext::new();
|
|
1382
2171
|
let base = row("entity-a", "change-base", "base");
|
|
1383
2172
|
let delete = tombstone("entity-a", "change-delete", "child");
|
|
1384
|
-
|
|
1385
|
-
let mut transaction = storage
|
|
1386
|
-
.begin_write_transaction()
|
|
1387
|
-
.await
|
|
1388
|
-
.expect("base transaction should open");
|
|
1389
2173
|
write_root_for_test(
|
|
1390
|
-
|
|
2174
|
+
&storage,
|
|
1391
2175
|
&tracked_state,
|
|
1392
2176
|
"base",
|
|
1393
2177
|
None,
|
|
1394
2178
|
std::slice::from_ref(&base),
|
|
1395
2179
|
)
|
|
1396
2180
|
.await
|
|
1397
|
-
.expect("base
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
.await
|
|
1403
|
-
.expect("materialize transaction should open");
|
|
1404
|
-
let mut writes = StorageWriteSet::new();
|
|
2181
|
+
.expect("base root should write");
|
|
2182
|
+
let read = storage
|
|
2183
|
+
.begin_read(StorageReadOptions::default())
|
|
2184
|
+
.expect("read should open");
|
|
2185
|
+
let mut writes = storage.new_write_set();
|
|
1405
2186
|
tracked_state
|
|
1406
|
-
.
|
|
1407
|
-
|
|
1408
|
-
&mut writes,
|
|
1409
|
-
&CommitStoreContext::new(),
|
|
1410
|
-
)
|
|
1411
|
-
.materialize_root_at("base")
|
|
1412
|
-
.await
|
|
1413
|
-
.expect("base projection root should materialize");
|
|
1414
|
-
writes
|
|
1415
|
-
.apply(transaction.as_mut())
|
|
1416
|
-
.await
|
|
1417
|
-
.expect("base root writes should apply");
|
|
1418
|
-
transaction
|
|
1419
|
-
.commit()
|
|
2187
|
+
.root_rebuilder(&read, &mut writes)
|
|
2188
|
+
.rebuild_commit_root_at("base")
|
|
1420
2189
|
.await
|
|
2190
|
+
.expect("base commit root should materialize");
|
|
2191
|
+
storage
|
|
2192
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1421
2193
|
.expect("materialized base should commit");
|
|
1422
|
-
|
|
1423
|
-
let mut transaction = storage
|
|
1424
|
-
.begin_write_transaction()
|
|
1425
|
-
.await
|
|
1426
|
-
.expect("child transaction should open");
|
|
1427
2194
|
write_root_for_test(
|
|
1428
|
-
|
|
2195
|
+
&storage,
|
|
1429
2196
|
&tracked_state,
|
|
1430
2197
|
"child",
|
|
1431
2198
|
Some("base"),
|
|
1432
2199
|
std::slice::from_ref(&delete),
|
|
1433
2200
|
)
|
|
1434
2201
|
.await
|
|
1435
|
-
.expect("child tombstone
|
|
1436
|
-
transaction.commit().await.expect("child should commit");
|
|
2202
|
+
.expect("child tombstone root should write");
|
|
1437
2203
|
|
|
1438
2204
|
let rows = tracked_state
|
|
1439
|
-
.reader(
|
|
1440
|
-
|
|
2205
|
+
.reader(
|
|
2206
|
+
storage
|
|
2207
|
+
.begin_read(StorageReadOptions::default())
|
|
2208
|
+
.expect("read should open"),
|
|
2209
|
+
)
|
|
2210
|
+
.scan_rows_at_commit("child", &test_schema_scan_request())
|
|
1441
2211
|
.await
|
|
1442
|
-
.expect("child scan should apply
|
|
2212
|
+
.expect("child scan should apply tombstone over base root");
|
|
1443
2213
|
|
|
1444
2214
|
assert!(rows.is_empty(), "pending tombstone must hide base row");
|
|
1445
2215
|
}
|
|
1446
2216
|
|
|
1447
2217
|
#[tokio::test]
|
|
1448
|
-
async fn
|
|
1449
|
-
let
|
|
1450
|
-
let storage = StorageContext::new(backend.clone());
|
|
2218
|
+
async fn root_scan_keeps_last_mutation_for_duplicate_key() {
|
|
2219
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1451
2220
|
let tracked_state = TrackedStateContext::new();
|
|
1452
|
-
|
|
1453
|
-
let mut transaction = storage
|
|
1454
|
-
.begin_write_transaction()
|
|
1455
|
-
.await
|
|
1456
|
-
.expect("transaction should open");
|
|
1457
2221
|
write_root_for_test(
|
|
1458
|
-
|
|
2222
|
+
&storage,
|
|
1459
2223
|
&tracked_state,
|
|
1460
2224
|
"commit-1",
|
|
1461
2225
|
None,
|
|
@@ -1467,23 +2231,23 @@ mod tests {
|
|
|
1467
2231
|
],
|
|
1468
2232
|
)
|
|
1469
2233
|
.await
|
|
1470
|
-
.expect("
|
|
1471
|
-
transaction
|
|
1472
|
-
.commit()
|
|
1473
|
-
.await
|
|
1474
|
-
.expect("transaction should commit");
|
|
2234
|
+
.expect("root should write");
|
|
1475
2235
|
|
|
1476
2236
|
let rows = tracked_state
|
|
1477
|
-
.reader(
|
|
1478
|
-
|
|
2237
|
+
.reader(
|
|
2238
|
+
storage
|
|
2239
|
+
.begin_read(StorageReadOptions::default())
|
|
2240
|
+
.expect("read should open"),
|
|
2241
|
+
)
|
|
2242
|
+
.scan_rows_at_commit("commit-1", &test_schema_scan_request())
|
|
1479
2243
|
.await
|
|
1480
|
-
.expect("
|
|
2244
|
+
.expect("root should scan");
|
|
1481
2245
|
|
|
1482
2246
|
assert_eq!(rows.len(), 2);
|
|
1483
2247
|
assert_eq!(
|
|
1484
2248
|
rows.iter()
|
|
1485
2249
|
.map(|row| (
|
|
1486
|
-
row.
|
|
2250
|
+
row.entity_pk.as_single_string_owned().expect("entity pk"),
|
|
1487
2251
|
row.snapshot_content.clone()
|
|
1488
2252
|
))
|
|
1489
2253
|
.collect::<Vec<_>>(),
|
|
@@ -1502,16 +2266,10 @@ mod tests {
|
|
|
1502
2266
|
|
|
1503
2267
|
#[tokio::test]
|
|
1504
2268
|
async fn scan_limit_applies_after_tombstone_visibility() {
|
|
1505
|
-
let
|
|
1506
|
-
let storage = StorageContext::new(backend.clone());
|
|
2269
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1507
2270
|
let tracked_state = TrackedStateContext::new();
|
|
1508
|
-
|
|
1509
|
-
let mut transaction = storage
|
|
1510
|
-
.begin_write_transaction()
|
|
1511
|
-
.await
|
|
1512
|
-
.expect("transaction should open");
|
|
1513
2271
|
write_root_for_test(
|
|
1514
|
-
|
|
2272
|
+
&storage,
|
|
1515
2273
|
&tracked_state,
|
|
1516
2274
|
"commit-1",
|
|
1517
2275
|
None,
|
|
@@ -1522,16 +2280,20 @@ mod tests {
|
|
|
1522
2280
|
)
|
|
1523
2281
|
.await
|
|
1524
2282
|
.expect("root should write");
|
|
1525
|
-
transaction
|
|
1526
|
-
.commit()
|
|
1527
|
-
.await
|
|
1528
|
-
.expect("transaction should commit");
|
|
1529
2283
|
|
|
1530
2284
|
let rows = tracked_state
|
|
1531
|
-
.reader(
|
|
2285
|
+
.reader(
|
|
2286
|
+
storage
|
|
2287
|
+
.begin_read(StorageReadOptions::default())
|
|
2288
|
+
.expect("read should open"),
|
|
2289
|
+
)
|
|
1532
2290
|
.scan_rows_at_commit(
|
|
1533
2291
|
"commit-1",
|
|
1534
2292
|
&TrackedStateScanRequest {
|
|
2293
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
2294
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
2295
|
+
..Default::default()
|
|
2296
|
+
},
|
|
1535
2297
|
limit: Some(1),
|
|
1536
2298
|
..Default::default()
|
|
1537
2299
|
},
|
|
@@ -1542,43 +2304,31 @@ mod tests {
|
|
|
1542
2304
|
assert_eq!(rows.len(), 1);
|
|
1543
2305
|
assert_eq!(
|
|
1544
2306
|
rows[0]
|
|
1545
|
-
.
|
|
2307
|
+
.entity_pk
|
|
1546
2308
|
.as_single_string_owned()
|
|
1547
|
-
.expect("entity
|
|
2309
|
+
.expect("entity pk"),
|
|
1548
2310
|
"entity-b"
|
|
1549
2311
|
);
|
|
1550
2312
|
}
|
|
1551
2313
|
|
|
1552
2314
|
#[tokio::test]
|
|
1553
|
-
async fn
|
|
1554
|
-
let
|
|
1555
|
-
let storage = StorageContext::new(backend.clone());
|
|
2315
|
+
async fn file_filtered_scan_limit_applies_after_tombstone_visibility() {
|
|
2316
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1556
2317
|
let tracked_state = TrackedStateContext::new();
|
|
1557
2318
|
let mut deleted = tombstone("entity-a", "change-delete", "commit-1");
|
|
1558
2319
|
deleted.file_id = Some("file-a.json".to_string());
|
|
1559
2320
|
let mut live = row("entity-b", "change-live", "commit-1");
|
|
1560
2321
|
live.file_id = Some("file-a.json".to_string());
|
|
1561
|
-
|
|
1562
|
-
let mut transaction = storage
|
|
1563
|
-
.begin_write_transaction()
|
|
1564
|
-
.await
|
|
1565
|
-
.expect("transaction should open");
|
|
1566
|
-
write_root_for_test(
|
|
1567
|
-
transaction.as_mut(),
|
|
1568
|
-
&tracked_state,
|
|
1569
|
-
"commit-1",
|
|
1570
|
-
None,
|
|
1571
|
-
&[deleted, live],
|
|
1572
|
-
)
|
|
1573
|
-
.await
|
|
1574
|
-
.expect("root should write");
|
|
1575
|
-
transaction
|
|
1576
|
-
.commit()
|
|
2322
|
+
write_root_for_test(&storage, &tracked_state, "commit-1", None, &[deleted, live])
|
|
1577
2323
|
.await
|
|
1578
|
-
.expect("
|
|
2324
|
+
.expect("root should write");
|
|
1579
2325
|
|
|
1580
2326
|
let rows = tracked_state
|
|
1581
|
-
.reader(
|
|
2327
|
+
.reader(
|
|
2328
|
+
storage
|
|
2329
|
+
.begin_read(StorageReadOptions::default())
|
|
2330
|
+
.expect("read should open"),
|
|
2331
|
+
)
|
|
1582
2332
|
.scan_rows_at_commit(
|
|
1583
2333
|
"commit-1",
|
|
1584
2334
|
&TrackedStateScanRequest {
|
|
@@ -1586,39 +2336,33 @@ mod tests {
|
|
|
1586
2336
|
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1587
2337
|
..Default::default()
|
|
1588
2338
|
},
|
|
1589
|
-
|
|
1590
|
-
columns: vec!["
|
|
2339
|
+
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
2340
|
+
columns: vec!["entity_pk".to_string()],
|
|
1591
2341
|
},
|
|
1592
2342
|
limit: Some(1),
|
|
1593
2343
|
},
|
|
1594
2344
|
)
|
|
1595
2345
|
.await
|
|
1596
|
-
.expect("limited
|
|
2346
|
+
.expect("limited file scan should apply visibility before limit");
|
|
1597
2347
|
|
|
1598
2348
|
assert_eq!(rows.len(), 1);
|
|
1599
2349
|
assert_eq!(
|
|
1600
2350
|
rows[0]
|
|
1601
|
-
.
|
|
2351
|
+
.entity_pk
|
|
1602
2352
|
.as_single_string_owned()
|
|
1603
|
-
.expect("entity
|
|
2353
|
+
.expect("entity pk"),
|
|
1604
2354
|
"entity-b"
|
|
1605
2355
|
);
|
|
1606
2356
|
}
|
|
1607
2357
|
|
|
1608
2358
|
#[tokio::test]
|
|
1609
2359
|
async fn reads_resolve_json_snapshot_refs() {
|
|
1610
|
-
let
|
|
1611
|
-
let storage = StorageContext::new(backend.clone());
|
|
2360
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1612
2361
|
let tracked_state = TrackedStateContext::new();
|
|
1613
2362
|
let large_value = "x".repeat(1536);
|
|
1614
2363
|
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
1615
|
-
|
|
1616
|
-
let mut transaction = storage
|
|
1617
|
-
.begin_write_transaction()
|
|
1618
|
-
.await
|
|
1619
|
-
.expect("transaction should open");
|
|
1620
2364
|
write_root_for_test(
|
|
1621
|
-
|
|
2365
|
+
&storage,
|
|
1622
2366
|
&tracked_state,
|
|
1623
2367
|
"commit-1",
|
|
1624
2368
|
None,
|
|
@@ -1626,19 +2370,19 @@ mod tests {
|
|
|
1626
2370
|
)
|
|
1627
2371
|
.await
|
|
1628
2372
|
.expect("root should write");
|
|
1629
|
-
transaction
|
|
1630
|
-
.commit()
|
|
1631
|
-
.await
|
|
1632
|
-
.expect("transaction should commit");
|
|
1633
2373
|
|
|
1634
|
-
let mut reader = tracked_state.reader(
|
|
2374
|
+
let mut reader = tracked_state.reader(
|
|
2375
|
+
storage
|
|
2376
|
+
.begin_read(StorageReadOptions::default())
|
|
2377
|
+
.expect("read should open"),
|
|
2378
|
+
);
|
|
1635
2379
|
let loaded = reader
|
|
1636
2380
|
.load_rows_at_commit(
|
|
1637
2381
|
"commit-1",
|
|
1638
|
-
&[
|
|
2382
|
+
&[TrackedStateKey {
|
|
1639
2383
|
schema_key: row.schema_key.clone(),
|
|
1640
|
-
|
|
1641
|
-
file_id:
|
|
2384
|
+
entity_pk: row.entity_pk.clone(),
|
|
2385
|
+
file_id: None,
|
|
1642
2386
|
}],
|
|
1643
2387
|
)
|
|
1644
2388
|
.await
|
|
@@ -1647,7 +2391,7 @@ mod tests {
|
|
|
1647
2391
|
.flatten()
|
|
1648
2392
|
.expect("row should exist");
|
|
1649
2393
|
let scanned = reader
|
|
1650
|
-
.scan_rows_at_commit("commit-1", &
|
|
2394
|
+
.scan_rows_at_commit("commit-1", &test_schema_scan_request())
|
|
1651
2395
|
.await
|
|
1652
2396
|
.expect("rows should scan");
|
|
1653
2397
|
|
|
@@ -1656,20 +2400,14 @@ mod tests {
|
|
|
1656
2400
|
}
|
|
1657
2401
|
|
|
1658
2402
|
#[tokio::test]
|
|
1659
|
-
async fn
|
|
1660
|
-
let
|
|
1661
|
-
let storage = StorageContext::new(backend.clone());
|
|
2403
|
+
async fn commit_root_cache_uses_seen_updated_at_not_change_created_at() {
|
|
2404
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1662
2405
|
let tracked_state = TrackedStateContext::new();
|
|
1663
2406
|
let mut row = row("entity-a", "change-a", "commit-1");
|
|
1664
2407
|
row.created_at = "2026-01-01T00:00:00Z".to_string();
|
|
1665
2408
|
row.updated_at = "2026-01-02T00:00:00Z".to_string();
|
|
1666
|
-
|
|
1667
|
-
let mut transaction = storage
|
|
1668
|
-
.begin_write_transaction()
|
|
1669
|
-
.await
|
|
1670
|
-
.expect("transaction should open");
|
|
1671
2409
|
write_root_for_test(
|
|
1672
|
-
|
|
2410
|
+
&storage,
|
|
1673
2411
|
&tracked_state,
|
|
1674
2412
|
"commit-1",
|
|
1675
2413
|
None,
|
|
@@ -1677,19 +2415,19 @@ mod tests {
|
|
|
1677
2415
|
)
|
|
1678
2416
|
.await
|
|
1679
2417
|
.expect("root should write");
|
|
1680
|
-
transaction
|
|
1681
|
-
.commit()
|
|
1682
|
-
.await
|
|
1683
|
-
.expect("transaction should commit");
|
|
1684
2418
|
|
|
1685
2419
|
let loaded = tracked_state
|
|
1686
|
-
.reader(
|
|
2420
|
+
.reader(
|
|
2421
|
+
storage
|
|
2422
|
+
.begin_read(StorageReadOptions::default())
|
|
2423
|
+
.expect("read should open"),
|
|
2424
|
+
)
|
|
1687
2425
|
.load_rows_at_commit(
|
|
1688
2426
|
"commit-1",
|
|
1689
|
-
&[
|
|
2427
|
+
&[TrackedStateKey {
|
|
1690
2428
|
schema_key: row.schema_key.clone(),
|
|
1691
|
-
|
|
1692
|
-
file_id:
|
|
2429
|
+
entity_pk: row.entity_pk.clone(),
|
|
2430
|
+
file_id: None,
|
|
1693
2431
|
}],
|
|
1694
2432
|
)
|
|
1695
2433
|
.await
|
|
@@ -1703,19 +2441,104 @@ mod tests {
|
|
|
1703
2441
|
}
|
|
1704
2442
|
|
|
1705
2443
|
#[tokio::test]
|
|
1706
|
-
async fn
|
|
1707
|
-
let
|
|
1708
|
-
let storage = StorageContext::new(backend.clone());
|
|
2444
|
+
async fn updates_preserve_first_visible_created_at_across_rebuild() {
|
|
2445
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1709
2446
|
let tracked_state = TrackedStateContext::new();
|
|
1710
|
-
let
|
|
1711
|
-
|
|
2447
|
+
let mut parent = row("entity-a", "change-parent", "parent");
|
|
2448
|
+
parent.created_at = "2026-01-01T00:00:00Z".to_string();
|
|
2449
|
+
parent.updated_at = "2026-01-01T00:00:00Z".to_string();
|
|
2450
|
+
write_root_for_test(
|
|
2451
|
+
&storage,
|
|
2452
|
+
&tracked_state,
|
|
2453
|
+
"parent",
|
|
2454
|
+
None,
|
|
2455
|
+
std::slice::from_ref(&parent),
|
|
2456
|
+
)
|
|
2457
|
+
.await
|
|
2458
|
+
.expect("parent root should write");
|
|
2459
|
+
|
|
2460
|
+
let mut child = row("entity-a", "change-child", "child");
|
|
2461
|
+
child.created_at = "2026-01-02T00:00:00Z".to_string();
|
|
2462
|
+
child.updated_at = "2026-01-03T00:00:00Z".to_string();
|
|
2463
|
+
write_root_for_test(
|
|
2464
|
+
&storage,
|
|
2465
|
+
&tracked_state,
|
|
2466
|
+
"child",
|
|
2467
|
+
Some("parent"),
|
|
2468
|
+
std::slice::from_ref(&child),
|
|
2469
|
+
)
|
|
2470
|
+
.await
|
|
2471
|
+
.expect("child root should write");
|
|
2472
|
+
|
|
2473
|
+
let key = TrackedStateKey {
|
|
2474
|
+
schema_key: child.schema_key.clone(),
|
|
2475
|
+
file_id: child.file_id.clone(),
|
|
2476
|
+
entity_pk: child.entity_pk.clone(),
|
|
2477
|
+
};
|
|
2478
|
+
let loaded = tracked_state
|
|
2479
|
+
.reader(
|
|
2480
|
+
storage
|
|
2481
|
+
.begin_read(StorageReadOptions::default())
|
|
2482
|
+
.expect("read should open"),
|
|
2483
|
+
)
|
|
2484
|
+
.load_rows_at_commit("child", std::slice::from_ref(&key))
|
|
2485
|
+
.await
|
|
2486
|
+
.expect("child row should load")
|
|
2487
|
+
.pop()
|
|
2488
|
+
.flatten()
|
|
2489
|
+
.expect("child row should exist");
|
|
2490
|
+
assert_eq!(loaded.created_at, "2026-01-01T00:00:00Z");
|
|
2491
|
+
assert_eq!(loaded.updated_at, "2026-01-03T00:00:00Z");
|
|
2492
|
+
|
|
2493
|
+
{
|
|
2494
|
+
let mut writes = storage.new_write_set();
|
|
2495
|
+
writes.delete(
|
|
2496
|
+
storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
|
|
2497
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"child")),
|
|
2498
|
+
);
|
|
2499
|
+
storage
|
|
2500
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2501
|
+
.expect("child root delete should commit");
|
|
2502
|
+
}
|
|
2503
|
+
{
|
|
2504
|
+
let read = storage
|
|
2505
|
+
.begin_read(StorageReadOptions::default())
|
|
2506
|
+
.expect("read should open");
|
|
2507
|
+
let mut writes = storage.new_write_set();
|
|
2508
|
+
tracked_state
|
|
2509
|
+
.root_rebuilder(&read, &mut writes)
|
|
2510
|
+
.rebuild_commit_root_at("child")
|
|
2511
|
+
.await
|
|
2512
|
+
.expect("child root should rebuild");
|
|
2513
|
+
storage
|
|
2514
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2515
|
+
.expect("rebuilt child root should commit");
|
|
2516
|
+
}
|
|
1712
2517
|
|
|
1713
|
-
let
|
|
1714
|
-
.
|
|
2518
|
+
let rebuilt = tracked_state
|
|
2519
|
+
.reader(
|
|
2520
|
+
storage
|
|
2521
|
+
.begin_read(StorageReadOptions::default())
|
|
2522
|
+
.expect("read should open"),
|
|
2523
|
+
)
|
|
2524
|
+
.load_rows_at_commit("child", &[key])
|
|
1715
2525
|
.await
|
|
1716
|
-
.expect("
|
|
2526
|
+
.expect("rebuilt child row should load")
|
|
2527
|
+
.pop()
|
|
2528
|
+
.flatten()
|
|
2529
|
+
.expect("rebuilt child row should exist");
|
|
2530
|
+
assert_eq!(rebuilt.created_at, "2026-01-01T00:00:00Z");
|
|
2531
|
+
assert_eq!(rebuilt.updated_at, "2026-01-03T00:00:00Z");
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
#[tokio::test]
|
|
2535
|
+
async fn selected_column_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
|
|
2536
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2537
|
+
let tracked_state = TrackedStateContext::new();
|
|
2538
|
+
let large_value = "x".repeat(1536);
|
|
2539
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
1717
2540
|
write_root_for_test(
|
|
1718
|
-
|
|
2541
|
+
&storage,
|
|
1719
2542
|
&tracked_state,
|
|
1720
2543
|
"commit-1",
|
|
1721
2544
|
None,
|
|
@@ -1723,18 +2546,22 @@ mod tests {
|
|
|
1723
2546
|
)
|
|
1724
2547
|
.await
|
|
1725
2548
|
.expect("root should write");
|
|
1726
|
-
transaction
|
|
1727
|
-
.commit()
|
|
1728
|
-
.await
|
|
1729
|
-
.expect("transaction should commit");
|
|
1730
2549
|
|
|
1731
2550
|
let rows = tracked_state
|
|
1732
|
-
.reader(
|
|
2551
|
+
.reader(
|
|
2552
|
+
storage
|
|
2553
|
+
.begin_read(StorageReadOptions::default())
|
|
2554
|
+
.expect("read should open"),
|
|
2555
|
+
)
|
|
1733
2556
|
.scan_rows_at_commit(
|
|
1734
2557
|
"commit-1",
|
|
1735
2558
|
&TrackedStateScanRequest {
|
|
1736
|
-
|
|
1737
|
-
|
|
2559
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
2560
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
2561
|
+
..Default::default()
|
|
2562
|
+
},
|
|
2563
|
+
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
2564
|
+
columns: vec!["entity_pk".to_string()],
|
|
1738
2565
|
},
|
|
1739
2566
|
..Default::default()
|
|
1740
2567
|
},
|
|
@@ -1751,54 +2578,39 @@ mod tests {
|
|
|
1751
2578
|
target_rows: &[MaterializedTrackedStateRow],
|
|
1752
2579
|
source_rows: &[MaterializedTrackedStateRow],
|
|
1753
2580
|
) -> (StorageContext, TrackedStateContext) {
|
|
1754
|
-
let
|
|
1755
|
-
let storage = StorageContext::new(backend.clone());
|
|
2581
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1756
2582
|
let tracked_state = TrackedStateContext::new();
|
|
1757
|
-
|
|
1758
|
-
.begin_write_transaction()
|
|
2583
|
+
write_root_for_test(&storage, &tracked_state, "base", None, base_rows)
|
|
1759
2584
|
.await
|
|
1760
|
-
.expect("
|
|
1761
|
-
write_root_for_test(
|
|
1762
|
-
transaction.as_mut(),
|
|
1763
|
-
&tracked_state,
|
|
1764
|
-
"base",
|
|
1765
|
-
None,
|
|
1766
|
-
base_rows,
|
|
1767
|
-
)
|
|
1768
|
-
.await
|
|
1769
|
-
.expect("base root should write");
|
|
2585
|
+
.expect("base root should write");
|
|
1770
2586
|
write_root_for_test(
|
|
1771
|
-
|
|
2587
|
+
&storage,
|
|
1772
2588
|
&tracked_state,
|
|
1773
2589
|
"target",
|
|
1774
|
-
|
|
2590
|
+
Some("base"),
|
|
1775
2591
|
target_rows,
|
|
1776
2592
|
)
|
|
1777
2593
|
.await
|
|
1778
2594
|
.expect("target root should write");
|
|
1779
2595
|
write_root_for_test(
|
|
1780
|
-
|
|
2596
|
+
&storage,
|
|
1781
2597
|
&tracked_state,
|
|
1782
2598
|
"source",
|
|
1783
|
-
|
|
2599
|
+
Some("base"),
|
|
1784
2600
|
source_rows,
|
|
1785
2601
|
)
|
|
1786
2602
|
.await
|
|
1787
2603
|
.expect("source root should write");
|
|
1788
|
-
transaction
|
|
1789
|
-
.commit()
|
|
1790
|
-
.await
|
|
1791
|
-
.expect("transaction should commit");
|
|
1792
2604
|
(storage, tracked_state)
|
|
1793
2605
|
}
|
|
1794
2606
|
|
|
1795
|
-
fn
|
|
1796
|
-
plan.
|
|
2607
|
+
fn merge_pick_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
2608
|
+
plan.picks
|
|
1797
2609
|
.iter()
|
|
1798
2610
|
.map(|entry| {
|
|
1799
2611
|
entry
|
|
1800
2612
|
.identity()
|
|
1801
|
-
.
|
|
2613
|
+
.entity_pk
|
|
1802
2614
|
.as_single_string_owned()
|
|
1803
2615
|
.expect("identity")
|
|
1804
2616
|
})
|
|
@@ -1811,7 +2623,7 @@ mod tests {
|
|
|
1811
2623
|
.map(|entry| {
|
|
1812
2624
|
entry
|
|
1813
2625
|
.identity
|
|
1814
|
-
.
|
|
2626
|
+
.entity_pk
|
|
1815
2627
|
.as_single_string_owned()
|
|
1816
2628
|
.expect("identity")
|
|
1817
2629
|
})
|
|
@@ -1819,40 +2631,162 @@ mod tests {
|
|
|
1819
2631
|
}
|
|
1820
2632
|
|
|
1821
2633
|
async fn write_root_for_test(
|
|
1822
|
-
|
|
2634
|
+
storage: &StorageContext,
|
|
1823
2635
|
tracked_state: &TrackedStateContext,
|
|
1824
2636
|
commit_id: &str,
|
|
1825
2637
|
parent_commit_id: Option<&str>,
|
|
1826
2638
|
rows: &[MaterializedTrackedStateRow],
|
|
1827
2639
|
) -> Result<(), LixError> {
|
|
2640
|
+
let mut read = storage
|
|
2641
|
+
.begin_read(StorageReadOptions::default())
|
|
2642
|
+
.expect("read should open");
|
|
2643
|
+
let mut writes = storage.new_write_set();
|
|
1828
2644
|
crate::test_support::stage_tracked_root_from_materialized(
|
|
1829
|
-
|
|
2645
|
+
&mut read,
|
|
2646
|
+
&mut writes,
|
|
1830
2647
|
tracked_state,
|
|
1831
2648
|
commit_id,
|
|
1832
2649
|
parent_commit_id,
|
|
1833
2650
|
rows,
|
|
1834
2651
|
)
|
|
1835
|
-
.await
|
|
2652
|
+
.await?;
|
|
2653
|
+
storage.commit_write_set(writes, StorageWriteOptions::default())?;
|
|
2654
|
+
Ok(())
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
async fn delete_root_chunk_for_test(storage: &StorageContext, commit_id: &str) {
|
|
2658
|
+
let read = storage
|
|
2659
|
+
.begin_read(StorageReadOptions::default())
|
|
2660
|
+
.expect("read should open");
|
|
2661
|
+
let root_id = storage::load_root(&read, commit_id)
|
|
2662
|
+
.await
|
|
2663
|
+
.expect("root metadata should load")
|
|
2664
|
+
.expect("root metadata should exist");
|
|
2665
|
+
let mut writes = storage.new_write_set();
|
|
2666
|
+
writes.delete(
|
|
2667
|
+
storage::TRACKED_STATE_TREE_CHUNK_SPACE,
|
|
2668
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(root_id.as_bytes())),
|
|
2669
|
+
);
|
|
2670
|
+
storage
|
|
2671
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2672
|
+
.expect("root chunk delete should commit");
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
async fn corrupt_root_chunk_for_test(storage: &StorageContext, commit_id: &str) {
|
|
2676
|
+
let read = storage
|
|
2677
|
+
.begin_read(StorageReadOptions::default())
|
|
2678
|
+
.expect("read should open");
|
|
2679
|
+
let root_id = storage::load_root(&read, commit_id)
|
|
2680
|
+
.await
|
|
2681
|
+
.expect("root metadata should load")
|
|
2682
|
+
.expect("root metadata should exist");
|
|
2683
|
+
let mut writes = storage.new_write_set();
|
|
2684
|
+
writes.put(
|
|
2685
|
+
storage::TRACKED_STATE_TREE_CHUNK_SPACE,
|
|
2686
|
+
crate::storage::StorageKey(bytes::Bytes::copy_from_slice(root_id.as_bytes())),
|
|
2687
|
+
b"corrupt tracked-state root chunk".as_slice(),
|
|
2688
|
+
);
|
|
2689
|
+
storage
|
|
2690
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2691
|
+
.expect("root chunk corruption should commit");
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
async fn overwrite_root_without_commit_row_for_test(
|
|
2695
|
+
storage: &StorageContext,
|
|
2696
|
+
commit_id: &str,
|
|
2697
|
+
rows: &[MaterializedTrackedStateRow],
|
|
2698
|
+
) {
|
|
2699
|
+
let read = storage
|
|
2700
|
+
.begin_read(StorageReadOptions::default())
|
|
2701
|
+
.expect("read should open");
|
|
2702
|
+
let mut writes = storage.new_write_set();
|
|
2703
|
+
let mutations =
|
|
2704
|
+
rows.iter()
|
|
2705
|
+
.map(|row| {
|
|
2706
|
+
let key = TrackedStateKey {
|
|
2707
|
+
schema_key: row.schema_key.clone(),
|
|
2708
|
+
file_id: row.file_id.clone(),
|
|
2709
|
+
entity_pk: row.entity_pk.clone(),
|
|
2710
|
+
};
|
|
2711
|
+
let value = TrackedStateIndexValue {
|
|
2712
|
+
change_id: row.change_id.clone(),
|
|
2713
|
+
commit_id: row.commit_id.clone(),
|
|
2714
|
+
deleted: row.deleted,
|
|
2715
|
+
snapshot_ref: row.snapshot_content.as_ref().map(|content| {
|
|
2716
|
+
crate::json_store::JsonRef::for_content(content.as_bytes())
|
|
2717
|
+
}),
|
|
2718
|
+
metadata_ref: row.metadata.as_ref().map(|metadata| {
|
|
2719
|
+
crate::json_store::JsonRef::for_content(metadata.as_bytes())
|
|
2720
|
+
}),
|
|
2721
|
+
created_at: row.created_at.clone(),
|
|
2722
|
+
updated_at: row.updated_at.clone(),
|
|
2723
|
+
};
|
|
2724
|
+
TrackedStateMutation::put_encoded(
|
|
2725
|
+
crate::tracked_state::codec::encode_key(&key),
|
|
2726
|
+
crate::tracked_state::codec::encode_value(&value),
|
|
2727
|
+
)
|
|
2728
|
+
})
|
|
2729
|
+
.collect::<Vec<_>>();
|
|
2730
|
+
let result = TrackedStateTree::new()
|
|
2731
|
+
.apply_mutations(&read, &mut writes, None, mutations, Some(commit_id))
|
|
2732
|
+
.await
|
|
2733
|
+
.expect("stale root should write");
|
|
2734
|
+
storage::stage_commit_root(
|
|
2735
|
+
&mut writes,
|
|
2736
|
+
&TrackedStateCommitRoot {
|
|
2737
|
+
commit_id: commit_id.to_string(),
|
|
2738
|
+
root_id: result.root_id,
|
|
2739
|
+
parent_roots: Vec::new(),
|
|
2740
|
+
changed_key_count: rows.len() as u64,
|
|
2741
|
+
row_count_estimate: result.row_count as u64,
|
|
2742
|
+
tree_height: result.tree_height as u32,
|
|
2743
|
+
primary_chunk_count: result.chunk_count as u64,
|
|
2744
|
+
primary_chunk_bytes: result.chunk_bytes as u64,
|
|
2745
|
+
},
|
|
2746
|
+
)
|
|
2747
|
+
.expect("stale metadata should encode");
|
|
2748
|
+
storage
|
|
2749
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2750
|
+
.expect("stale root overwrite should commit");
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
fn test_schema_scan_request() -> TrackedStateScanRequest {
|
|
2754
|
+
TrackedStateScanRequest {
|
|
2755
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
2756
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
2757
|
+
..Default::default()
|
|
2758
|
+
},
|
|
2759
|
+
..Default::default()
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
fn test_schema_diff_request() -> TrackedStateDiffRequest {
|
|
2764
|
+
TrackedStateDiffRequest {
|
|
2765
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
2766
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
2767
|
+
..Default::default()
|
|
2768
|
+
},
|
|
2769
|
+
}
|
|
1836
2770
|
}
|
|
1837
2771
|
|
|
1838
|
-
fn tombstone(
|
|
1839
|
-
let mut row = row(
|
|
2772
|
+
fn tombstone(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
2773
|
+
let mut row = row(entity_pk, change_id, commit_id);
|
|
1840
2774
|
row.snapshot_content = None;
|
|
1841
2775
|
row
|
|
1842
2776
|
}
|
|
1843
2777
|
|
|
1844
|
-
fn row(
|
|
1845
|
-
row_with_value(
|
|
2778
|
+
fn row(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
2779
|
+
row_with_value(entity_pk, change_id, commit_id, "value")
|
|
1846
2780
|
}
|
|
1847
2781
|
|
|
1848
2782
|
fn row_with_value(
|
|
1849
|
-
|
|
2783
|
+
entity_pk: &str,
|
|
1850
2784
|
change_id: &str,
|
|
1851
2785
|
commit_id: &str,
|
|
1852
2786
|
value: &str,
|
|
1853
2787
|
) -> MaterializedTrackedStateRow {
|
|
1854
2788
|
MaterializedTrackedStateRow {
|
|
1855
|
-
|
|
2789
|
+
entity_pk: crate::entity_pk::EntityPk::single(entity_pk),
|
|
1856
2790
|
schema_key: "test_schema".to_string(),
|
|
1857
2791
|
file_id: None,
|
|
1858
2792
|
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|