@lix-js/sdk 0.6.0-preview.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -4
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -3
- package/dist/native.d.ts +1 -0
- package/dist/native.js +47 -0
- package/dist/open-lix.d.ts +38 -207
- package/dist/open-lix.js +59 -284
- package/dist/result.d.ts +18 -0
- package/dist/result.js +48 -0
- package/dist/types.d.ts +114 -1
- package/dist/value.d.ts +28 -0
- package/dist/value.js +245 -0
- package/package.json +38 -71
- package/SKILL.md +0 -507
- package/dist/builtin-schemas.d.ts +0 -1
- package/dist/builtin-schemas.js +0 -1
- package/dist/engine-wasm/index.d.ts +0 -87
- package/dist/engine-wasm/index.js +0 -339
- package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
- package/dist/engine-wasm/wasm/lix_engine.js +0 -833
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
- package/dist/generated/builtin-schemas.d.ts +0 -427
- package/dist/generated/builtin-schemas.js +0 -643
- package/dist/sqlite/index.d.ts +0 -12
- package/dist/sqlite/index.js +0 -359
- package/dist-engine-src/README.md +0 -18
- package/dist-engine-src/src/backend/capabilities.rs +0 -67
- package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
- package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
- package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
- package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
- package/dist-engine-src/src/backend/conformance/model.rs +0 -28
- package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
- package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
- package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
- package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
- package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
- package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
- package/dist-engine-src/src/backend/conformance/write.rs +0 -16
- package/dist-engine-src/src/backend/error.rs +0 -94
- package/dist-engine-src/src/backend/in_memory.rs +0 -670
- package/dist-engine-src/src/backend/mod.rs +0 -39
- package/dist-engine-src/src/backend/predicate.rs +0 -80
- package/dist-engine-src/src/backend/traits.rs +0 -260
- package/dist-engine-src/src/backend/types.rs +0 -239
- package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
- package/dist-engine-src/src/binary_cas/codec.rs +0 -346
- package/dist-engine-src/src/binary_cas/context.rs +0 -139
- package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
- package/dist-engine-src/src/binary_cas/mod.rs +0 -11
- package/dist-engine-src/src/binary_cas/types.rs +0 -121
- package/dist-engine-src/src/branch/context.rs +0 -40
- package/dist-engine-src/src/branch/lifecycle.rs +0 -221
- package/dist-engine-src/src/branch/mod.rs +0 -13
- package/dist-engine-src/src/branch/refs.rs +0 -321
- package/dist-engine-src/src/branch/stage_rows.rs +0 -67
- package/dist-engine-src/src/branch/types.rs +0 -21
- package/dist-engine-src/src/catalog/context.rs +0 -412
- package/dist-engine-src/src/catalog/mod.rs +0 -10
- package/dist-engine-src/src/catalog/schema.rs +0 -4
- package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
- package/dist-engine-src/src/cel/context.rs +0 -86
- package/dist-engine-src/src/cel/error.rs +0 -19
- package/dist-engine-src/src/cel/mod.rs +0 -8
- package/dist-engine-src/src/cel/provider.rs +0 -9
- package/dist-engine-src/src/cel/runtime.rs +0 -167
- package/dist-engine-src/src/cel/value.rs +0 -50
- package/dist-engine-src/src/changelog/bench_support.rs +0 -785
- package/dist-engine-src/src/changelog/change.rs +0 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -497
- package/dist-engine-src/src/changelog/commit.rs +0 -1
- package/dist-engine-src/src/changelog/context.rs +0 -1614
- package/dist-engine-src/src/changelog/mod.rs +0 -29
- package/dist-engine-src/src/changelog/store.rs +0 -163
- package/dist-engine-src/src/changelog/test_support.rs +0 -54
- package/dist-engine-src/src/changelog/types.rs +0 -213
- package/dist-engine-src/src/commit_graph/context.rs +0 -944
- package/dist-engine-src/src/commit_graph/mod.rs +0 -9
- package/dist-engine-src/src/commit_graph/types.rs +0 -89
- package/dist-engine-src/src/commit_graph/walker.rs +0 -786
- package/dist-engine-src/src/common/error.rs +0 -347
- package/dist-engine-src/src/common/fingerprint.rs +0 -3
- package/dist-engine-src/src/common/fs_path.rs +0 -1336
- package/dist-engine-src/src/common/identity.rs +0 -145
- package/dist-engine-src/src/common/json_pointer.rs +0 -67
- package/dist-engine-src/src/common/metadata.rs +0 -40
- package/dist-engine-src/src/common/mod.rs +0 -23
- package/dist-engine-src/src/common/types.rs +0 -105
- package/dist-engine-src/src/common/wire.rs +0 -222
- package/dist-engine-src/src/domain.rs +0 -320
- package/dist-engine-src/src/engine.rs +0 -203
- package/dist-engine-src/src/entity_pk.rs +0 -402
- package/dist-engine-src/src/functions/context.rs +0 -296
- package/dist-engine-src/src/functions/deterministic.rs +0 -113
- package/dist-engine-src/src/functions/mod.rs +0 -18
- package/dist-engine-src/src/functions/provider.rs +0 -130
- package/dist-engine-src/src/functions/state.rs +0 -335
- package/dist-engine-src/src/functions/types.rs +0 -37
- package/dist-engine-src/src/init.rs +0 -692
- package/dist-engine-src/src/json_store/compression.rs +0 -77
- package/dist-engine-src/src/json_store/context.rs +0 -172
- package/dist-engine-src/src/json_store/encoded.rs +0 -15
- package/dist-engine-src/src/json_store/mod.rs +0 -38
- package/dist-engine-src/src/json_store/store.rs +0 -494
- package/dist-engine-src/src/json_store/types.rs +0 -212
- package/dist-engine-src/src/lib.rs +0 -92
- package/dist-engine-src/src/live_state/context.rs +0 -1883
- package/dist-engine-src/src/live_state/mod.rs +0 -21
- package/dist-engine-src/src/live_state/overlay.rs +0 -75
- package/dist-engine-src/src/live_state/reader.rs +0 -23
- package/dist-engine-src/src/live_state/types.rs +0 -231
- package/dist-engine-src/src/live_state/visibility.rs +0 -666
- package/dist-engine-src/src/plugin/archive.rs +0 -438
- package/dist-engine-src/src/plugin/component.rs +0 -183
- package/dist-engine-src/src/plugin/install.rs +0 -619
- package/dist-engine-src/src/plugin/manifest.rs +0 -516
- package/dist-engine-src/src/plugin/materializer.rs +0 -202
- package/dist-engine-src/src/plugin/mod.rs +0 -33
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
- package/dist-engine-src/src/plugin/storage.rs +0 -74
- package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
- package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
- package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
- package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
- package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
- package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
- package/dist-engine-src/src/schema/compatibility.rs +0 -787
- package/dist-engine-src/src/schema/definition.json +0 -187
- package/dist-engine-src/src/schema/definition.rs +0 -742
- package/dist-engine-src/src/schema/key.rs +0 -138
- package/dist-engine-src/src/schema/mod.rs +0 -20
- package/dist-engine-src/src/schema/seed.rs +0 -14
- package/dist-engine-src/src/schema/tests.rs +0 -780
- package/dist-engine-src/src/session/context.rs +0 -1059
- package/dist-engine-src/src/session/create_branch.rs +0 -94
- package/dist-engine-src/src/session/execute.rs +0 -681
- package/dist-engine-src/src/session/merge/analysis.rs +0 -108
- package/dist-engine-src/src/session/merge/branch.rs +0 -417
- package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
- package/dist-engine-src/src/session/merge/mod.rs +0 -10
- package/dist-engine-src/src/session/merge/stats.rs +0 -61
- package/dist-engine-src/src/session/mod.rs +0 -30
- package/dist-engine-src/src/session/switch_branch.rs +0 -113
- package/dist-engine-src/src/session/transaction.rs +0 -557
- package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
- package/dist-engine-src/src/sql2/bind/error.rs +0 -5
- package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
- package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
- package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
- package/dist-engine-src/src/sql2/bind/read.rs +0 -65
- package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
- package/dist-engine-src/src/sql2/bind/table.rs +0 -273
- package/dist-engine-src/src/sql2/bind/write.rs +0 -86
- package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
- package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
- package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
- package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
- package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
- package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
- package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
- package/dist-engine-src/src/sql2/context.rs +0 -317
- package/dist-engine-src/src/sql2/dml.rs +0 -148
- package/dist-engine-src/src/sql2/error.rs +0 -215
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
- package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
- package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
- package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
- package/dist-engine-src/src/sql2/exec/write.rs +0 -661
- package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
- package/dist-engine-src/src/sql2/history_projection.rs +0 -56
- package/dist-engine-src/src/sql2/history_route.rs +0 -661
- package/dist-engine-src/src/sql2/mod.rs +0 -52
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
- package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
- package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
- package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
- package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
- package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
- package/dist-engine-src/src/sql2/plan/write.rs +0 -147
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
- package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
- package/dist-engine-src/src/sql2/providers/change.rs +0 -445
- package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
- package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
- package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
- package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
- package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
- package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
- package/dist-engine-src/src/sql2/providers/history.rs +0 -426
- package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
- package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
- package/dist-engine-src/src/sql2/read_only.rs +0 -63
- package/dist-engine-src/src/sql2/record_batch.rs +0 -17
- package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
- package/dist-engine-src/src/sql2/runtime.rs +0 -60
- package/dist-engine-src/src/sql2/session.rs +0 -83
- package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
- package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
- package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
- package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
- package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
- package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
- package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
- package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
- package/dist-engine-src/src/storage/conformance.rs +0 -399
- package/dist-engine-src/src/storage/context.rs +0 -620
- package/dist-engine-src/src/storage/mod.rs +0 -52
- package/dist-engine-src/src/storage/point.rs +0 -440
- package/dist-engine-src/src/storage/read_scope.rs +0 -67
- package/dist-engine-src/src/storage/reader.rs +0 -867
- package/dist-engine-src/src/storage/scan.rs +0 -784
- package/dist-engine-src/src/storage/spaces.rs +0 -236
- package/dist-engine-src/src/storage/stats.rs +0 -80
- package/dist-engine-src/src/storage/write_set.rs +0 -962
- package/dist-engine-src/src/storage_bench.rs +0 -171
- package/dist-engine-src/src/test_support.rs +0 -450
- package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
- package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
- package/dist-engine-src/src/tracked_state/context.rs +0 -2801
- package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
- package/dist-engine-src/src/tracked_state/merge.rs +0 -478
- package/dist-engine-src/src/tracked_state/mod.rs +0 -35
- package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
- package/dist-engine-src/src/tracked_state/storage.rs +0 -427
- package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
- package/dist-engine-src/src/tracked_state/types.rs +0 -238
- package/dist-engine-src/src/transaction/bench_support.rs +0 -407
- package/dist-engine-src/src/transaction/commit.rs +0 -1592
- package/dist-engine-src/src/transaction/context.rs +0 -1653
- package/dist-engine-src/src/transaction/mod.rs +0 -24
- package/dist-engine-src/src/transaction/normalization.rs +0 -877
- package/dist-engine-src/src/transaction/prep.rs +0 -37
- package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
- package/dist-engine-src/src/transaction/staging.rs +0 -1525
- package/dist-engine-src/src/transaction/types.rs +0 -403
- package/dist-engine-src/src/transaction/validation.rs +0 -5766
- package/dist-engine-src/src/untracked_state/codec.rs +0 -615
- package/dist-engine-src/src/untracked_state/context.rs +0 -98
- package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
- package/dist-engine-src/src/untracked_state/mod.rs +0 -15
- package/dist-engine-src/src/untracked_state/storage.rs +0 -898
- package/dist-engine-src/src/untracked_state/types.rs +0 -146
- package/dist-engine-src/src/wasm/mod.rs +0 -60
|
@@ -1,2801 +0,0 @@
|
|
|
1
|
-
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
2
|
-
|
|
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};
|
|
9
|
-
use crate::tracked_state::codec::{encode_key_ref, encode_value_ref};
|
|
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)]
|
|
16
|
-
use crate::tracked_state::merge::{self, TrackedStateMergePlan};
|
|
17
|
-
use crate::tracked_state::storage;
|
|
18
|
-
use crate::tracked_state::tree::TrackedStateTree;
|
|
19
|
-
use crate::tracked_state::types::{
|
|
20
|
-
TrackedStateCommitRoot, TrackedStateCommitRootParent, TrackedStateIndexValue, TrackedStateKey,
|
|
21
|
-
TrackedStateKeyRef, TrackedStateMutation, TrackedStateRootId, TrackedStateTreeScanRequest,
|
|
22
|
-
};
|
|
23
|
-
use crate::tracked_state::{
|
|
24
|
-
MaterializedTrackedStateRow, TrackedStateDeltaRef, TrackedStateScanRequest,
|
|
25
|
-
};
|
|
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
|
-
}
|
|
34
|
-
|
|
35
|
-
/// Factory for tracked-state readers, root writers, and commit-root rebuilders.
|
|
36
|
-
///
|
|
37
|
-
/// Tracked state is stored as content-addressed roots. Branch refs
|
|
38
|
-
/// choose which commit/root to read; this context only owns root operations.
|
|
39
|
-
#[derive(Clone)]
|
|
40
|
-
pub(crate) struct TrackedStateContext {
|
|
41
|
-
tree: TrackedStateTree,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
impl TrackedStateContext {
|
|
45
|
-
pub(crate) fn new() -> Self {
|
|
46
|
-
Self {
|
|
47
|
-
tree: TrackedStateTree::new(),
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/// Creates a commit-id-addressed tracked-state reader.
|
|
52
|
-
pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
|
|
53
|
-
where
|
|
54
|
-
S: StorageRead + Send + Sync,
|
|
55
|
-
{
|
|
56
|
-
TrackedStateStoreReader {
|
|
57
|
-
store,
|
|
58
|
-
tree: self.tree.clone(),
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// Creates a tracked-state writer over a caller-owned transaction and write set.
|
|
63
|
-
pub(crate) fn writer<'a, S>(
|
|
64
|
-
&'a self,
|
|
65
|
-
store: &'a S,
|
|
66
|
-
writes: &'a mut StorageWriteSet,
|
|
67
|
-
) -> TrackedStateWriter<'a, S>
|
|
68
|
-
where
|
|
69
|
-
S: StorageRead + Send + Sync + ?Sized,
|
|
70
|
-
{
|
|
71
|
-
TrackedStateWriter {
|
|
72
|
-
chunk_overlay: storage::TrackedStateChunkOverlay::new(),
|
|
73
|
-
staged_roots: BTreeMap::new(),
|
|
74
|
-
tree: self.tree.clone(),
|
|
75
|
-
store,
|
|
76
|
-
writes,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// Creates an explicit tracked-state commit-root rebuilder.
|
|
81
|
-
///
|
|
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>(
|
|
85
|
-
&'a self,
|
|
86
|
-
store: &'a S,
|
|
87
|
-
writes: &'a mut StorageWriteSet,
|
|
88
|
-
) -> TrackedStateRootRebuilder<'a, S>
|
|
89
|
-
where
|
|
90
|
-
S: StorageRead + Send + Sync + ?Sized,
|
|
91
|
-
{
|
|
92
|
-
let _ = self;
|
|
93
|
-
TrackedStateRootRebuilder { store, writes }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/// Store-backed tracked-state reader created by `TrackedStateContext`.
|
|
98
|
-
pub(crate) struct TrackedStateStoreReader<S> {
|
|
99
|
-
store: S,
|
|
100
|
-
tree: TrackedStateTree,
|
|
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
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
impl<S> TrackedStateStoreReader<S>
|
|
124
|
-
where
|
|
125
|
-
S: StorageRead + Send + Sync,
|
|
126
|
-
{
|
|
127
|
-
pub(crate) async fn scan_rows_at_commit(
|
|
128
|
-
&mut self,
|
|
129
|
-
commit_id: &str,
|
|
130
|
-
request: &TrackedStateScanRequest,
|
|
131
|
-
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
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));
|
|
134
|
-
};
|
|
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,
|
|
145
|
-
);
|
|
146
|
-
let mut rows =
|
|
147
|
-
materialize_rows_from_index_entries(&mut self.store, rows, &materialization).await?;
|
|
148
|
-
if !request.filter.include_tombstones {
|
|
149
|
-
rows.retain(|row| !row.deleted);
|
|
150
|
-
}
|
|
151
|
-
if let Some(limit) = request.limit {
|
|
152
|
-
rows.truncate(limit);
|
|
153
|
-
}
|
|
154
|
-
Ok(rows)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
#[cfg(any(test, feature = "storage-benches"))]
|
|
158
|
-
pub(crate) async fn load_rows_at_commit(
|
|
159
|
-
&mut self,
|
|
160
|
-
commit_id: &str,
|
|
161
|
-
keys: &[TrackedStateKey],
|
|
162
|
-
) -> Result<Vec<Option<MaterializedTrackedStateRow>>, LixError> {
|
|
163
|
-
if keys.is_empty() {
|
|
164
|
-
return Ok(Vec::new());
|
|
165
|
-
}
|
|
166
|
-
let values = self.commit_root_values_for_keys(commit_id, &keys).await?;
|
|
167
|
-
let mut entry_indices = Vec::new();
|
|
168
|
-
let mut entries = Vec::new();
|
|
169
|
-
for (index, (key, value)) in keys.iter().cloned().zip(values).enumerate() {
|
|
170
|
-
if let Some(value) = value {
|
|
171
|
-
entry_indices.push(index);
|
|
172
|
-
entries.push((key, value));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
let materialized = materialize_rows_from_index_entries(
|
|
176
|
-
&mut self.store,
|
|
177
|
-
entries,
|
|
178
|
-
&crate::tracked_state::TrackedRowMaterialization::full(),
|
|
179
|
-
)
|
|
180
|
-
.await?;
|
|
181
|
-
let mut rows = vec![None; keys.len()];
|
|
182
|
-
for (index, row) in entry_indices.into_iter().zip(materialized) {
|
|
183
|
-
rows[index] = Some(row);
|
|
184
|
-
}
|
|
185
|
-
Ok(rows)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
pub(crate) async fn diff_commits(
|
|
189
|
-
&mut self,
|
|
190
|
-
left_commit_id: &str,
|
|
191
|
-
right_commit_id: &str,
|
|
192
|
-
request: &TrackedStateDiffRequest,
|
|
193
|
-
) -> Result<TrackedStateDiff, LixError> {
|
|
194
|
-
diff_commits(self, left_commit_id, right_commit_id, request).await
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
pub(crate) async fn diff_commits_with_validation(
|
|
198
|
-
&mut self,
|
|
199
|
-
left_commit_id: &str,
|
|
200
|
-
right_commit_id: &str,
|
|
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
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
pub(crate) async fn validate_diff_rows_for_commits_against_changelog(
|
|
217
|
-
&mut self,
|
|
218
|
-
rows: &[(&TrackedStateDiffRow, &str)],
|
|
219
|
-
) -> Result<(), LixError> {
|
|
220
|
-
if rows.is_empty() {
|
|
221
|
-
return Ok(());
|
|
222
|
-
}
|
|
223
|
-
|
|
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();
|
|
231
|
-
|
|
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
|
-
)));
|
|
244
|
-
};
|
|
245
|
-
changes.insert(change_id, change);
|
|
246
|
-
}
|
|
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
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
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
|
-
)
|
|
290
|
-
.await?;
|
|
291
|
-
}
|
|
292
|
-
Ok(())
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async fn validate_diff_row_commit_root_membership(
|
|
296
|
-
&mut self,
|
|
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)
|
|
310
|
-
.await?;
|
|
311
|
-
self.validate_commit_root_parent_matches_changelog(root_commit_id, &root_metadata, cache)
|
|
312
|
-
.await?;
|
|
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
|
-
)));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let winners = self
|
|
324
|
-
.load_cached_commit_ref_winners(¤t_commit_id, cache)
|
|
325
|
-
.await?;
|
|
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
|
-
)));
|
|
332
|
-
}
|
|
333
|
-
self.validate_diff_row_created_at(row, &key, ¤t_commit_id, change_created_at)
|
|
334
|
-
.await?;
|
|
335
|
-
return Ok(());
|
|
336
|
-
}
|
|
337
|
-
|
|
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
|
-
)));
|
|
352
|
-
};
|
|
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
|
-
)));
|
|
361
|
-
}
|
|
362
|
-
current_commit_id = parent.commit_id.clone();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
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
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
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)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async fn load_cached_commit_ref_winners(
|
|
440
|
-
&mut self,
|
|
441
|
-
commit_id: &str,
|
|
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());
|
|
446
|
-
}
|
|
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
|
-
})
|
|
454
|
-
.await?;
|
|
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)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async fn load_cached_commit_root_metadata(
|
|
495
|
-
&mut self,
|
|
496
|
-
commit_id: &str,
|
|
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());
|
|
501
|
-
}
|
|
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)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async fn load_cached_commit_root_optional(
|
|
512
|
-
&mut self,
|
|
513
|
-
commit_id: &str,
|
|
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)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
async fn load_cached_changelog_first_parent(
|
|
550
|
-
&mut self,
|
|
551
|
-
commit_id: &str,
|
|
552
|
-
cache: &mut DiffCommitRootValidationCache,
|
|
553
|
-
) -> Result<Option<String>, LixError> {
|
|
554
|
-
if let Some(parent_id) = cache.changelog_first_parents.get(commit_id) {
|
|
555
|
-
return Ok(parent_id.clone());
|
|
556
|
-
}
|
|
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
|
-
)));
|
|
569
|
-
};
|
|
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
|
-
)));
|
|
574
|
-
};
|
|
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)
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
async fn validate_diff_row_created_at(
|
|
583
|
-
&mut self,
|
|
584
|
-
row: &TrackedStateDiffRow,
|
|
585
|
-
key: &TrackedStateKey,
|
|
586
|
-
commit_id: &str,
|
|
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
|
|
595
|
-
.tree
|
|
596
|
-
.get_many(&mut self.store, &parent.root_id, std::slice::from_ref(key))
|
|
597
|
-
.await?
|
|
598
|
-
.into_iter()
|
|
599
|
-
.next()
|
|
600
|
-
.flatten();
|
|
601
|
-
if let Some(parent_value) = parent_value {
|
|
602
|
-
expected_created_at = parent_value.created_at;
|
|
603
|
-
}
|
|
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)
|
|
608
|
-
.await?
|
|
609
|
-
{
|
|
610
|
-
expected_created_at = merge_parent_created_at;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
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;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
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
|
-
)))
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async fn load_merge_parent_created_at_for_row(
|
|
630
|
-
&mut self,
|
|
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 {
|
|
649
|
-
continue;
|
|
650
|
-
};
|
|
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));
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
Ok(None)
|
|
665
|
-
}
|
|
666
|
-
|
|
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
|
|
812
|
-
/// changes should be applied.
|
|
813
|
-
#[cfg(test)]
|
|
814
|
-
pub(crate) async fn plan_merge(
|
|
815
|
-
&mut self,
|
|
816
|
-
base_commit_id: &str,
|
|
817
|
-
target_commit_id: &str,
|
|
818
|
-
source_commit_id: &str,
|
|
819
|
-
request: &TrackedStateDiffRequest,
|
|
820
|
-
) -> Result<TrackedStateMergePlan, LixError> {
|
|
821
|
-
let target_diff = self
|
|
822
|
-
.diff_commits(base_commit_id, target_commit_id, request)
|
|
823
|
-
.await?;
|
|
824
|
-
let source_diff = self
|
|
825
|
-
.diff_commits(base_commit_id, source_commit_id, request)
|
|
826
|
-
.await?;
|
|
827
|
-
merge::plan_merge(&target_diff, &source_diff)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/// Writer for changelog-backed tracked-state commit roots.
|
|
832
|
-
pub(crate) struct TrackedStateWriter<'a, S: ?Sized> {
|
|
833
|
-
chunk_overlay: storage::TrackedStateChunkOverlay,
|
|
834
|
-
staged_roots: BTreeMap<String, crate::tracked_state::types::TrackedStateRootId>,
|
|
835
|
-
tree: TrackedStateTree,
|
|
836
|
-
store: &'a S,
|
|
837
|
-
writes: &'a mut StorageWriteSet,
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/// Explicit commit-root rebuilder created by `TrackedStateContext`.
|
|
841
|
-
pub(crate) struct TrackedStateRootRebuilder<'a, S: ?Sized> {
|
|
842
|
-
pub(super) store: &'a S,
|
|
843
|
-
pub(super) writes: &'a mut StorageWriteSet,
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
impl<S> TrackedStateRootRebuilder<'_, S>
|
|
847
|
-
where
|
|
848
|
-
S: StorageRead + Send + Sync + ?Sized,
|
|
849
|
-
{
|
|
850
|
-
pub(crate) async fn rebuild_commit_root_at(
|
|
851
|
-
&mut self,
|
|
852
|
-
commit_id: &str,
|
|
853
|
-
) -> Result<TrackedStateWriteReport, LixError> {
|
|
854
|
-
crate::tracked_state::commit_root_rebuild::rebuild_commit_root_at(self, commit_id).await
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
impl<S> TrackedStateWriter<'_, S>
|
|
859
|
-
where
|
|
860
|
-
S: StorageRead + Send + Sync + ?Sized,
|
|
861
|
-
{
|
|
862
|
-
pub(crate) async fn stage_commit_root<'a, I>(
|
|
863
|
-
&mut self,
|
|
864
|
-
commit_id: &str,
|
|
865
|
-
parent_commit_id: Option<&str>,
|
|
866
|
-
deltas: I,
|
|
867
|
-
) -> Result<TrackedStateWriteReport, LixError>
|
|
868
|
-
where
|
|
869
|
-
I: IntoIterator<Item = TrackedStateDeltaRef<'a>>,
|
|
870
|
-
{
|
|
871
|
-
let deltas = deltas.into_iter().collect::<Vec<_>>();
|
|
872
|
-
let base_root = match parent_commit_id {
|
|
873
|
-
Some(parent_commit_id) => {
|
|
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 {
|
|
879
|
-
return Err(LixError::new(
|
|
880
|
-
"LIX_ERROR_UNKNOWN",
|
|
881
|
-
format!(
|
|
882
|
-
"tracked-state parent root for commit '{parent_commit_id}' is missing"
|
|
883
|
-
),
|
|
884
|
-
));
|
|
885
|
-
};
|
|
886
|
-
Some(root)
|
|
887
|
-
}
|
|
888
|
-
None => None,
|
|
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
|
-
};
|
|
903
|
-
let mut mutations = Vec::with_capacity(deltas.len());
|
|
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);
|
|
909
|
-
let key = TrackedStateKeyRef {
|
|
910
|
-
schema_key: delta.schema_key,
|
|
911
|
-
file_id: delta.file_id,
|
|
912
|
-
entity_pk: delta.entity_pk,
|
|
913
|
-
};
|
|
914
|
-
let value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
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,
|
|
921
|
-
updated_at: delta.updated_at,
|
|
922
|
-
};
|
|
923
|
-
mutations.push(TrackedStateMutation::put_encoded(
|
|
924
|
-
encode_key_ref(key),
|
|
925
|
-
encode_value_ref(value),
|
|
926
|
-
));
|
|
927
|
-
}
|
|
928
|
-
let result = self
|
|
929
|
-
.tree
|
|
930
|
-
.apply_mutations_with_overlay(
|
|
931
|
-
self.store,
|
|
932
|
-
self.writes,
|
|
933
|
-
&mut self.chunk_overlay,
|
|
934
|
-
base_root.as_ref(),
|
|
935
|
-
mutations,
|
|
936
|
-
Some(commit_id),
|
|
937
|
-
)
|
|
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
|
-
)?;
|
|
987
|
-
|
|
988
|
-
Ok(TrackedStateWriteReport {
|
|
989
|
-
commit_id: commit_id.to_string(),
|
|
990
|
-
root_id: result.root_id,
|
|
991
|
-
changed_rows: deltas.len(),
|
|
992
|
-
primary_chunk_puts: result.chunk_count,
|
|
993
|
-
})
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
998
|
-
pub(crate) struct TrackedStateWriteReport {
|
|
999
|
-
pub(crate) commit_id: String,
|
|
1000
|
-
pub(crate) root_id: TrackedStateRootId,
|
|
1001
|
-
pub(crate) changed_rows: usize,
|
|
1002
|
-
pub(crate) primary_chunk_puts: usize,
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
fn missing_commit_root_error(commit_id: &str) -> LixError {
|
|
1006
|
-
LixError::new(
|
|
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
|
-
),
|
|
1011
|
-
)
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
fn tree_scan_request_from_tracked(
|
|
1015
|
-
request: &TrackedStateScanRequest,
|
|
1016
|
-
) -> TrackedStateTreeScanRequest {
|
|
1017
|
-
TrackedStateTreeScanRequest {
|
|
1018
|
-
schema_keys: request.filter.schema_keys.clone(),
|
|
1019
|
-
entity_pks: request.filter.entity_pks.clone(),
|
|
1020
|
-
file_ids: request.filter.file_ids.clone(),
|
|
1021
|
-
include_tombstones: request.filter.include_tombstones,
|
|
1022
|
-
// User limits belong above delta overlay and tombstone visibility.
|
|
1023
|
-
// Pushing them into the physical tree can stop on rows that are later
|
|
1024
|
-
// hidden, returning too few live rows.
|
|
1025
|
-
limit: None,
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
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
|
-
)));
|
|
1065
|
-
}
|
|
1066
|
-
Ok(())
|
|
1067
|
-
}
|
|
1068
|
-
|
|
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(),
|
|
1082
|
-
})
|
|
1083
|
-
}
|
|
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
|
-
|
|
1138
|
-
#[cfg(test)]
|
|
1139
|
-
mod tests {
|
|
1140
|
-
use super::*;
|
|
1141
|
-
use crate::storage::StorageContext;
|
|
1142
|
-
use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
|
|
1143
|
-
use crate::NullableKeyFilter;
|
|
1144
|
-
|
|
1145
|
-
#[tokio::test]
|
|
1146
|
-
async fn stage_commit_root_requires_parent_commit_root() {
|
|
1147
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1148
|
-
let tracked_state = TrackedStateContext::new();
|
|
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
|
-
)
|
|
1160
|
-
.await
|
|
1161
|
-
.expect("parent changelog commit should stage");
|
|
1162
|
-
storage
|
|
1163
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1164
|
-
.expect("parent changelog commit should commit");
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
write_root_for_test(
|
|
1168
|
-
&storage,
|
|
1169
|
-
&tracked_state,
|
|
1170
|
-
"commit-child",
|
|
1171
|
-
Some("missing-parent"),
|
|
1172
|
-
&[row("entity-child", "change-child", "commit-child")],
|
|
1173
|
-
)
|
|
1174
|
-
.await
|
|
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);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
#[tokio::test]
|
|
1233
|
-
async fn plan_merge_from_roots_applies_source_only_change() {
|
|
1234
|
-
let (storage, tracked_state) = seed_merge_roots(
|
|
1235
|
-
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1236
|
-
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1237
|
-
&[row_with_value(
|
|
1238
|
-
"entity-a",
|
|
1239
|
-
"change-source",
|
|
1240
|
-
"source",
|
|
1241
|
-
"source",
|
|
1242
|
-
)],
|
|
1243
|
-
)
|
|
1244
|
-
.await;
|
|
1245
|
-
|
|
1246
|
-
let plan = tracked_state
|
|
1247
|
-
.reader(
|
|
1248
|
-
storage
|
|
1249
|
-
.begin_read(StorageReadOptions::default())
|
|
1250
|
-
.expect("read should open"),
|
|
1251
|
-
)
|
|
1252
|
-
.plan_merge(
|
|
1253
|
-
"base",
|
|
1254
|
-
"target",
|
|
1255
|
-
"source",
|
|
1256
|
-
&TrackedStateDiffRequest::default(),
|
|
1257
|
-
)
|
|
1258
|
-
.await
|
|
1259
|
-
.expect("merge should plan");
|
|
1260
|
-
|
|
1261
|
-
assert_eq!(merge_pick_ids(&plan), vec!["entity-a"]);
|
|
1262
|
-
assert!(plan.conflicts.is_empty());
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
#[tokio::test]
|
|
1266
|
-
async fn plan_merge_from_roots_keeps_target_only_change() {
|
|
1267
|
-
let (storage, tracked_state) = seed_merge_roots(
|
|
1268
|
-
&[row("entity-a", "change-base", "base")],
|
|
1269
|
-
&[row("entity-a", "change-target", "target")],
|
|
1270
|
-
&[row("entity-a", "change-base", "base")],
|
|
1271
|
-
)
|
|
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;
|
|
1770
|
-
|
|
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"),
|
|
1789
|
-
)
|
|
1790
|
-
.diff_commits("base", "child", &test_schema_diff_request())
|
|
1791
|
-
.await
|
|
1792
|
-
.expect("diff should use repaired root chunk");
|
|
1793
|
-
|
|
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
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
#[tokio::test]
|
|
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),
|
|
1822
|
-
)
|
|
1823
|
-
.await;
|
|
1824
|
-
|
|
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")
|
|
1832
|
-
.await
|
|
1833
|
-
.expect("stale root should repair");
|
|
1834
|
-
storage
|
|
1835
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1836
|
-
.expect("repaired root should commit");
|
|
1837
|
-
|
|
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");
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
#[tokio::test]
|
|
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),
|
|
1872
|
-
)
|
|
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;
|
|
1886
|
-
|
|
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")
|
|
1894
|
-
.await
|
|
1895
|
-
.expect("stale child root should repair");
|
|
1896
|
-
storage
|
|
1897
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1898
|
-
.expect("repaired root should commit");
|
|
1899
|
-
|
|
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
|
-
);
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
#[tokio::test]
|
|
1918
|
-
async fn scan_rows_filters_by_file() {
|
|
1919
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1920
|
-
let tracked_state = TrackedStateContext::new();
|
|
1921
|
-
let mut file_a = row("entity-a", "change-a", "commit-1");
|
|
1922
|
-
file_a.file_id = Some("file-a.json".to_string());
|
|
1923
|
-
let mut file_b = row("entity-b", "change-b", "commit-1");
|
|
1924
|
-
file_b.file_id = Some("file-b.json".to_string());
|
|
1925
|
-
write_root_for_test(
|
|
1926
|
-
&storage,
|
|
1927
|
-
&tracked_state,
|
|
1928
|
-
"commit-1",
|
|
1929
|
-
None,
|
|
1930
|
-
&[file_a, file_b],
|
|
1931
|
-
)
|
|
1932
|
-
.await
|
|
1933
|
-
.expect("root should write");
|
|
1934
|
-
|
|
1935
|
-
let rows = tracked_state
|
|
1936
|
-
.reader(
|
|
1937
|
-
storage
|
|
1938
|
-
.begin_read(StorageReadOptions::default())
|
|
1939
|
-
.expect("read should open"),
|
|
1940
|
-
)
|
|
1941
|
-
.scan_rows_at_commit(
|
|
1942
|
-
"commit-1",
|
|
1943
|
-
&TrackedStateScanRequest {
|
|
1944
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
1945
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1946
|
-
..Default::default()
|
|
1947
|
-
},
|
|
1948
|
-
..Default::default()
|
|
1949
|
-
},
|
|
1950
|
-
)
|
|
1951
|
-
.await
|
|
1952
|
-
.expect("file scan should use primary root");
|
|
1953
|
-
|
|
1954
|
-
assert_eq!(rows.len(), 1);
|
|
1955
|
-
assert_eq!(
|
|
1956
|
-
rows[0]
|
|
1957
|
-
.entity_pk
|
|
1958
|
-
.as_single_string_owned()
|
|
1959
|
-
.expect("entity pk"),
|
|
1960
|
-
"entity-a"
|
|
1961
|
-
);
|
|
1962
|
-
assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
#[tokio::test]
|
|
1966
|
-
async fn file_filtered_header_scan_fetches_primary_payload_only_when_requested() {
|
|
1967
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1968
|
-
let tracked_state = TrackedStateContext::new();
|
|
1969
|
-
let mut row = row("entity-a", "change-a", "commit-1");
|
|
1970
|
-
row.file_id = Some("file-a.json".to_string());
|
|
1971
|
-
let expected_snapshot = row.snapshot_content.clone();
|
|
1972
|
-
write_root_for_test(
|
|
1973
|
-
&storage,
|
|
1974
|
-
&tracked_state,
|
|
1975
|
-
"commit-1",
|
|
1976
|
-
None,
|
|
1977
|
-
std::slice::from_ref(&row),
|
|
1978
|
-
)
|
|
1979
|
-
.await
|
|
1980
|
-
.expect("root should write");
|
|
1981
|
-
|
|
1982
|
-
let mut reader = tracked_state.reader(
|
|
1983
|
-
storage
|
|
1984
|
-
.begin_read(StorageReadOptions::default())
|
|
1985
|
-
.expect("read should open"),
|
|
1986
|
-
);
|
|
1987
|
-
let header_rows = reader
|
|
1988
|
-
.scan_rows_at_commit(
|
|
1989
|
-
"commit-1",
|
|
1990
|
-
&TrackedStateScanRequest {
|
|
1991
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
1992
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1993
|
-
..Default::default()
|
|
1994
|
-
},
|
|
1995
|
-
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
1996
|
-
columns: vec!["entity_pk".to_string()],
|
|
1997
|
-
},
|
|
1998
|
-
..Default::default()
|
|
1999
|
-
},
|
|
2000
|
-
)
|
|
2001
|
-
.await
|
|
2002
|
-
.expect("header scan should use primary root");
|
|
2003
|
-
let full_rows = reader
|
|
2004
|
-
.scan_rows_at_commit(
|
|
2005
|
-
"commit-1",
|
|
2006
|
-
&TrackedStateScanRequest {
|
|
2007
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2008
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
2009
|
-
..Default::default()
|
|
2010
|
-
},
|
|
2011
|
-
..Default::default()
|
|
2012
|
-
},
|
|
2013
|
-
)
|
|
2014
|
-
.await
|
|
2015
|
-
.expect("full scan should fetch primary payload");
|
|
2016
|
-
|
|
2017
|
-
assert_eq!(header_rows[0].snapshot_content, None);
|
|
2018
|
-
assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
#[tokio::test]
|
|
2022
|
-
async fn null_file_rows_match_null_file_filter() {
|
|
2023
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2024
|
-
let tracked_state = TrackedStateContext::new();
|
|
2025
|
-
let row = row("entity-a", "change-a", "commit-1");
|
|
2026
|
-
write_root_for_test(
|
|
2027
|
-
&storage,
|
|
2028
|
-
&tracked_state,
|
|
2029
|
-
"commit-1",
|
|
2030
|
-
None,
|
|
2031
|
-
std::slice::from_ref(&row),
|
|
2032
|
-
)
|
|
2033
|
-
.await
|
|
2034
|
-
.expect("root should write");
|
|
2035
|
-
|
|
2036
|
-
let rows = tracked_state
|
|
2037
|
-
.reader(
|
|
2038
|
-
storage
|
|
2039
|
-
.begin_read(StorageReadOptions::default())
|
|
2040
|
-
.expect("read should open"),
|
|
2041
|
-
)
|
|
2042
|
-
.scan_rows_at_commit(
|
|
2043
|
-
"commit-1",
|
|
2044
|
-
&TrackedStateScanRequest {
|
|
2045
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2046
|
-
schema_keys: vec!["test_schema".to_string()],
|
|
2047
|
-
file_ids: vec![NullableKeyFilter::Null],
|
|
2048
|
-
..Default::default()
|
|
2049
|
-
},
|
|
2050
|
-
..Default::default()
|
|
2051
|
-
},
|
|
2052
|
-
)
|
|
2053
|
-
.await
|
|
2054
|
-
.expect("null file scan should use primary tree");
|
|
2055
|
-
|
|
2056
|
-
assert_eq!(rows.len(), 1);
|
|
2057
|
-
assert_eq!(
|
|
2058
|
-
rows[0]
|
|
2059
|
-
.entity_pk
|
|
2060
|
-
.as_single_string_owned()
|
|
2061
|
-
.expect("entity pk"),
|
|
2062
|
-
"entity-a"
|
|
2063
|
-
);
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
#[tokio::test]
|
|
2067
|
-
async fn mixed_null_and_concrete_file_scan_uses_primary_tree() {
|
|
2068
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2069
|
-
let tracked_state = TrackedStateContext::new();
|
|
2070
|
-
let null_row = row("entity-null", "change-null", "commit-1");
|
|
2071
|
-
let mut file_row = row("entity-file", "change-file", "commit-2");
|
|
2072
|
-
file_row.file_id = Some("file-a.json".to_string());
|
|
2073
|
-
write_root_for_test(
|
|
2074
|
-
&storage,
|
|
2075
|
-
&tracked_state,
|
|
2076
|
-
"commit-1",
|
|
2077
|
-
None,
|
|
2078
|
-
std::slice::from_ref(&null_row),
|
|
2079
|
-
)
|
|
2080
|
-
.await
|
|
2081
|
-
.expect("parent root should write");
|
|
2082
|
-
write_root_for_test(
|
|
2083
|
-
&storage,
|
|
2084
|
-
&tracked_state,
|
|
2085
|
-
"commit-2",
|
|
2086
|
-
Some("commit-1"),
|
|
2087
|
-
std::slice::from_ref(&file_row),
|
|
2088
|
-
)
|
|
2089
|
-
.await
|
|
2090
|
-
.expect("child root should write");
|
|
2091
|
-
|
|
2092
|
-
let rows = tracked_state
|
|
2093
|
-
.reader(
|
|
2094
|
-
storage
|
|
2095
|
-
.begin_read(StorageReadOptions::default())
|
|
2096
|
-
.expect("read should open"),
|
|
2097
|
-
)
|
|
2098
|
-
.scan_rows_at_commit(
|
|
2099
|
-
"commit-2",
|
|
2100
|
-
&TrackedStateScanRequest {
|
|
2101
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2102
|
-
schema_keys: vec!["test_schema".to_string()],
|
|
2103
|
-
file_ids: vec![
|
|
2104
|
-
NullableKeyFilter::Null,
|
|
2105
|
-
NullableKeyFilter::Value("file-a.json".to_string()),
|
|
2106
|
-
],
|
|
2107
|
-
..Default::default()
|
|
2108
|
-
},
|
|
2109
|
-
..Default::default()
|
|
2110
|
-
},
|
|
2111
|
-
)
|
|
2112
|
-
.await
|
|
2113
|
-
.expect("mixed scan should use primary tree");
|
|
2114
|
-
|
|
2115
|
-
let mut entity_pks = rows
|
|
2116
|
-
.iter()
|
|
2117
|
-
.map(|row| row.entity_pk.as_single_string_owned().expect("entity pk"))
|
|
2118
|
-
.collect::<Vec<_>>();
|
|
2119
|
-
entity_pks.sort();
|
|
2120
|
-
assert_eq!(entity_pks, vec!["entity-file", "entity-null"]);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
#[tokio::test]
|
|
2124
|
-
async fn file_filtered_header_scan_filters_tombstones_without_payload_sentinel() {
|
|
2125
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2126
|
-
let tracked_state = TrackedStateContext::new();
|
|
2127
|
-
let mut live = row("entity-live", "change-live", "commit-1");
|
|
2128
|
-
live.file_id = Some("file-a.json".to_string());
|
|
2129
|
-
let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
|
|
2130
|
-
deleted.file_id = Some("file-a.json".to_string());
|
|
2131
|
-
write_root_for_test(&storage, &tracked_state, "commit-1", None, &[live, deleted])
|
|
2132
|
-
.await
|
|
2133
|
-
.expect("root should write");
|
|
2134
|
-
|
|
2135
|
-
let rows = tracked_state
|
|
2136
|
-
.reader(
|
|
2137
|
-
storage
|
|
2138
|
-
.begin_read(StorageReadOptions::default())
|
|
2139
|
-
.expect("read should open"),
|
|
2140
|
-
)
|
|
2141
|
-
.scan_rows_at_commit(
|
|
2142
|
-
"commit-1",
|
|
2143
|
-
&TrackedStateScanRequest {
|
|
2144
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2145
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
2146
|
-
..Default::default()
|
|
2147
|
-
},
|
|
2148
|
-
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
2149
|
-
columns: vec!["entity_pk".to_string()],
|
|
2150
|
-
},
|
|
2151
|
-
..Default::default()
|
|
2152
|
-
},
|
|
2153
|
-
)
|
|
2154
|
-
.await
|
|
2155
|
-
.expect("file scan should use primary root");
|
|
2156
|
-
|
|
2157
|
-
assert_eq!(rows.len(), 1);
|
|
2158
|
-
assert_eq!(
|
|
2159
|
-
rows[0]
|
|
2160
|
-
.entity_pk
|
|
2161
|
-
.as_single_string_owned()
|
|
2162
|
-
.expect("entity pk"),
|
|
2163
|
-
"entity-live"
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
#[tokio::test]
|
|
2168
|
-
async fn child_root_tombstone_hides_materialized_base_row() {
|
|
2169
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2170
|
-
let tracked_state = TrackedStateContext::new();
|
|
2171
|
-
let base = row("entity-a", "change-base", "base");
|
|
2172
|
-
let delete = tombstone("entity-a", "change-delete", "child");
|
|
2173
|
-
write_root_for_test(
|
|
2174
|
-
&storage,
|
|
2175
|
-
&tracked_state,
|
|
2176
|
-
"base",
|
|
2177
|
-
None,
|
|
2178
|
-
std::slice::from_ref(&base),
|
|
2179
|
-
)
|
|
2180
|
-
.await
|
|
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();
|
|
2186
|
-
tracked_state
|
|
2187
|
-
.root_rebuilder(&read, &mut writes)
|
|
2188
|
-
.rebuild_commit_root_at("base")
|
|
2189
|
-
.await
|
|
2190
|
-
.expect("base commit root should materialize");
|
|
2191
|
-
storage
|
|
2192
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
2193
|
-
.expect("materialized base should commit");
|
|
2194
|
-
write_root_for_test(
|
|
2195
|
-
&storage,
|
|
2196
|
-
&tracked_state,
|
|
2197
|
-
"child",
|
|
2198
|
-
Some("base"),
|
|
2199
|
-
std::slice::from_ref(&delete),
|
|
2200
|
-
)
|
|
2201
|
-
.await
|
|
2202
|
-
.expect("child tombstone root should write");
|
|
2203
|
-
|
|
2204
|
-
let rows = tracked_state
|
|
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())
|
|
2211
|
-
.await
|
|
2212
|
-
.expect("child scan should apply tombstone over base root");
|
|
2213
|
-
|
|
2214
|
-
assert!(rows.is_empty(), "pending tombstone must hide base row");
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
#[tokio::test]
|
|
2218
|
-
async fn root_scan_keeps_last_mutation_for_duplicate_key() {
|
|
2219
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2220
|
-
let tracked_state = TrackedStateContext::new();
|
|
2221
|
-
write_root_for_test(
|
|
2222
|
-
&storage,
|
|
2223
|
-
&tracked_state,
|
|
2224
|
-
"commit-1",
|
|
2225
|
-
None,
|
|
2226
|
-
&[
|
|
2227
|
-
row_with_value("entity-a", "change-a1", "commit-1", "first"),
|
|
2228
|
-
row_with_value("entity-b", "change-b", "commit-1", "middle"),
|
|
2229
|
-
row_with_value("entity-a", "change-a2", "commit-1", "second"),
|
|
2230
|
-
tombstone("entity-c", "change-c1", "commit-1"),
|
|
2231
|
-
],
|
|
2232
|
-
)
|
|
2233
|
-
.await
|
|
2234
|
-
.expect("root should write");
|
|
2235
|
-
|
|
2236
|
-
let rows = tracked_state
|
|
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())
|
|
2243
|
-
.await
|
|
2244
|
-
.expect("root should scan");
|
|
2245
|
-
|
|
2246
|
-
assert_eq!(rows.len(), 2);
|
|
2247
|
-
assert_eq!(
|
|
2248
|
-
rows.iter()
|
|
2249
|
-
.map(|row| (
|
|
2250
|
-
row.entity_pk.as_single_string_owned().expect("entity pk"),
|
|
2251
|
-
row.snapshot_content.clone()
|
|
2252
|
-
))
|
|
2253
|
-
.collect::<Vec<_>>(),
|
|
2254
|
-
vec![
|
|
2255
|
-
(
|
|
2256
|
-
"entity-a".to_string(),
|
|
2257
|
-
Some("{\"value\":\"second\"}".to_string())
|
|
2258
|
-
),
|
|
2259
|
-
(
|
|
2260
|
-
"entity-b".to_string(),
|
|
2261
|
-
Some("{\"value\":\"middle\"}".to_string())
|
|
2262
|
-
),
|
|
2263
|
-
]
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
#[tokio::test]
|
|
2268
|
-
async fn scan_limit_applies_after_tombstone_visibility() {
|
|
2269
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2270
|
-
let tracked_state = TrackedStateContext::new();
|
|
2271
|
-
write_root_for_test(
|
|
2272
|
-
&storage,
|
|
2273
|
-
&tracked_state,
|
|
2274
|
-
"commit-1",
|
|
2275
|
-
None,
|
|
2276
|
-
&[
|
|
2277
|
-
tombstone("entity-a", "change-delete", "commit-1"),
|
|
2278
|
-
row("entity-b", "change-live", "commit-1"),
|
|
2279
|
-
],
|
|
2280
|
-
)
|
|
2281
|
-
.await
|
|
2282
|
-
.expect("root should write");
|
|
2283
|
-
|
|
2284
|
-
let rows = tracked_state
|
|
2285
|
-
.reader(
|
|
2286
|
-
storage
|
|
2287
|
-
.begin_read(StorageReadOptions::default())
|
|
2288
|
-
.expect("read should open"),
|
|
2289
|
-
)
|
|
2290
|
-
.scan_rows_at_commit(
|
|
2291
|
-
"commit-1",
|
|
2292
|
-
&TrackedStateScanRequest {
|
|
2293
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2294
|
-
schema_keys: vec!["test_schema".to_string()],
|
|
2295
|
-
..Default::default()
|
|
2296
|
-
},
|
|
2297
|
-
limit: Some(1),
|
|
2298
|
-
..Default::default()
|
|
2299
|
-
},
|
|
2300
|
-
)
|
|
2301
|
-
.await
|
|
2302
|
-
.expect("limited scan should apply visibility before limit");
|
|
2303
|
-
|
|
2304
|
-
assert_eq!(rows.len(), 1);
|
|
2305
|
-
assert_eq!(
|
|
2306
|
-
rows[0]
|
|
2307
|
-
.entity_pk
|
|
2308
|
-
.as_single_string_owned()
|
|
2309
|
-
.expect("entity pk"),
|
|
2310
|
-
"entity-b"
|
|
2311
|
-
);
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
#[tokio::test]
|
|
2315
|
-
async fn file_filtered_scan_limit_applies_after_tombstone_visibility() {
|
|
2316
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2317
|
-
let tracked_state = TrackedStateContext::new();
|
|
2318
|
-
let mut deleted = tombstone("entity-a", "change-delete", "commit-1");
|
|
2319
|
-
deleted.file_id = Some("file-a.json".to_string());
|
|
2320
|
-
let mut live = row("entity-b", "change-live", "commit-1");
|
|
2321
|
-
live.file_id = Some("file-a.json".to_string());
|
|
2322
|
-
write_root_for_test(&storage, &tracked_state, "commit-1", None, &[deleted, live])
|
|
2323
|
-
.await
|
|
2324
|
-
.expect("root should write");
|
|
2325
|
-
|
|
2326
|
-
let rows = tracked_state
|
|
2327
|
-
.reader(
|
|
2328
|
-
storage
|
|
2329
|
-
.begin_read(StorageReadOptions::default())
|
|
2330
|
-
.expect("read should open"),
|
|
2331
|
-
)
|
|
2332
|
-
.scan_rows_at_commit(
|
|
2333
|
-
"commit-1",
|
|
2334
|
-
&TrackedStateScanRequest {
|
|
2335
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
2336
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
2337
|
-
..Default::default()
|
|
2338
|
-
},
|
|
2339
|
-
read_columns: crate::tracked_state::TrackedStateReadColumns {
|
|
2340
|
-
columns: vec!["entity_pk".to_string()],
|
|
2341
|
-
},
|
|
2342
|
-
limit: Some(1),
|
|
2343
|
-
},
|
|
2344
|
-
)
|
|
2345
|
-
.await
|
|
2346
|
-
.expect("limited file scan should apply visibility before limit");
|
|
2347
|
-
|
|
2348
|
-
assert_eq!(rows.len(), 1);
|
|
2349
|
-
assert_eq!(
|
|
2350
|
-
rows[0]
|
|
2351
|
-
.entity_pk
|
|
2352
|
-
.as_single_string_owned()
|
|
2353
|
-
.expect("entity pk"),
|
|
2354
|
-
"entity-b"
|
|
2355
|
-
);
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
#[tokio::test]
|
|
2359
|
-
async fn reads_resolve_json_snapshot_refs() {
|
|
2360
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2361
|
-
let tracked_state = TrackedStateContext::new();
|
|
2362
|
-
let large_value = "x".repeat(1536);
|
|
2363
|
-
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
2364
|
-
write_root_for_test(
|
|
2365
|
-
&storage,
|
|
2366
|
-
&tracked_state,
|
|
2367
|
-
"commit-1",
|
|
2368
|
-
None,
|
|
2369
|
-
std::slice::from_ref(&row),
|
|
2370
|
-
)
|
|
2371
|
-
.await
|
|
2372
|
-
.expect("root should write");
|
|
2373
|
-
|
|
2374
|
-
let mut reader = tracked_state.reader(
|
|
2375
|
-
storage
|
|
2376
|
-
.begin_read(StorageReadOptions::default())
|
|
2377
|
-
.expect("read should open"),
|
|
2378
|
-
);
|
|
2379
|
-
let loaded = reader
|
|
2380
|
-
.load_rows_at_commit(
|
|
2381
|
-
"commit-1",
|
|
2382
|
-
&[TrackedStateKey {
|
|
2383
|
-
schema_key: row.schema_key.clone(),
|
|
2384
|
-
entity_pk: row.entity_pk.clone(),
|
|
2385
|
-
file_id: None,
|
|
2386
|
-
}],
|
|
2387
|
-
)
|
|
2388
|
-
.await
|
|
2389
|
-
.expect("row should load")
|
|
2390
|
-
.pop()
|
|
2391
|
-
.flatten()
|
|
2392
|
-
.expect("row should exist");
|
|
2393
|
-
let scanned = reader
|
|
2394
|
-
.scan_rows_at_commit("commit-1", &test_schema_scan_request())
|
|
2395
|
-
.await
|
|
2396
|
-
.expect("rows should scan");
|
|
2397
|
-
|
|
2398
|
-
assert_eq!(loaded.snapshot_content, row.snapshot_content);
|
|
2399
|
-
assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
#[tokio::test]
|
|
2403
|
-
async fn commit_root_cache_uses_seen_updated_at_not_change_created_at() {
|
|
2404
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2405
|
-
let tracked_state = TrackedStateContext::new();
|
|
2406
|
-
let mut row = row("entity-a", "change-a", "commit-1");
|
|
2407
|
-
row.created_at = "2026-01-01T00:00:00Z".to_string();
|
|
2408
|
-
row.updated_at = "2026-01-02T00:00:00Z".to_string();
|
|
2409
|
-
write_root_for_test(
|
|
2410
|
-
&storage,
|
|
2411
|
-
&tracked_state,
|
|
2412
|
-
"commit-1",
|
|
2413
|
-
None,
|
|
2414
|
-
std::slice::from_ref(&row),
|
|
2415
|
-
)
|
|
2416
|
-
.await
|
|
2417
|
-
.expect("root should write");
|
|
2418
|
-
|
|
2419
|
-
let loaded = tracked_state
|
|
2420
|
-
.reader(
|
|
2421
|
-
storage
|
|
2422
|
-
.begin_read(StorageReadOptions::default())
|
|
2423
|
-
.expect("read should open"),
|
|
2424
|
-
)
|
|
2425
|
-
.load_rows_at_commit(
|
|
2426
|
-
"commit-1",
|
|
2427
|
-
&[TrackedStateKey {
|
|
2428
|
-
schema_key: row.schema_key.clone(),
|
|
2429
|
-
entity_pk: row.entity_pk.clone(),
|
|
2430
|
-
file_id: None,
|
|
2431
|
-
}],
|
|
2432
|
-
)
|
|
2433
|
-
.await
|
|
2434
|
-
.expect("row should load")
|
|
2435
|
-
.pop()
|
|
2436
|
-
.flatten()
|
|
2437
|
-
.expect("row should exist");
|
|
2438
|
-
|
|
2439
|
-
assert_eq!(loaded.created_at, "2026-01-01T00:00:00Z");
|
|
2440
|
-
assert_eq!(loaded.updated_at, "2026-01-02T00:00:00Z");
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
#[tokio::test]
|
|
2444
|
-
async fn updates_preserve_first_visible_created_at_across_rebuild() {
|
|
2445
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2446
|
-
let tracked_state = TrackedStateContext::new();
|
|
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
|
-
}
|
|
2517
|
-
|
|
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])
|
|
2525
|
-
.await
|
|
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);
|
|
2540
|
-
write_root_for_test(
|
|
2541
|
-
&storage,
|
|
2542
|
-
&tracked_state,
|
|
2543
|
-
"commit-1",
|
|
2544
|
-
None,
|
|
2545
|
-
std::slice::from_ref(&row),
|
|
2546
|
-
)
|
|
2547
|
-
.await
|
|
2548
|
-
.expect("root should write");
|
|
2549
|
-
|
|
2550
|
-
let rows = tracked_state
|
|
2551
|
-
.reader(
|
|
2552
|
-
storage
|
|
2553
|
-
.begin_read(StorageReadOptions::default())
|
|
2554
|
-
.expect("read should open"),
|
|
2555
|
-
)
|
|
2556
|
-
.scan_rows_at_commit(
|
|
2557
|
-
"commit-1",
|
|
2558
|
-
&TrackedStateScanRequest {
|
|
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()],
|
|
2565
|
-
},
|
|
2566
|
-
..Default::default()
|
|
2567
|
-
},
|
|
2568
|
-
)
|
|
2569
|
-
.await
|
|
2570
|
-
.expect("rows should scan");
|
|
2571
|
-
|
|
2572
|
-
assert_eq!(rows.len(), 1);
|
|
2573
|
-
assert_eq!(rows[0].snapshot_content, None);
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
async fn seed_merge_roots(
|
|
2577
|
-
base_rows: &[MaterializedTrackedStateRow],
|
|
2578
|
-
target_rows: &[MaterializedTrackedStateRow],
|
|
2579
|
-
source_rows: &[MaterializedTrackedStateRow],
|
|
2580
|
-
) -> (StorageContext, TrackedStateContext) {
|
|
2581
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
2582
|
-
let tracked_state = TrackedStateContext::new();
|
|
2583
|
-
write_root_for_test(&storage, &tracked_state, "base", None, base_rows)
|
|
2584
|
-
.await
|
|
2585
|
-
.expect("base root should write");
|
|
2586
|
-
write_root_for_test(
|
|
2587
|
-
&storage,
|
|
2588
|
-
&tracked_state,
|
|
2589
|
-
"target",
|
|
2590
|
-
Some("base"),
|
|
2591
|
-
target_rows,
|
|
2592
|
-
)
|
|
2593
|
-
.await
|
|
2594
|
-
.expect("target root should write");
|
|
2595
|
-
write_root_for_test(
|
|
2596
|
-
&storage,
|
|
2597
|
-
&tracked_state,
|
|
2598
|
-
"source",
|
|
2599
|
-
Some("base"),
|
|
2600
|
-
source_rows,
|
|
2601
|
-
)
|
|
2602
|
-
.await
|
|
2603
|
-
.expect("source root should write");
|
|
2604
|
-
(storage, tracked_state)
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
fn merge_pick_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
2608
|
-
plan.picks
|
|
2609
|
-
.iter()
|
|
2610
|
-
.map(|entry| {
|
|
2611
|
-
entry
|
|
2612
|
-
.identity()
|
|
2613
|
-
.entity_pk
|
|
2614
|
-
.as_single_string_owned()
|
|
2615
|
-
.expect("identity")
|
|
2616
|
-
})
|
|
2617
|
-
.collect()
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
2621
|
-
plan.conflicts
|
|
2622
|
-
.iter()
|
|
2623
|
-
.map(|entry| {
|
|
2624
|
-
entry
|
|
2625
|
-
.identity
|
|
2626
|
-
.entity_pk
|
|
2627
|
-
.as_single_string_owned()
|
|
2628
|
-
.expect("identity")
|
|
2629
|
-
})
|
|
2630
|
-
.collect()
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
async fn write_root_for_test(
|
|
2634
|
-
storage: &StorageContext,
|
|
2635
|
-
tracked_state: &TrackedStateContext,
|
|
2636
|
-
commit_id: &str,
|
|
2637
|
-
parent_commit_id: Option<&str>,
|
|
2638
|
-
rows: &[MaterializedTrackedStateRow],
|
|
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();
|
|
2644
|
-
crate::test_support::stage_tracked_root_from_materialized(
|
|
2645
|
-
&mut read,
|
|
2646
|
-
&mut writes,
|
|
2647
|
-
tracked_state,
|
|
2648
|
-
commit_id,
|
|
2649
|
-
parent_commit_id,
|
|
2650
|
-
rows,
|
|
2651
|
-
)
|
|
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
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
fn tombstone(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
2773
|
-
let mut row = row(entity_pk, change_id, commit_id);
|
|
2774
|
-
row.snapshot_content = None;
|
|
2775
|
-
row
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
fn row(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
2779
|
-
row_with_value(entity_pk, change_id, commit_id, "value")
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
fn row_with_value(
|
|
2783
|
-
entity_pk: &str,
|
|
2784
|
-
change_id: &str,
|
|
2785
|
-
commit_id: &str,
|
|
2786
|
-
value: &str,
|
|
2787
|
-
) -> MaterializedTrackedStateRow {
|
|
2788
|
-
MaterializedTrackedStateRow {
|
|
2789
|
-
entity_pk: crate::entity_pk::EntityPk::single(entity_pk),
|
|
2790
|
-
schema_key: "test_schema".to_string(),
|
|
2791
|
-
file_id: None,
|
|
2792
|
-
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
2793
|
-
metadata: None,
|
|
2794
|
-
deleted: false,
|
|
2795
|
-
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2796
|
-
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2797
|
-
change_id: change_id.to_string(),
|
|
2798
|
-
commit_id: commit_id.to_string(),
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
}
|