@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +46 -8
- package/dist/engine-wasm/wasm/lix_engine.d.ts +25 -1
- package/dist/engine-wasm/wasm/lix_engine.js +60 -2
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +5 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +10 -3
- package/dist/open-lix.js +39 -0
- package/dist-engine-src/src/binary_cas/types.rs +0 -6
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/mod.rs +1 -1
- package/dist-engine-src/src/cel/provider.rs +1 -1
- package/dist-engine-src/src/commit_graph/context.rs +328 -1015
- package/dist-engine-src/src/commit_graph/mod.rs +2 -3
- package/dist-engine-src/src/commit_graph/types.rs +7 -43
- package/dist-engine-src/src/commit_graph/walker.rs +57 -81
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/identity.rs +15 -5
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +17 -12
- package/dist-engine-src/src/common/mod.rs +5 -5
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +29 -43
- package/dist-engine-src/src/entity_identity.rs +238 -118
- package/dist-engine-src/src/functions/context.rs +17 -52
- package/dist-engine-src/src/functions/deterministic.rs +1 -1
- package/dist-engine-src/src/functions/mod.rs +1 -1
- package/dist-engine-src/src/functions/provider.rs +4 -4
- package/dist-engine-src/src/functions/state.rs +39 -66
- package/dist-engine-src/src/functions/types.rs +1 -1
- package/dist-engine-src/src/init.rs +204 -151
- package/dist-engine-src/src/json_store/context.rs +354 -60
- package/dist-engine-src/src/json_store/encoded.rs +6 -6
- package/dist-engine-src/src/json_store/mod.rs +4 -1
- package/dist-engine-src/src/json_store/store.rs +884 -11
- package/dist-engine-src/src/json_store/types.rs +166 -1
- package/dist-engine-src/src/lib.rs +11 -10
- package/dist-engine-src/src/live_state/context.rs +608 -830
- package/dist-engine-src/src/live_state/mod.rs +3 -3
- package/dist-engine-src/src/live_state/overlay.rs +7 -7
- package/dist-engine-src/src/live_state/reader.rs +5 -5
- package/dist-engine-src/src/live_state/types.rs +19 -36
- package/dist-engine-src/src/live_state/visibility.rs +19 -14
- package/dist-engine-src/src/plugin/archive.rs +3 -6
- package/dist-engine-src/src/plugin/install.rs +0 -18
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
- package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +47 -17
- package/dist-engine-src/src/schema/definition.rs +202 -96
- package/dist-engine-src/src/schema/key.rs +9 -77
- package/dist-engine-src/src/schema/mod.rs +4 -4
- package/dist-engine-src/src/schema/tests.rs +133 -92
- package/dist-engine-src/src/session/context.rs +86 -48
- package/dist-engine-src/src/session/create_version.rs +22 -14
- package/dist-engine-src/src/session/execute.rs +117 -23
- package/dist-engine-src/src/session/merge/apply.rs +4 -4
- package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
- package/dist-engine-src/src/session/merge/stats.rs +1 -1
- package/dist-engine-src/src/session/merge/version.rs +35 -45
- package/dist-engine-src/src/session/mod.rs +9 -7
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +17 -28
- package/dist-engine-src/src/session/transaction.rs +76 -0
- package/dist-engine-src/src/sql2/change_provider.rs +14 -20
- package/dist-engine-src/src/sql2/classify.rs +75 -48
- package/dist-engine-src/src/sql2/context.rs +22 -18
- package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
- package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
- package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
- package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
- package/dist-engine-src/src/sql2/error.rs +24 -5
- package/dist-engine-src/src/sql2/execute.rs +426 -272
- package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
- package/dist-engine-src/src/sql2/file_provider.rs +533 -108
- package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
- package/dist-engine-src/src/sql2/history_projection.rs +3 -27
- package/dist-engine-src/src/sql2/history_provider.rs +11 -17
- package/dist-engine-src/src/sql2/history_route.rs +22 -8
- package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
- package/dist-engine-src/src/sql2/mod.rs +8 -4
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +172 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +26 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +10 -12
- package/dist-engine-src/src/sql2/session.rs +7 -10
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
- package/dist-engine-src/src/sql2/udfs/public_call.rs +238 -0
- package/dist-engine-src/src/sql2/version_provider.rs +46 -31
- package/dist-engine-src/src/sql2/version_scope.rs +4 -4
- package/dist-engine-src/src/storage_bench.rs +1782 -325
- package/dist-engine-src/src/test_support.rs +183 -36
- package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
- package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
- package/dist-engine-src/src/tracked_state/context.rs +1155 -271
- package/dist-engine-src/src/tracked_state/diff.rs +249 -57
- package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +37 -19
- package/dist-engine-src/src/tracked_state/mod.rs +8 -7
- package/dist-engine-src/src/tracked_state/storage.rs +138 -6
- package/dist-engine-src/src/tracked_state/tree.rs +695 -252
- package/dist-engine-src/src/tracked_state/types.rs +176 -6
- package/dist-engine-src/src/transaction/commit.rs +695 -435
- package/dist-engine-src/src/transaction/context.rs +551 -310
- package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
- package/dist-engine-src/src/transaction/mod.rs +2 -0
- package/dist-engine-src/src/transaction/normalization.rs +311 -447
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
- package/dist-engine-src/src/transaction/staging.rs +701 -406
- package/dist-engine-src/src/transaction/types.rs +231 -122
- package/dist-engine-src/src/transaction/validation.rs +2717 -1698
- package/dist-engine-src/src/untracked_state/codec.rs +40 -96
- package/dist-engine-src/src/untracked_state/context.rs +21 -5
- package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
- package/dist-engine-src/src/untracked_state/mod.rs +3 -5
- package/dist-engine-src/src/untracked_state/storage.rs +105 -57
- package/dist-engine-src/src/untracked_state/types.rs +63 -13
- package/dist-engine-src/src/version/context.rs +1 -13
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +3 -2
- package/dist-engine-src/src/version/refs.rs +12 -103
- package/dist-engine-src/src/version/stage_rows.rs +15 -19
- package/package.json +1 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -321
- package/dist-engine-src/src/changelog/context.rs +0 -92
- package/dist-engine-src/src/changelog/materialization.rs +0 -121
- package/dist-engine-src/src/changelog/mod.rs +0 -13
- package/dist-engine-src/src/changelog/reader.rs +0 -20
- package/dist-engine-src/src/changelog/storage.rs +0 -220
- package/dist-engine-src/src/changelog/types.rs +0 -38
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
- package/dist-engine-src/src/schema_registry.rs +0 -294
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
- package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
- package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
use
|
|
2
|
-
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
|
|
3
|
+
use crate::commit_store::CommitStoreContext;
|
|
3
4
|
use crate::storage::{StorageReader, StorageWriteSet};
|
|
4
5
|
use crate::tracked_state::by_file_index::ByFileIndex;
|
|
6
|
+
use crate::tracked_state::codec::{encode_key_ref, encode_value_ref};
|
|
5
7
|
use crate::tracked_state::diff::{diff_commits, TrackedStateDiff, TrackedStateDiffRequest};
|
|
6
|
-
use crate::tracked_state::
|
|
8
|
+
use crate::tracked_state::materialize_index_entries;
|
|
7
9
|
use crate::tracked_state::merge::{self, TrackedStateMergePlan};
|
|
8
|
-
use crate::tracked_state::rebuild::TrackedStateRebuildReport;
|
|
9
10
|
use crate::tracked_state::storage;
|
|
11
|
+
use crate::tracked_state::storage::DeltaJsonPackIndexesRef;
|
|
10
12
|
use crate::tracked_state::tree::TrackedStateTree;
|
|
11
|
-
use crate::tracked_state::
|
|
12
|
-
|
|
13
|
+
use crate::tracked_state::types::{
|
|
14
|
+
TrackedStateIndexValue, TrackedStateKey, TrackedStateKeyRef, TrackedStateMutation,
|
|
15
|
+
TrackedStateTreeDiffEntry, TrackedStateTreeScanRequest,
|
|
16
|
+
};
|
|
17
|
+
use crate::tracked_state::{
|
|
18
|
+
MaterializedTrackedStateRow, TrackedStateDeltaRef, TrackedStateRowRequest,
|
|
19
|
+
TrackedStateScanRequest,
|
|
13
20
|
};
|
|
14
|
-
use crate::tracked_state::{TrackedStateRow, TrackedStateRowRequest, TrackedStateScanRequest};
|
|
15
21
|
use crate::LixError;
|
|
16
22
|
|
|
17
|
-
/// Factory for
|
|
23
|
+
/// Factory for tracked-state readers, delta writers, and projection-root materializers.
|
|
18
24
|
///
|
|
19
25
|
/// Tracked state is stored as content-addressed roots. Version refs
|
|
20
26
|
/// choose which commit/root to read; this context only owns root operations.
|
|
21
27
|
#[derive(Clone)]
|
|
22
28
|
pub(crate) struct TrackedStateContext {
|
|
23
29
|
tree: TrackedStateTree,
|
|
30
|
+
commit_store: CommitStoreContext,
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
impl TrackedStateContext {
|
|
27
34
|
pub(crate) fn new() -> Self {
|
|
28
35
|
Self {
|
|
29
36
|
tree: TrackedStateTree::new(),
|
|
37
|
+
commit_store: CommitStoreContext::new(),
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -38,40 +46,45 @@ impl TrackedStateContext {
|
|
|
38
46
|
TrackedStateStoreReader {
|
|
39
47
|
store,
|
|
40
48
|
tree: self.tree.clone(),
|
|
49
|
+
commit_store: self.commit_store,
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
/// Creates a tracked-state writer
|
|
45
|
-
pub(crate) fn writer(
|
|
53
|
+
/// Creates a tracked-state writer over a caller-owned transaction and write set.
|
|
54
|
+
pub(crate) fn writer<'a, S>(
|
|
55
|
+
&'a self,
|
|
56
|
+
store: &'a mut S,
|
|
57
|
+
writes: &'a mut StorageWriteSet,
|
|
58
|
+
) -> TrackedStateWriter<'a, S>
|
|
59
|
+
where
|
|
60
|
+
S: StorageReader + ?Sized,
|
|
61
|
+
{
|
|
46
62
|
TrackedStateWriter {
|
|
47
63
|
tree: self.tree.clone(),
|
|
64
|
+
store,
|
|
65
|
+
writes,
|
|
48
66
|
}
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
///
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
) ->
|
|
69
|
+
/// Creates an explicit tracked-state projection-root materializer.
|
|
70
|
+
///
|
|
71
|
+
/// Normal commits should use `writer(...).stage_delta(...)`. Materializing a
|
|
72
|
+
/// projection root is a caller-chosen maintenance/read-acceleration step.
|
|
73
|
+
pub(crate) fn materializer<'a, S>(
|
|
74
|
+
&'a self,
|
|
75
|
+
store: &'a mut S,
|
|
76
|
+
writes: &'a mut StorageWriteSet,
|
|
77
|
+
commit_store: &'a CommitStoreContext,
|
|
78
|
+
) -> TrackedStateMaterializer<'a, S>
|
|
61
79
|
where
|
|
62
|
-
R: StorageReader,
|
|
63
80
|
S: StorageReader + ?Sized,
|
|
64
81
|
{
|
|
65
|
-
|
|
66
|
-
self,
|
|
67
|
-
|
|
68
|
-
read_store,
|
|
69
|
-
tracked_store,
|
|
82
|
+
TrackedStateMaterializer {
|
|
83
|
+
tracked_state: self,
|
|
84
|
+
store,
|
|
70
85
|
writes,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
.await
|
|
86
|
+
commit_store,
|
|
87
|
+
}
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
@@ -79,6 +92,7 @@ impl TrackedStateContext {
|
|
|
79
92
|
pub(crate) struct TrackedStateStoreReader<S> {
|
|
80
93
|
store: S,
|
|
81
94
|
tree: TrackedStateTree,
|
|
95
|
+
commit_store: CommitStoreContext,
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
impl<S> TrackedStateStoreReader<S>
|
|
@@ -89,67 +103,84 @@ where
|
|
|
89
103
|
&mut self,
|
|
90
104
|
commit_id: &str,
|
|
91
105
|
request: &TrackedStateScanRequest,
|
|
92
|
-
) -> Result<Vec<
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
107
|
+
let root_id = self.tree.load_root(&mut self.store, commit_id).await?;
|
|
108
|
+
let rows = if let Some(root_id) = root_id {
|
|
109
|
+
if ByFileIndex::should_use(request) {
|
|
110
|
+
if let Some(by_file_root_id) =
|
|
111
|
+
storage::load_by_file_root(&mut self.store, commit_id).await?
|
|
112
|
+
{
|
|
113
|
+
self.scan_rows_at_commit_by_file_index(&root_id, &by_file_root_id, request)
|
|
114
|
+
.await?
|
|
115
|
+
} else {
|
|
116
|
+
self.tree
|
|
117
|
+
.scan(
|
|
118
|
+
&mut self.store,
|
|
119
|
+
&root_id,
|
|
120
|
+
&tree_scan_request_from_tracked(request),
|
|
121
|
+
)
|
|
122
|
+
.await?
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
self.tree
|
|
126
|
+
.scan(
|
|
127
|
+
&mut self.store,
|
|
128
|
+
&root_id,
|
|
129
|
+
&tree_scan_request_from_tracked(request),
|
|
130
|
+
)
|
|
131
|
+
.await?
|
|
132
|
+
}
|
|
104
133
|
} else {
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
.scan(
|
|
108
|
-
&mut self.store,
|
|
109
|
-
&root_id,
|
|
110
|
-
&tree_scan_request_from_tracked(request),
|
|
111
|
-
)
|
|
112
|
-
.await?;
|
|
113
|
-
rows
|
|
134
|
+
self.projection_entries_at_commit(commit_id, &tree_scan_request_from_tracked(request))
|
|
135
|
+
.await?
|
|
114
136
|
};
|
|
115
137
|
let projection = crate::tracked_state::TrackedMaterializationProjection::from_columns(
|
|
116
138
|
&request.projection.columns,
|
|
117
139
|
);
|
|
118
|
-
let mut
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
let mut rows = materialize_index_entries(&mut self.store, rows, &projection).await?;
|
|
141
|
+
if !request.filter.include_tombstones {
|
|
142
|
+
rows.retain(|row| !row.deleted);
|
|
143
|
+
}
|
|
144
|
+
if let Some(limit) = request.limit {
|
|
145
|
+
rows.truncate(limit);
|
|
122
146
|
}
|
|
123
|
-
Ok(
|
|
147
|
+
Ok(rows)
|
|
124
148
|
}
|
|
125
149
|
|
|
126
|
-
pub(crate) async fn
|
|
150
|
+
pub(crate) async fn load_rows_at_commit(
|
|
127
151
|
&mut self,
|
|
128
152
|
commit_id: &str,
|
|
129
|
-
|
|
130
|
-
) -> Result<Option<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return Ok(None);
|
|
134
|
-
};
|
|
135
|
-
let row = self
|
|
136
|
-
.tree
|
|
137
|
-
.get(&mut self.store, &root_id, &key)
|
|
138
|
-
.await?
|
|
139
|
-
.map(|value| async {
|
|
140
|
-
let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
|
|
141
|
-
materialize_value(
|
|
142
|
-
&mut json_reader,
|
|
143
|
-
key,
|
|
144
|
-
value,
|
|
145
|
-
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
146
|
-
)
|
|
147
|
-
.await
|
|
148
|
-
});
|
|
149
|
-
match row {
|
|
150
|
-
Some(row) => row.await.map(Some),
|
|
151
|
-
None => Ok(None),
|
|
153
|
+
requests: &[TrackedStateRowRequest],
|
|
154
|
+
) -> Result<Vec<Option<MaterializedTrackedStateRow>>, LixError> {
|
|
155
|
+
if requests.is_empty() {
|
|
156
|
+
return Ok(Vec::new());
|
|
152
157
|
}
|
|
158
|
+
let keys = requests
|
|
159
|
+
.iter()
|
|
160
|
+
.map(tracked_key_from_request)
|
|
161
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
162
|
+
let values = self
|
|
163
|
+
.projection_values_at_commit_for_keys(commit_id, &keys)
|
|
164
|
+
.await?;
|
|
165
|
+
let mut entry_indices = Vec::new();
|
|
166
|
+
let mut entries = Vec::new();
|
|
167
|
+
for (index, (key, value)) in keys.into_iter().zip(values).enumerate() {
|
|
168
|
+
if let Some(value) = value {
|
|
169
|
+
entry_indices.push(index);
|
|
170
|
+
entries.push((key, value));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let materialized = materialize_index_entries(
|
|
174
|
+
&mut self.store,
|
|
175
|
+
entries,
|
|
176
|
+
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
177
|
+
)
|
|
178
|
+
.await?;
|
|
179
|
+
let mut rows = vec![None; requests.len()];
|
|
180
|
+
for (index, row) in entry_indices.into_iter().zip(materialized) {
|
|
181
|
+
rows[index] = Some(row);
|
|
182
|
+
}
|
|
183
|
+
Ok(rows)
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
pub(crate) async fn diff_commits(
|
|
@@ -166,34 +197,168 @@ where
|
|
|
166
197
|
left_commit_id: &str,
|
|
167
198
|
right_commit_id: &str,
|
|
168
199
|
request: &TrackedStateTreeScanRequest,
|
|
169
|
-
) -> Result<Vec<crate::tracked_state::
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
200
|
+
) -> Result<Vec<crate::tracked_state::types::TrackedStateTreeDiffEntry>, LixError> {
|
|
201
|
+
if !self.projection_has_pending_deltas(left_commit_id).await?
|
|
202
|
+
&& !self.projection_has_pending_deltas(right_commit_id).await?
|
|
203
|
+
&& self.projection_root_exists(left_commit_id).await?
|
|
204
|
+
&& self.projection_root_exists(right_commit_id).await?
|
|
205
|
+
{
|
|
206
|
+
let left_root = self.tree.load_root(&mut self.store, left_commit_id).await?;
|
|
207
|
+
let right_root = self
|
|
208
|
+
.tree
|
|
209
|
+
.load_root(&mut self.store, right_commit_id)
|
|
210
|
+
.await?;
|
|
211
|
+
let entries = self
|
|
212
|
+
.tree
|
|
213
|
+
.diff(
|
|
214
|
+
&mut self.store,
|
|
215
|
+
left_root.as_ref(),
|
|
216
|
+
right_root.as_ref(),
|
|
217
|
+
request,
|
|
218
|
+
)
|
|
219
|
+
.await?;
|
|
220
|
+
return Ok(entries);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if let Some(entries) = self
|
|
224
|
+
.diff_pending_delta_suffix(left_commit_id, right_commit_id, request)
|
|
225
|
+
.await?
|
|
226
|
+
{
|
|
227
|
+
return Ok(entries);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let left = self
|
|
231
|
+
.projection_entries_at_commit(left_commit_id, request)
|
|
232
|
+
.await?
|
|
233
|
+
.into_iter()
|
|
234
|
+
.collect::<BTreeMap<_, _>>();
|
|
235
|
+
let right = self
|
|
236
|
+
.projection_entries_at_commit(right_commit_id, request)
|
|
237
|
+
.await?
|
|
238
|
+
.into_iter()
|
|
239
|
+
.collect::<BTreeMap<_, _>>();
|
|
240
|
+
let keys = left
|
|
241
|
+
.keys()
|
|
242
|
+
.chain(right.keys())
|
|
243
|
+
.cloned()
|
|
244
|
+
.collect::<BTreeSet<_>>();
|
|
245
|
+
let entries = keys
|
|
246
|
+
.into_iter()
|
|
247
|
+
.filter_map(|key| {
|
|
248
|
+
let before = left.get(&key).cloned().map(|value| (key.clone(), value));
|
|
249
|
+
let after = right.get(&key).cloned().map(|value| (key, value));
|
|
250
|
+
if before == after {
|
|
251
|
+
None
|
|
252
|
+
} else {
|
|
253
|
+
Some(TrackedStateTreeDiffEntry { before, after })
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.collect();
|
|
257
|
+
Ok(entries)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async fn diff_pending_delta_suffix(
|
|
261
|
+
&mut self,
|
|
262
|
+
left_commit_id: &str,
|
|
263
|
+
right_commit_id: &str,
|
|
264
|
+
request: &TrackedStateTreeScanRequest,
|
|
265
|
+
) -> Result<Option<Vec<TrackedStateTreeDiffEntry>>, LixError> {
|
|
266
|
+
let left_delta_ids = self
|
|
267
|
+
.delta_commit_ids_since_projection_root(left_commit_id)
|
|
174
268
|
.await?;
|
|
175
|
-
let
|
|
176
|
-
.
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
269
|
+
let right_delta_ids = self
|
|
270
|
+
.delta_commit_ids_since_projection_root(right_commit_id)
|
|
271
|
+
.await?;
|
|
272
|
+
let left_base_commit_id = self
|
|
273
|
+
.projection_base_commit_id(left_commit_id, &left_delta_ids)
|
|
274
|
+
.await?;
|
|
275
|
+
let right_base_commit_id = self
|
|
276
|
+
.projection_base_commit_id(right_commit_id, &right_delta_ids)
|
|
183
277
|
.await?;
|
|
278
|
+
if left_base_commit_id != right_base_commit_id {
|
|
279
|
+
return Ok(None);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if right_delta_ids.starts_with(&left_delta_ids) {
|
|
283
|
+
let suffix = &right_delta_ids[left_delta_ids.len()..];
|
|
284
|
+
return self
|
|
285
|
+
.diff_pending_delta_suffix_from_base(left_commit_id, suffix, request, true)
|
|
286
|
+
.await
|
|
287
|
+
.map(Some);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if left_delta_ids.starts_with(&right_delta_ids) {
|
|
291
|
+
let suffix = &left_delta_ids[right_delta_ids.len()..];
|
|
292
|
+
return self
|
|
293
|
+
.diff_pending_delta_suffix_from_base(right_commit_id, suffix, request, false)
|
|
294
|
+
.await
|
|
295
|
+
.map(Some);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
Ok(None)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async fn diff_pending_delta_suffix_from_base(
|
|
302
|
+
&mut self,
|
|
303
|
+
base_commit_id: &str,
|
|
304
|
+
suffix_commit_ids: &[String],
|
|
305
|
+
request: &TrackedStateTreeScanRequest,
|
|
306
|
+
suffix_is_after: bool,
|
|
307
|
+
) -> Result<Vec<TrackedStateTreeDiffEntry>, LixError> {
|
|
308
|
+
if suffix_commit_ids.is_empty() {
|
|
309
|
+
return Ok(Vec::new());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let mut changed = BTreeMap::<TrackedStateKey, TrackedStateIndexValue>::new();
|
|
313
|
+
for commit_id in suffix_commit_ids {
|
|
314
|
+
let Some(delta_entries) = storage::load_delta_pack(&mut self.store, commit_id).await?
|
|
315
|
+
else {
|
|
316
|
+
continue;
|
|
317
|
+
};
|
|
318
|
+
for delta in delta_entries {
|
|
319
|
+
if request.matches_key(&delta.key) {
|
|
320
|
+
changed.insert(delta.key, delta.value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if changed.is_empty() {
|
|
326
|
+
return Ok(Vec::new());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let keys = changed.keys().cloned().collect::<Vec<_>>();
|
|
330
|
+
let base_values = self
|
|
331
|
+
.projection_values_at_commit_for_keys(base_commit_id, &keys)
|
|
332
|
+
.await?;
|
|
333
|
+
let entries = keys
|
|
334
|
+
.into_iter()
|
|
335
|
+
.zip(base_values)
|
|
336
|
+
.filter_map(|(key, base_value)| {
|
|
337
|
+
let changed_value = changed.get(&key).cloned();
|
|
338
|
+
let (before_value, after_value) = if suffix_is_after {
|
|
339
|
+
(base_value, changed_value)
|
|
340
|
+
} else {
|
|
341
|
+
(changed_value, base_value)
|
|
342
|
+
};
|
|
343
|
+
if before_value == after_value {
|
|
344
|
+
return None;
|
|
345
|
+
}
|
|
346
|
+
Some(TrackedStateTreeDiffEntry {
|
|
347
|
+
before: before_value.map(|value| (key.clone(), value)),
|
|
348
|
+
after: after_value.map(|value| (key, value)),
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
.collect();
|
|
184
352
|
Ok(entries)
|
|
185
353
|
}
|
|
186
354
|
|
|
187
|
-
pub(crate) async fn
|
|
355
|
+
pub(crate) async fn materialize_tree_values(
|
|
188
356
|
&mut self,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
&mut json_reader,
|
|
195
|
-
key,
|
|
196
|
-
value,
|
|
357
|
+
entries: Vec<(TrackedStateKey, TrackedStateIndexValue)>,
|
|
358
|
+
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
359
|
+
materialize_index_entries(
|
|
360
|
+
&mut self.store,
|
|
361
|
+
entries,
|
|
197
362
|
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
198
363
|
)
|
|
199
364
|
.await
|
|
@@ -201,10 +366,10 @@ where
|
|
|
201
366
|
|
|
202
367
|
async fn scan_rows_at_commit_by_file_index(
|
|
203
368
|
&mut self,
|
|
204
|
-
primary_root_id: &crate::tracked_state::
|
|
205
|
-
by_file_root_id: &crate::tracked_state::
|
|
369
|
+
primary_root_id: &crate::tracked_state::types::TrackedStateRootId,
|
|
370
|
+
by_file_root_id: &crate::tracked_state::types::TrackedStateRootId,
|
|
206
371
|
request: &TrackedStateScanRequest,
|
|
207
|
-
) -> Result<Vec<(TrackedStateKey,
|
|
372
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
208
373
|
let by_file_request = ByFileIndex::scan_request_from_tracked(request);
|
|
209
374
|
let index_match_count = self
|
|
210
375
|
.tree
|
|
@@ -244,9 +409,6 @@ where
|
|
|
244
409
|
.get_many(&mut self.store, primary_root_id, &primary_keys)
|
|
245
410
|
.await?;
|
|
246
411
|
for (primary_key, value) in primary_keys.into_iter().zip(primary_values) {
|
|
247
|
-
if request.limit.is_some_and(|limit| rows.len() >= limit) {
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
412
|
let Some(value) = value else {
|
|
251
413
|
continue;
|
|
252
414
|
};
|
|
@@ -259,9 +421,6 @@ where
|
|
|
259
421
|
}
|
|
260
422
|
|
|
261
423
|
for (index_key, index_value) in index_rows {
|
|
262
|
-
if request.limit.is_some_and(|limit| rows.len() >= limit) {
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
424
|
let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) else {
|
|
266
425
|
continue;
|
|
267
426
|
};
|
|
@@ -273,6 +432,254 @@ where
|
|
|
273
432
|
Ok(rows)
|
|
274
433
|
}
|
|
275
434
|
|
|
435
|
+
async fn projection_root_exists(&mut self, commit_id: &str) -> Result<bool, LixError> {
|
|
436
|
+
Ok(self
|
|
437
|
+
.tree
|
|
438
|
+
.load_root(&mut self.store, commit_id)
|
|
439
|
+
.await?
|
|
440
|
+
.is_some())
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async fn projection_has_pending_deltas(&mut self, commit_id: &str) -> Result<bool, LixError> {
|
|
444
|
+
Ok(!self
|
|
445
|
+
.delta_commit_ids_since_projection_root(commit_id)
|
|
446
|
+
.await?
|
|
447
|
+
.is_empty())
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async fn projection_entries_at_commit(
|
|
451
|
+
&mut self,
|
|
452
|
+
commit_id: &str,
|
|
453
|
+
request: &TrackedStateTreeScanRequest,
|
|
454
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
455
|
+
let delta_commit_ids = self
|
|
456
|
+
.delta_commit_ids_since_projection_root(commit_id)
|
|
457
|
+
.await?;
|
|
458
|
+
let base_commit_id = self
|
|
459
|
+
.projection_base_commit_id(commit_id, &delta_commit_ids)
|
|
460
|
+
.await?;
|
|
461
|
+
if base_commit_id.is_none() && delta_commit_ids.len() == 1 {
|
|
462
|
+
return self
|
|
463
|
+
.single_delta_pack_entries(&delta_commit_ids[0], request)
|
|
464
|
+
.await;
|
|
465
|
+
}
|
|
466
|
+
let mut entries = if let Some(base_commit_id) = base_commit_id {
|
|
467
|
+
let root_id = self
|
|
468
|
+
.tree
|
|
469
|
+
.load_root(&mut self.store, &base_commit_id)
|
|
470
|
+
.await?
|
|
471
|
+
.ok_or_else(|| {
|
|
472
|
+
LixError::new(
|
|
473
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
474
|
+
format!(
|
|
475
|
+
"tracked_state projection base root '{base_commit_id}' disappeared"
|
|
476
|
+
),
|
|
477
|
+
)
|
|
478
|
+
})?;
|
|
479
|
+
self.tree
|
|
480
|
+
.scan(&mut self.store, &root_id, request)
|
|
481
|
+
.await?
|
|
482
|
+
.into_iter()
|
|
483
|
+
.collect::<BTreeMap<_, _>>()
|
|
484
|
+
} else {
|
|
485
|
+
BTreeMap::new()
|
|
486
|
+
};
|
|
487
|
+
self.apply_delta_packs_to_entries(&delta_commit_ids, Some(request), &mut entries)
|
|
488
|
+
.await?;
|
|
489
|
+
Ok(entries.into_iter().collect())
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async fn single_delta_pack_entries(
|
|
493
|
+
&mut self,
|
|
494
|
+
commit_id: &str,
|
|
495
|
+
request: &TrackedStateTreeScanRequest,
|
|
496
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
497
|
+
let Some(delta_entries) = storage::load_delta_pack(&mut self.store, commit_id).await?
|
|
498
|
+
else {
|
|
499
|
+
return Ok(Vec::new());
|
|
500
|
+
};
|
|
501
|
+
let mut rows = delta_entries
|
|
502
|
+
.into_iter()
|
|
503
|
+
.enumerate()
|
|
504
|
+
.filter_map(|(ordinal, delta)| {
|
|
505
|
+
request
|
|
506
|
+
.matches_key(&delta.key)
|
|
507
|
+
.then_some((ordinal, delta.key, delta.value))
|
|
508
|
+
})
|
|
509
|
+
.collect::<Vec<_>>();
|
|
510
|
+
rows.sort_by(|left, right| left.1.cmp(&right.1).then(left.0.cmp(&right.0)));
|
|
511
|
+
|
|
512
|
+
let mut out = Vec::new();
|
|
513
|
+
let mut rows = rows.into_iter().peekable();
|
|
514
|
+
while let Some((_, key, mut value)) = rows.next() {
|
|
515
|
+
while rows.peek().is_some_and(|(_, next_key, _)| next_key == &key) {
|
|
516
|
+
let (_, _, next_value) = rows
|
|
517
|
+
.next()
|
|
518
|
+
.expect("peek confirmed duplicate delta entry exists");
|
|
519
|
+
value = next_value;
|
|
520
|
+
}
|
|
521
|
+
if !request.include_tombstones && value.deleted {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
out.push((key, value));
|
|
525
|
+
}
|
|
526
|
+
Ok(out)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async fn projection_values_at_commit_for_keys(
|
|
530
|
+
&mut self,
|
|
531
|
+
commit_id: &str,
|
|
532
|
+
keys: &[TrackedStateKey],
|
|
533
|
+
) -> Result<Vec<Option<TrackedStateIndexValue>>, LixError> {
|
|
534
|
+
let delta_commit_ids = self
|
|
535
|
+
.delta_commit_ids_since_projection_root(commit_id)
|
|
536
|
+
.await?;
|
|
537
|
+
let base_commit_id = self
|
|
538
|
+
.projection_base_commit_id(commit_id, &delta_commit_ids)
|
|
539
|
+
.await?;
|
|
540
|
+
let mut entries = if let Some(base_commit_id) = base_commit_id {
|
|
541
|
+
let root_id = self
|
|
542
|
+
.tree
|
|
543
|
+
.load_root(&mut self.store, &base_commit_id)
|
|
544
|
+
.await?
|
|
545
|
+
.ok_or_else(|| {
|
|
546
|
+
LixError::new(
|
|
547
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
548
|
+
format!(
|
|
549
|
+
"tracked_state projection base root '{base_commit_id}' disappeared"
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
})?;
|
|
553
|
+
let values = self.tree.get_many(&mut self.store, &root_id, keys).await?;
|
|
554
|
+
keys.iter()
|
|
555
|
+
.cloned()
|
|
556
|
+
.zip(values)
|
|
557
|
+
.filter_map(|(key, value)| value.map(|value| (key, value)))
|
|
558
|
+
.collect::<BTreeMap<_, _>>()
|
|
559
|
+
} else {
|
|
560
|
+
BTreeMap::new()
|
|
561
|
+
};
|
|
562
|
+
let key_filter = keys.iter().cloned().collect::<BTreeSet<_>>();
|
|
563
|
+
self.apply_delta_packs_to_entries_for_keys(&delta_commit_ids, &key_filter, &mut entries)
|
|
564
|
+
.await?;
|
|
565
|
+
Ok(keys.iter().map(|key| entries.get(key).cloned()).collect())
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async fn projection_base_commit_id(
|
|
569
|
+
&mut self,
|
|
570
|
+
commit_id: &str,
|
|
571
|
+
delta_commit_ids: &[String],
|
|
572
|
+
) -> Result<Option<String>, LixError> {
|
|
573
|
+
if delta_commit_ids.is_empty() {
|
|
574
|
+
return Ok(if self.projection_root_exists(commit_id).await? {
|
|
575
|
+
Some(commit_id.to_string())
|
|
576
|
+
} else {
|
|
577
|
+
None
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
let Some(first_delta_commit_id) = delta_commit_ids.first() else {
|
|
581
|
+
return Ok(None);
|
|
582
|
+
};
|
|
583
|
+
let commit = self
|
|
584
|
+
.commit_store
|
|
585
|
+
.load_commit_from(&mut self.store, first_delta_commit_id)
|
|
586
|
+
.await?
|
|
587
|
+
.ok_or_else(|| missing_commit_error(first_delta_commit_id))?;
|
|
588
|
+
let Some(parent_id) = commit.parent_ids.first() else {
|
|
589
|
+
return Ok(None);
|
|
590
|
+
};
|
|
591
|
+
Ok(if self.projection_root_exists(parent_id).await? {
|
|
592
|
+
Some(parent_id.clone())
|
|
593
|
+
} else {
|
|
594
|
+
None
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async fn delta_commit_ids_since_projection_root(
|
|
599
|
+
&mut self,
|
|
600
|
+
commit_id: &str,
|
|
601
|
+
) -> Result<Vec<String>, LixError> {
|
|
602
|
+
let mut out = Vec::new();
|
|
603
|
+
let mut seen = BTreeSet::new();
|
|
604
|
+
let mut current = Some(commit_id.to_string());
|
|
605
|
+
while let Some(current_id) = current {
|
|
606
|
+
if !seen.insert(current_id.clone()) {
|
|
607
|
+
return Err(LixError::new(
|
|
608
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
609
|
+
format!("tracked_state projection found first-parent cycle at '{current_id}'"),
|
|
610
|
+
));
|
|
611
|
+
}
|
|
612
|
+
if self
|
|
613
|
+
.tree
|
|
614
|
+
.load_root(&mut self.store, ¤t_id)
|
|
615
|
+
.await?
|
|
616
|
+
.is_some()
|
|
617
|
+
{
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
if storage::delta_pack_exists(&mut self.store, ¤t_id).await? {
|
|
621
|
+
out.push(current_id.clone());
|
|
622
|
+
}
|
|
623
|
+
let commit = self
|
|
624
|
+
.commit_store
|
|
625
|
+
.load_commit_from(&mut self.store, ¤t_id)
|
|
626
|
+
.await?
|
|
627
|
+
.ok_or_else(|| missing_commit_error(¤t_id))?;
|
|
628
|
+
current = commit.parent_ids.first().cloned();
|
|
629
|
+
}
|
|
630
|
+
out.reverse();
|
|
631
|
+
Ok(out)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async fn apply_delta_packs_to_entries(
|
|
635
|
+
&mut self,
|
|
636
|
+
commit_ids: &[String],
|
|
637
|
+
request: Option<&TrackedStateTreeScanRequest>,
|
|
638
|
+
entries: &mut BTreeMap<TrackedStateKey, TrackedStateIndexValue>,
|
|
639
|
+
) -> Result<(), LixError> {
|
|
640
|
+
for commit_id in commit_ids {
|
|
641
|
+
let Some(delta_entries) = storage::load_delta_pack(&mut self.store, commit_id).await?
|
|
642
|
+
else {
|
|
643
|
+
continue;
|
|
644
|
+
};
|
|
645
|
+
for delta in delta_entries {
|
|
646
|
+
if let Some(request) = request {
|
|
647
|
+
if !request.matches_key(&delta.key) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if !request.include_tombstones && delta.value.deleted {
|
|
651
|
+
entries.remove(&delta.key);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
entries.insert(delta.key, delta.value);
|
|
655
|
+
} else {
|
|
656
|
+
entries.insert(delta.key, delta.value);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
Ok(())
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async fn apply_delta_packs_to_entries_for_keys(
|
|
664
|
+
&mut self,
|
|
665
|
+
commit_ids: &[String],
|
|
666
|
+
keys: &BTreeSet<TrackedStateKey>,
|
|
667
|
+
entries: &mut BTreeMap<TrackedStateKey, TrackedStateIndexValue>,
|
|
668
|
+
) -> Result<(), LixError> {
|
|
669
|
+
for commit_id in commit_ids {
|
|
670
|
+
let Some(delta_entries) = storage::load_delta_pack(&mut self.store, commit_id).await?
|
|
671
|
+
else {
|
|
672
|
+
continue;
|
|
673
|
+
};
|
|
674
|
+
for delta in delta_entries {
|
|
675
|
+
if keys.contains(&delta.key) {
|
|
676
|
+
entries.insert(delta.key, delta.value);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
Ok(())
|
|
681
|
+
}
|
|
682
|
+
|
|
276
683
|
/// Plans a three-way merge by diffing both heads against the same base.
|
|
277
684
|
///
|
|
278
685
|
/// `target_commit_id` is the destination root that should keep its own
|
|
@@ -294,39 +701,89 @@ where
|
|
|
294
701
|
.await?;
|
|
295
702
|
merge::plan_merge(&target_diff, &source_diff)
|
|
296
703
|
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// Writer for commit-store-backed tracked-state projection roots.
|
|
707
|
+
pub(crate) struct TrackedStateWriter<'a, S: ?Sized> {
|
|
708
|
+
tree: TrackedStateTree,
|
|
709
|
+
store: &'a mut S,
|
|
710
|
+
writes: &'a mut StorageWriteSet,
|
|
711
|
+
}
|
|
297
712
|
|
|
298
|
-
|
|
299
|
-
|
|
713
|
+
/// Explicit projection-root materializer created by `TrackedStateContext`.
|
|
714
|
+
pub(crate) struct TrackedStateMaterializer<'a, S: ?Sized> {
|
|
715
|
+
pub(super) tracked_state: &'a TrackedStateContext,
|
|
716
|
+
pub(super) store: &'a mut S,
|
|
717
|
+
pub(super) writes: &'a mut StorageWriteSet,
|
|
718
|
+
pub(super) commit_store: &'a CommitStoreContext,
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
impl<S> TrackedStateMaterializer<'_, S>
|
|
722
|
+
where
|
|
723
|
+
S: StorageReader + ?Sized,
|
|
724
|
+
{
|
|
725
|
+
pub(crate) async fn materialize_root_at(
|
|
300
726
|
&mut self,
|
|
301
727
|
commit_id: &str,
|
|
302
|
-
) -> Result<
|
|
303
|
-
|
|
728
|
+
) -> Result<TrackedStateWriteReport, LixError> {
|
|
729
|
+
crate::tracked_state::materializer::materialize_root_at(self, commit_id).await
|
|
304
730
|
}
|
|
305
731
|
}
|
|
306
732
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
733
|
+
impl<S> TrackedStateWriter<'_, S>
|
|
734
|
+
where
|
|
735
|
+
S: StorageReader + ?Sized,
|
|
736
|
+
{
|
|
737
|
+
/// Stages one tracked-state projection delta for `commit_id`.
|
|
738
|
+
pub(crate) async fn stage_delta(
|
|
739
|
+
&mut self,
|
|
740
|
+
commit_id: &str,
|
|
741
|
+
_parent_commit_id: Option<&str>,
|
|
742
|
+
deltas: &[TrackedStateDeltaRef<'_>],
|
|
743
|
+
) -> Result<TrackedStateWriteReport, LixError> {
|
|
744
|
+
storage::stage_delta_pack_refs(self.writes, commit_id, deltas)?;
|
|
745
|
+
Ok(TrackedStateWriteReport {
|
|
746
|
+
commit_id: commit_id.to_string(),
|
|
747
|
+
changed_rows: deltas.len(),
|
|
748
|
+
primary_chunk_puts: 0,
|
|
749
|
+
by_file_chunk_puts: 0,
|
|
750
|
+
})
|
|
751
|
+
}
|
|
311
752
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
753
|
+
pub(crate) async fn stage_delta_with_json_pack_indexes(
|
|
754
|
+
&mut self,
|
|
755
|
+
commit_id: &str,
|
|
756
|
+
_parent_commit_id: Option<&str>,
|
|
757
|
+
deltas: &[TrackedStateDeltaRef<'_>],
|
|
758
|
+
json_pack_indexes: DeltaJsonPackIndexesRef<'_>,
|
|
759
|
+
) -> Result<TrackedStateWriteReport, LixError> {
|
|
760
|
+
storage::stage_delta_pack_refs_with_json_pack_indexes(
|
|
761
|
+
self.writes,
|
|
762
|
+
commit_id,
|
|
763
|
+
deltas,
|
|
764
|
+
json_pack_indexes,
|
|
765
|
+
)?;
|
|
766
|
+
Ok(TrackedStateWriteReport {
|
|
767
|
+
commit_id: commit_id.to_string(),
|
|
768
|
+
changed_rows: deltas.len(),
|
|
769
|
+
primary_chunk_puts: 0,
|
|
770
|
+
by_file_chunk_puts: 0,
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
pub(crate) async fn stage_projection_root<'a, I>(
|
|
319
775
|
&mut self,
|
|
320
|
-
store: &mut (impl StorageReader + ?Sized),
|
|
321
|
-
writes: &mut StorageWriteSet,
|
|
322
|
-
json_writer: &mut JsonStoreWriter,
|
|
323
776
|
commit_id: &str,
|
|
324
777
|
parent_commit_id: Option<&str>,
|
|
325
|
-
|
|
326
|
-
) -> Result<
|
|
778
|
+
deltas: I,
|
|
779
|
+
) -> Result<TrackedStateWriteReport, LixError>
|
|
780
|
+
where
|
|
781
|
+
I: IntoIterator<Item = TrackedStateDeltaRef<'a>>,
|
|
782
|
+
{
|
|
783
|
+
let deltas = deltas.into_iter().collect::<Vec<_>>();
|
|
327
784
|
let base_root = match parent_commit_id {
|
|
328
785
|
Some(parent_commit_id) => {
|
|
329
|
-
let Some(root) = self.tree.load_root(store, parent_commit_id).await? else {
|
|
786
|
+
let Some(root) = self.tree.load_root(self.store, parent_commit_id).await? else {
|
|
330
787
|
return Err(LixError::new(
|
|
331
788
|
"LIX_ERROR_UNKNOWN",
|
|
332
789
|
format!(
|
|
@@ -338,22 +795,31 @@ impl TrackedStateWriter {
|
|
|
338
795
|
}
|
|
339
796
|
None => None,
|
|
340
797
|
};
|
|
341
|
-
let mut
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
798
|
+
let mut mutations = Vec::with_capacity(deltas.len());
|
|
799
|
+
for delta in &deltas {
|
|
800
|
+
let key = TrackedStateKeyRef {
|
|
801
|
+
schema_key: delta.change.schema_key,
|
|
802
|
+
file_id: delta.change.file_id,
|
|
803
|
+
entity_id: delta.change.entity_id,
|
|
804
|
+
};
|
|
805
|
+
let value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
806
|
+
change_locator: delta.locator,
|
|
807
|
+
deleted: delta.change.snapshot_ref.is_none(),
|
|
808
|
+
snapshot_ref: delta.change.snapshot_ref,
|
|
809
|
+
metadata_ref: delta.change.metadata_ref,
|
|
810
|
+
created_at: delta.created_at,
|
|
811
|
+
updated_at: delta.updated_at,
|
|
812
|
+
};
|
|
813
|
+
mutations.push(TrackedStateMutation::put_encoded(
|
|
814
|
+
encode_key_ref(key),
|
|
815
|
+
encode_value_ref(value),
|
|
349
816
|
));
|
|
350
|
-
stored_rows.push((row, stored_value));
|
|
351
817
|
}
|
|
352
818
|
let result = self
|
|
353
819
|
.tree
|
|
354
820
|
.apply_mutations(
|
|
355
|
-
store,
|
|
356
|
-
writes,
|
|
821
|
+
self.store,
|
|
822
|
+
self.writes,
|
|
357
823
|
base_root.as_ref(),
|
|
358
824
|
mutations,
|
|
359
825
|
Some(commit_id),
|
|
@@ -361,62 +827,76 @@ impl TrackedStateWriter {
|
|
|
361
827
|
.await?;
|
|
362
828
|
|
|
363
829
|
let by_file_base_root = match parent_commit_id {
|
|
364
|
-
Some(parent_commit_id) =>
|
|
365
|
-
.await?
|
|
366
|
-
|
|
367
|
-
LixError::new(
|
|
368
|
-
"LIX_ERROR_UNKNOWN",
|
|
369
|
-
format!(
|
|
370
|
-
"tracked-state by-file parent root for commit '{parent_commit_id}' is missing"
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
})
|
|
374
|
-
.map(Some)?,
|
|
830
|
+
Some(parent_commit_id) => {
|
|
831
|
+
storage::load_by_file_root(self.store, parent_commit_id).await?
|
|
832
|
+
}
|
|
375
833
|
None => None,
|
|
376
834
|
};
|
|
377
|
-
let
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
))
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
835
|
+
let concrete_file_deltas = deltas
|
|
836
|
+
.iter()
|
|
837
|
+
.filter(|delta| delta.change.file_id.is_some())
|
|
838
|
+
.collect::<Vec<_>>();
|
|
839
|
+
let by_file_chunk_puts = if concrete_file_deltas.is_empty() {
|
|
840
|
+
if let Some(by_file_base_root) = by_file_base_root.as_ref() {
|
|
841
|
+
storage::stage_by_file_root(self.writes, commit_id, by_file_base_root);
|
|
842
|
+
}
|
|
843
|
+
0
|
|
844
|
+
} else {
|
|
845
|
+
let mut by_file_mutations = Vec::with_capacity(concrete_file_deltas.len());
|
|
846
|
+
for delta in concrete_file_deltas {
|
|
847
|
+
let key = TrackedStateKeyRef {
|
|
848
|
+
schema_key: delta.change.schema_key,
|
|
849
|
+
file_id: delta.change.file_id,
|
|
850
|
+
entity_id: delta.change.entity_id,
|
|
851
|
+
};
|
|
852
|
+
let header_value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
853
|
+
change_locator: delta.locator,
|
|
854
|
+
deleted: delta.change.snapshot_ref.is_none(),
|
|
855
|
+
snapshot_ref: None,
|
|
856
|
+
metadata_ref: None,
|
|
857
|
+
created_at: delta.created_at,
|
|
858
|
+
updated_at: delta.updated_at,
|
|
859
|
+
};
|
|
860
|
+
by_file_mutations.push(TrackedStateMutation::put_encoded(
|
|
861
|
+
ByFileIndex::encode_key_ref(key),
|
|
862
|
+
ByFileIndex::encode_header_value_ref(header_value),
|
|
863
|
+
));
|
|
864
|
+
}
|
|
865
|
+
let by_file_result = self
|
|
866
|
+
.tree
|
|
867
|
+
.apply_mutations(
|
|
868
|
+
self.store,
|
|
869
|
+
self.writes,
|
|
870
|
+
by_file_base_root.as_ref(),
|
|
871
|
+
by_file_mutations,
|
|
872
|
+
None,
|
|
873
|
+
)
|
|
874
|
+
.await?;
|
|
875
|
+
storage::stage_by_file_root(self.writes, commit_id, &by_file_result.root_id);
|
|
876
|
+
by_file_result.chunk_count
|
|
877
|
+
};
|
|
878
|
+
Ok(TrackedStateWriteReport {
|
|
396
879
|
commit_id: commit_id.to_string(),
|
|
397
|
-
|
|
880
|
+
changed_rows: deltas.len(),
|
|
881
|
+
primary_chunk_puts: result.chunk_count,
|
|
882
|
+
by_file_chunk_puts,
|
|
398
883
|
})
|
|
399
884
|
}
|
|
400
|
-
|
|
401
|
-
/// Deletes the root pointer for one commit.
|
|
402
|
-
///
|
|
403
|
-
/// This is intentionally root-scoped, not row-scoped. It is useful for
|
|
404
|
-
/// rebuild/corruption tests where the changelog remains authoritative and
|
|
405
|
-
/// the tracked-state projection must be recreated from the commit id.
|
|
406
|
-
#[cfg(test)]
|
|
407
|
-
pub(crate) fn stage_delete_root_for_rebuild(
|
|
408
|
-
&mut self,
|
|
409
|
-
writes: &mut StorageWriteSet,
|
|
410
|
-
commit_id: &str,
|
|
411
|
-
) {
|
|
412
|
-
storage::stage_delete_root(writes, commit_id)
|
|
413
|
-
}
|
|
414
885
|
}
|
|
415
886
|
|
|
416
887
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
417
|
-
pub(crate) struct
|
|
888
|
+
pub(crate) struct TrackedStateWriteReport {
|
|
418
889
|
pub(crate) commit_id: String,
|
|
419
|
-
pub(crate)
|
|
890
|
+
pub(crate) changed_rows: usize,
|
|
891
|
+
pub(crate) primary_chunk_puts: usize,
|
|
892
|
+
pub(crate) by_file_chunk_puts: usize,
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
fn missing_commit_error(commit_id: &str) -> LixError {
|
|
896
|
+
LixError::new(
|
|
897
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
898
|
+
format!("tracked_state projection references missing commit '{commit_id}'"),
|
|
899
|
+
)
|
|
420
900
|
}
|
|
421
901
|
|
|
422
902
|
fn tree_scan_request_from_tracked(
|
|
@@ -427,7 +907,10 @@ fn tree_scan_request_from_tracked(
|
|
|
427
907
|
entity_ids: request.filter.entity_ids.clone(),
|
|
428
908
|
file_ids: request.filter.file_ids.clone(),
|
|
429
909
|
include_tombstones: request.filter.include_tombstones,
|
|
430
|
-
|
|
910
|
+
// User limits belong above delta overlay and tombstone visibility.
|
|
911
|
+
// Pushing them into the physical tree can stop on rows that are later
|
|
912
|
+
// hidden, returning too few live rows.
|
|
913
|
+
limit: None,
|
|
431
914
|
}
|
|
432
915
|
}
|
|
433
916
|
|
|
@@ -466,11 +949,11 @@ mod tests {
|
|
|
466
949
|
|
|
467
950
|
use super::*;
|
|
468
951
|
use crate::backend::{testing::UnitTestBackend, Backend};
|
|
469
|
-
use crate::storage::{StorageContext,
|
|
952
|
+
use crate::storage::{StorageContext, StorageWriteTransaction};
|
|
470
953
|
use crate::NullableKeyFilter;
|
|
471
954
|
|
|
472
955
|
#[tokio::test]
|
|
473
|
-
async fn
|
|
956
|
+
async fn stage_delta_does_not_require_parent_projection_root() {
|
|
474
957
|
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
475
958
|
let storage = StorageContext::new(Arc::clone(&backend));
|
|
476
959
|
let tracked_state = TrackedStateContext::new();
|
|
@@ -479,7 +962,7 @@ mod tests {
|
|
|
479
962
|
.await
|
|
480
963
|
.expect("transaction should open");
|
|
481
964
|
|
|
482
|
-
|
|
965
|
+
write_root_for_test(
|
|
483
966
|
transaction.as_mut(),
|
|
484
967
|
&tracked_state,
|
|
485
968
|
"commit-child",
|
|
@@ -487,12 +970,7 @@ mod tests {
|
|
|
487
970
|
&[row("entity-child", "change-child", "commit-child")],
|
|
488
971
|
)
|
|
489
972
|
.await
|
|
490
|
-
.
|
|
491
|
-
|
|
492
|
-
assert!(
|
|
493
|
-
error.message.contains("parent root") && error.message.contains("missing-parent"),
|
|
494
|
-
"unexpected error: {error:?}"
|
|
495
|
-
);
|
|
973
|
+
.expect("delta pack staging should not require a parent projection root");
|
|
496
974
|
}
|
|
497
975
|
|
|
498
976
|
#[tokio::test]
|
|
@@ -600,22 +1078,135 @@ mod tests {
|
|
|
600
1078
|
&TrackedStateDiffRequest::default(),
|
|
601
1079
|
)
|
|
602
1080
|
.await
|
|
603
|
-
.expect("merge should plan");
|
|
1081
|
+
.expect("merge should plan");
|
|
1082
|
+
|
|
1083
|
+
assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
|
|
1084
|
+
assert_eq!(plan.patches[0].projected_row().snapshot_content, None);
|
|
1085
|
+
assert_eq!(plan.patches[0].change_id(), "change-source-delete");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
#[tokio::test]
|
|
1089
|
+
async fn scan_rows_by_file_uses_file_index_shape() {
|
|
1090
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1091
|
+
let storage = StorageContext::new(backend.clone());
|
|
1092
|
+
let tracked_state = TrackedStateContext::new();
|
|
1093
|
+
let mut file_a = row("entity-a", "change-a", "commit-1");
|
|
1094
|
+
file_a.file_id = Some("file-a.json".to_string());
|
|
1095
|
+
let mut file_b = row("entity-b", "change-b", "commit-1");
|
|
1096
|
+
file_b.file_id = Some("file-b.json".to_string());
|
|
1097
|
+
|
|
1098
|
+
let mut transaction = storage
|
|
1099
|
+
.begin_write_transaction()
|
|
1100
|
+
.await
|
|
1101
|
+
.expect("transaction should open");
|
|
1102
|
+
write_root_for_test(
|
|
1103
|
+
transaction.as_mut(),
|
|
1104
|
+
&tracked_state,
|
|
1105
|
+
"commit-1",
|
|
1106
|
+
None,
|
|
1107
|
+
&[file_a, file_b],
|
|
1108
|
+
)
|
|
1109
|
+
.await
|
|
1110
|
+
.expect("root should write");
|
|
1111
|
+
transaction
|
|
1112
|
+
.commit()
|
|
1113
|
+
.await
|
|
1114
|
+
.expect("transaction should commit");
|
|
1115
|
+
|
|
1116
|
+
let rows = tracked_state
|
|
1117
|
+
.reader(storage.clone())
|
|
1118
|
+
.scan_rows_at_commit(
|
|
1119
|
+
"commit-1",
|
|
1120
|
+
&TrackedStateScanRequest {
|
|
1121
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1122
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1123
|
+
..Default::default()
|
|
1124
|
+
},
|
|
1125
|
+
..Default::default()
|
|
1126
|
+
},
|
|
1127
|
+
)
|
|
1128
|
+
.await
|
|
1129
|
+
.expect("file scan should read through index");
|
|
1130
|
+
|
|
1131
|
+
assert_eq!(rows.len(), 1);
|
|
1132
|
+
assert_eq!(
|
|
1133
|
+
rows[0]
|
|
1134
|
+
.entity_id
|
|
1135
|
+
.as_single_string_owned()
|
|
1136
|
+
.expect("entity id"),
|
|
1137
|
+
"entity-a"
|
|
1138
|
+
);
|
|
1139
|
+
assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
#[tokio::test]
|
|
1143
|
+
async fn by_file_header_index_fetches_primary_payload_only_when_requested() {
|
|
1144
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1145
|
+
let storage = StorageContext::new(backend.clone());
|
|
1146
|
+
let tracked_state = TrackedStateContext::new();
|
|
1147
|
+
let mut row = row("entity-a", "change-a", "commit-1");
|
|
1148
|
+
row.file_id = Some("file-a.json".to_string());
|
|
1149
|
+
let expected_snapshot = row.snapshot_content.clone();
|
|
1150
|
+
|
|
1151
|
+
let mut transaction = storage
|
|
1152
|
+
.begin_write_transaction()
|
|
1153
|
+
.await
|
|
1154
|
+
.expect("transaction should open");
|
|
1155
|
+
write_root_for_test(
|
|
1156
|
+
transaction.as_mut(),
|
|
1157
|
+
&tracked_state,
|
|
1158
|
+
"commit-1",
|
|
1159
|
+
None,
|
|
1160
|
+
std::slice::from_ref(&row),
|
|
1161
|
+
)
|
|
1162
|
+
.await
|
|
1163
|
+
.expect("root should write");
|
|
1164
|
+
transaction
|
|
1165
|
+
.commit()
|
|
1166
|
+
.await
|
|
1167
|
+
.expect("transaction should commit");
|
|
1168
|
+
|
|
1169
|
+
let mut reader = tracked_state.reader(storage.clone());
|
|
1170
|
+
let header_rows = reader
|
|
1171
|
+
.scan_rows_at_commit(
|
|
1172
|
+
"commit-1",
|
|
1173
|
+
&TrackedStateScanRequest {
|
|
1174
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1175
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1176
|
+
..Default::default()
|
|
1177
|
+
},
|
|
1178
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
1179
|
+
columns: vec!["entity_id".to_string()],
|
|
1180
|
+
},
|
|
1181
|
+
..Default::default()
|
|
1182
|
+
},
|
|
1183
|
+
)
|
|
1184
|
+
.await
|
|
1185
|
+
.expect("header scan should read through by-file index");
|
|
1186
|
+
let full_rows = reader
|
|
1187
|
+
.scan_rows_at_commit(
|
|
1188
|
+
"commit-1",
|
|
1189
|
+
&TrackedStateScanRequest {
|
|
1190
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1191
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1192
|
+
..Default::default()
|
|
1193
|
+
},
|
|
1194
|
+
..Default::default()
|
|
1195
|
+
},
|
|
1196
|
+
)
|
|
1197
|
+
.await
|
|
1198
|
+
.expect("full scan should fetch primary payload");
|
|
604
1199
|
|
|
605
|
-
assert_eq!(
|
|
606
|
-
assert_eq!(
|
|
607
|
-
assert_eq!(plan.patches[0].change_id(), "change-source-delete");
|
|
1200
|
+
assert_eq!(header_rows[0].snapshot_content, None);
|
|
1201
|
+
assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
|
|
608
1202
|
}
|
|
609
1203
|
|
|
610
1204
|
#[tokio::test]
|
|
611
|
-
async fn
|
|
1205
|
+
async fn null_file_rows_do_not_stage_by_file_index() {
|
|
612
1206
|
let backend = Arc::new(UnitTestBackend::new());
|
|
613
1207
|
let storage = StorageContext::new(backend.clone());
|
|
614
1208
|
let tracked_state = TrackedStateContext::new();
|
|
615
|
-
let
|
|
616
|
-
file_a.file_id = Some("file-a.json".to_string());
|
|
617
|
-
let mut file_b = row("entity-b", "change-b", "commit-1");
|
|
618
|
-
file_b.file_id = Some("file-b.json".to_string());
|
|
1209
|
+
let row = row("entity-a", "change-a", "commit-1");
|
|
619
1210
|
|
|
620
1211
|
let mut transaction = storage
|
|
621
1212
|
.begin_write_transaction()
|
|
@@ -626,7 +1217,7 @@ mod tests {
|
|
|
626
1217
|
&tracked_state,
|
|
627
1218
|
"commit-1",
|
|
628
1219
|
None,
|
|
629
|
-
&
|
|
1220
|
+
std::slice::from_ref(&row),
|
|
630
1221
|
)
|
|
631
1222
|
.await
|
|
632
1223
|
.expect("root should write");
|
|
@@ -635,37 +1226,44 @@ mod tests {
|
|
|
635
1226
|
.await
|
|
636
1227
|
.expect("transaction should commit");
|
|
637
1228
|
|
|
1229
|
+
let by_file_root = storage::load_by_file_root(&mut storage.clone(), "commit-1")
|
|
1230
|
+
.await
|
|
1231
|
+
.expect("by-file root lookup should load");
|
|
1232
|
+
assert!(by_file_root.is_none());
|
|
1233
|
+
|
|
638
1234
|
let rows = tracked_state
|
|
639
1235
|
.reader(storage.clone())
|
|
640
1236
|
.scan_rows_at_commit(
|
|
641
1237
|
"commit-1",
|
|
642
1238
|
&TrackedStateScanRequest {
|
|
643
1239
|
filter: crate::tracked_state::TrackedStateFilter {
|
|
644
|
-
file_ids: vec![NullableKeyFilter::
|
|
1240
|
+
file_ids: vec![NullableKeyFilter::Null],
|
|
645
1241
|
..Default::default()
|
|
646
1242
|
},
|
|
647
1243
|
..Default::default()
|
|
648
1244
|
},
|
|
649
1245
|
)
|
|
650
1246
|
.await
|
|
651
|
-
.expect("file scan should
|
|
1247
|
+
.expect("null file scan should fall back to primary tree");
|
|
652
1248
|
|
|
653
1249
|
assert_eq!(rows.len(), 1);
|
|
654
1250
|
assert_eq!(
|
|
655
|
-
rows[0]
|
|
1251
|
+
rows[0]
|
|
1252
|
+
.entity_id
|
|
1253
|
+
.as_single_string_owned()
|
|
1254
|
+
.expect("entity id"),
|
|
656
1255
|
"entity-a"
|
|
657
1256
|
);
|
|
658
|
-
assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
|
|
659
1257
|
}
|
|
660
1258
|
|
|
661
1259
|
#[tokio::test]
|
|
662
|
-
async fn
|
|
1260
|
+
async fn mixed_null_and_concrete_file_scan_uses_primary_tree() {
|
|
663
1261
|
let backend = Arc::new(UnitTestBackend::new());
|
|
664
1262
|
let storage = StorageContext::new(backend.clone());
|
|
665
1263
|
let tracked_state = TrackedStateContext::new();
|
|
666
|
-
let
|
|
667
|
-
|
|
668
|
-
|
|
1264
|
+
let null_row = row("entity-null", "change-null", "commit-1");
|
|
1265
|
+
let mut file_row = row("entity-file", "change-file", "commit-2");
|
|
1266
|
+
file_row.file_id = Some("file-a.json".to_string());
|
|
669
1267
|
|
|
670
1268
|
let mut transaction = storage
|
|
671
1269
|
.begin_write_transaction()
|
|
@@ -676,7 +1274,70 @@ mod tests {
|
|
|
676
1274
|
&tracked_state,
|
|
677
1275
|
"commit-1",
|
|
678
1276
|
None,
|
|
679
|
-
std::slice::from_ref(&
|
|
1277
|
+
std::slice::from_ref(&null_row),
|
|
1278
|
+
)
|
|
1279
|
+
.await
|
|
1280
|
+
.expect("parent root should write");
|
|
1281
|
+
write_root_for_test(
|
|
1282
|
+
transaction.as_mut(),
|
|
1283
|
+
&tracked_state,
|
|
1284
|
+
"commit-2",
|
|
1285
|
+
Some("commit-1"),
|
|
1286
|
+
std::slice::from_ref(&file_row),
|
|
1287
|
+
)
|
|
1288
|
+
.await
|
|
1289
|
+
.expect("child root should write");
|
|
1290
|
+
transaction
|
|
1291
|
+
.commit()
|
|
1292
|
+
.await
|
|
1293
|
+
.expect("transaction should commit");
|
|
1294
|
+
|
|
1295
|
+
let rows = tracked_state
|
|
1296
|
+
.reader(storage.clone())
|
|
1297
|
+
.scan_rows_at_commit(
|
|
1298
|
+
"commit-2",
|
|
1299
|
+
&TrackedStateScanRequest {
|
|
1300
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1301
|
+
file_ids: vec![
|
|
1302
|
+
NullableKeyFilter::Null,
|
|
1303
|
+
NullableKeyFilter::Value("file-a.json".to_string()),
|
|
1304
|
+
],
|
|
1305
|
+
..Default::default()
|
|
1306
|
+
},
|
|
1307
|
+
..Default::default()
|
|
1308
|
+
},
|
|
1309
|
+
)
|
|
1310
|
+
.await
|
|
1311
|
+
.expect("mixed scan should use primary tree");
|
|
1312
|
+
|
|
1313
|
+
let mut entity_ids = rows
|
|
1314
|
+
.iter()
|
|
1315
|
+
.map(|row| row.entity_id.as_single_string_owned().expect("entity id"))
|
|
1316
|
+
.collect::<Vec<_>>();
|
|
1317
|
+
entity_ids.sort();
|
|
1318
|
+
assert_eq!(entity_ids, vec!["entity-file", "entity-null"]);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
#[tokio::test]
|
|
1322
|
+
async fn by_file_header_index_filters_tombstones_without_payload_sentinel() {
|
|
1323
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1324
|
+
let storage = StorageContext::new(backend.clone());
|
|
1325
|
+
let tracked_state = TrackedStateContext::new();
|
|
1326
|
+
let mut live = row("entity-live", "change-live", "commit-1");
|
|
1327
|
+
live.file_id = Some("file-a.json".to_string());
|
|
1328
|
+
let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
|
|
1329
|
+
deleted.file_id = Some("file-a.json".to_string());
|
|
1330
|
+
|
|
1331
|
+
let mut transaction = storage
|
|
1332
|
+
.begin_write_transaction()
|
|
1333
|
+
.await
|
|
1334
|
+
.expect("transaction should open");
|
|
1335
|
+
write_root_for_test(
|
|
1336
|
+
transaction.as_mut(),
|
|
1337
|
+
&tracked_state,
|
|
1338
|
+
"commit-1",
|
|
1339
|
+
None,
|
|
1340
|
+
&[live, deleted],
|
|
680
1341
|
)
|
|
681
1342
|
.await
|
|
682
1343
|
.expect("root should write");
|
|
@@ -685,8 +1346,8 @@ mod tests {
|
|
|
685
1346
|
.await
|
|
686
1347
|
.expect("transaction should commit");
|
|
687
1348
|
|
|
688
|
-
let
|
|
689
|
-
|
|
1349
|
+
let rows = tracked_state
|
|
1350
|
+
.reader(storage.clone())
|
|
690
1351
|
.scan_rows_at_commit(
|
|
691
1352
|
"commit-1",
|
|
692
1353
|
&TrackedStateScanRequest {
|
|
@@ -701,34 +1362,202 @@ mod tests {
|
|
|
701
1362
|
},
|
|
702
1363
|
)
|
|
703
1364
|
.await
|
|
704
|
-
.expect("
|
|
705
|
-
|
|
1365
|
+
.expect("file scan should read through index");
|
|
1366
|
+
|
|
1367
|
+
assert_eq!(rows.len(), 1);
|
|
1368
|
+
assert_eq!(
|
|
1369
|
+
rows[0]
|
|
1370
|
+
.entity_id
|
|
1371
|
+
.as_single_string_owned()
|
|
1372
|
+
.expect("entity id"),
|
|
1373
|
+
"entity-live"
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
#[tokio::test]
|
|
1378
|
+
async fn pending_tombstone_delta_hides_materialized_base_row() {
|
|
1379
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1380
|
+
let storage = StorageContext::new(backend.clone());
|
|
1381
|
+
let tracked_state = TrackedStateContext::new();
|
|
1382
|
+
let base = row("entity-a", "change-base", "base");
|
|
1383
|
+
let delete = tombstone("entity-a", "change-delete", "child");
|
|
1384
|
+
|
|
1385
|
+
let mut transaction = storage
|
|
1386
|
+
.begin_write_transaction()
|
|
1387
|
+
.await
|
|
1388
|
+
.expect("base transaction should open");
|
|
1389
|
+
write_root_for_test(
|
|
1390
|
+
transaction.as_mut(),
|
|
1391
|
+
&tracked_state,
|
|
1392
|
+
"base",
|
|
1393
|
+
None,
|
|
1394
|
+
std::slice::from_ref(&base),
|
|
1395
|
+
)
|
|
1396
|
+
.await
|
|
1397
|
+
.expect("base delta should write");
|
|
1398
|
+
transaction.commit().await.expect("base should commit");
|
|
1399
|
+
|
|
1400
|
+
let mut transaction = storage
|
|
1401
|
+
.begin_write_transaction()
|
|
1402
|
+
.await
|
|
1403
|
+
.expect("materialize transaction should open");
|
|
1404
|
+
let mut writes = StorageWriteSet::new();
|
|
1405
|
+
tracked_state
|
|
1406
|
+
.materializer(
|
|
1407
|
+
transaction.as_mut(),
|
|
1408
|
+
&mut writes,
|
|
1409
|
+
&CommitStoreContext::new(),
|
|
1410
|
+
)
|
|
1411
|
+
.materialize_root_at("base")
|
|
1412
|
+
.await
|
|
1413
|
+
.expect("base projection root should materialize");
|
|
1414
|
+
writes
|
|
1415
|
+
.apply(transaction.as_mut())
|
|
1416
|
+
.await
|
|
1417
|
+
.expect("base root writes should apply");
|
|
1418
|
+
transaction
|
|
1419
|
+
.commit()
|
|
1420
|
+
.await
|
|
1421
|
+
.expect("materialized base should commit");
|
|
1422
|
+
|
|
1423
|
+
let mut transaction = storage
|
|
1424
|
+
.begin_write_transaction()
|
|
1425
|
+
.await
|
|
1426
|
+
.expect("child transaction should open");
|
|
1427
|
+
write_root_for_test(
|
|
1428
|
+
transaction.as_mut(),
|
|
1429
|
+
&tracked_state,
|
|
1430
|
+
"child",
|
|
1431
|
+
Some("base"),
|
|
1432
|
+
std::slice::from_ref(&delete),
|
|
1433
|
+
)
|
|
1434
|
+
.await
|
|
1435
|
+
.expect("child tombstone delta should write");
|
|
1436
|
+
transaction.commit().await.expect("child should commit");
|
|
1437
|
+
|
|
1438
|
+
let rows = tracked_state
|
|
1439
|
+
.reader(storage.clone())
|
|
1440
|
+
.scan_rows_at_commit("child", &TrackedStateScanRequest::default())
|
|
1441
|
+
.await
|
|
1442
|
+
.expect("child scan should apply pending tombstone over base root");
|
|
1443
|
+
|
|
1444
|
+
assert!(rows.is_empty(), "pending tombstone must hide base row");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
#[tokio::test]
|
|
1448
|
+
async fn single_delta_pack_scan_keeps_last_delta_for_duplicate_key() {
|
|
1449
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1450
|
+
let storage = StorageContext::new(backend.clone());
|
|
1451
|
+
let tracked_state = TrackedStateContext::new();
|
|
1452
|
+
|
|
1453
|
+
let mut transaction = storage
|
|
1454
|
+
.begin_write_transaction()
|
|
1455
|
+
.await
|
|
1456
|
+
.expect("transaction should open");
|
|
1457
|
+
write_root_for_test(
|
|
1458
|
+
transaction.as_mut(),
|
|
1459
|
+
&tracked_state,
|
|
1460
|
+
"commit-1",
|
|
1461
|
+
None,
|
|
1462
|
+
&[
|
|
1463
|
+
row_with_value("entity-a", "change-a1", "commit-1", "first"),
|
|
1464
|
+
row_with_value("entity-b", "change-b", "commit-1", "middle"),
|
|
1465
|
+
row_with_value("entity-a", "change-a2", "commit-1", "second"),
|
|
1466
|
+
tombstone("entity-c", "change-c1", "commit-1"),
|
|
1467
|
+
],
|
|
1468
|
+
)
|
|
1469
|
+
.await
|
|
1470
|
+
.expect("delta pack should write");
|
|
1471
|
+
transaction
|
|
1472
|
+
.commit()
|
|
1473
|
+
.await
|
|
1474
|
+
.expect("transaction should commit");
|
|
1475
|
+
|
|
1476
|
+
let rows = tracked_state
|
|
1477
|
+
.reader(storage.clone())
|
|
1478
|
+
.scan_rows_at_commit("commit-1", &TrackedStateScanRequest::default())
|
|
1479
|
+
.await
|
|
1480
|
+
.expect("single delta pack should scan");
|
|
1481
|
+
|
|
1482
|
+
assert_eq!(rows.len(), 2);
|
|
1483
|
+
assert_eq!(
|
|
1484
|
+
rows.iter()
|
|
1485
|
+
.map(|row| (
|
|
1486
|
+
row.entity_id.as_single_string_owned().expect("entity id"),
|
|
1487
|
+
row.snapshot_content.clone()
|
|
1488
|
+
))
|
|
1489
|
+
.collect::<Vec<_>>(),
|
|
1490
|
+
vec![
|
|
1491
|
+
(
|
|
1492
|
+
"entity-a".to_string(),
|
|
1493
|
+
Some("{\"value\":\"second\"}".to_string())
|
|
1494
|
+
),
|
|
1495
|
+
(
|
|
1496
|
+
"entity-b".to_string(),
|
|
1497
|
+
Some("{\"value\":\"middle\"}".to_string())
|
|
1498
|
+
),
|
|
1499
|
+
]
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
#[tokio::test]
|
|
1504
|
+
async fn scan_limit_applies_after_tombstone_visibility() {
|
|
1505
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1506
|
+
let storage = StorageContext::new(backend.clone());
|
|
1507
|
+
let tracked_state = TrackedStateContext::new();
|
|
1508
|
+
|
|
1509
|
+
let mut transaction = storage
|
|
1510
|
+
.begin_write_transaction()
|
|
1511
|
+
.await
|
|
1512
|
+
.expect("transaction should open");
|
|
1513
|
+
write_root_for_test(
|
|
1514
|
+
transaction.as_mut(),
|
|
1515
|
+
&tracked_state,
|
|
1516
|
+
"commit-1",
|
|
1517
|
+
None,
|
|
1518
|
+
&[
|
|
1519
|
+
tombstone("entity-a", "change-delete", "commit-1"),
|
|
1520
|
+
row("entity-b", "change-live", "commit-1"),
|
|
1521
|
+
],
|
|
1522
|
+
)
|
|
1523
|
+
.await
|
|
1524
|
+
.expect("root should write");
|
|
1525
|
+
transaction
|
|
1526
|
+
.commit()
|
|
1527
|
+
.await
|
|
1528
|
+
.expect("transaction should commit");
|
|
1529
|
+
|
|
1530
|
+
let rows = tracked_state
|
|
1531
|
+
.reader(storage.clone())
|
|
706
1532
|
.scan_rows_at_commit(
|
|
707
1533
|
"commit-1",
|
|
708
1534
|
&TrackedStateScanRequest {
|
|
709
|
-
|
|
710
|
-
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
711
|
-
..Default::default()
|
|
712
|
-
},
|
|
1535
|
+
limit: Some(1),
|
|
713
1536
|
..Default::default()
|
|
714
1537
|
},
|
|
715
1538
|
)
|
|
716
1539
|
.await
|
|
717
|
-
.expect("
|
|
1540
|
+
.expect("limited scan should apply visibility before limit");
|
|
718
1541
|
|
|
719
|
-
assert_eq!(
|
|
720
|
-
assert_eq!(
|
|
1542
|
+
assert_eq!(rows.len(), 1);
|
|
1543
|
+
assert_eq!(
|
|
1544
|
+
rows[0]
|
|
1545
|
+
.entity_id
|
|
1546
|
+
.as_single_string_owned()
|
|
1547
|
+
.expect("entity id"),
|
|
1548
|
+
"entity-b"
|
|
1549
|
+
);
|
|
721
1550
|
}
|
|
722
1551
|
|
|
723
1552
|
#[tokio::test]
|
|
724
|
-
async fn
|
|
1553
|
+
async fn by_file_scan_limit_applies_after_tombstone_visibility() {
|
|
725
1554
|
let backend = Arc::new(UnitTestBackend::new());
|
|
726
1555
|
let storage = StorageContext::new(backend.clone());
|
|
727
1556
|
let tracked_state = TrackedStateContext::new();
|
|
728
|
-
let mut
|
|
729
|
-
live.file_id = Some("file-a.json".to_string());
|
|
730
|
-
let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
|
|
1557
|
+
let mut deleted = tombstone("entity-a", "change-delete", "commit-1");
|
|
731
1558
|
deleted.file_id = Some("file-a.json".to_string());
|
|
1559
|
+
let mut live = row("entity-b", "change-live", "commit-1");
|
|
1560
|
+
live.file_id = Some("file-a.json".to_string());
|
|
732
1561
|
|
|
733
1562
|
let mut transaction = storage
|
|
734
1563
|
.begin_write_transaction()
|
|
@@ -739,7 +1568,7 @@ mod tests {
|
|
|
739
1568
|
&tracked_state,
|
|
740
1569
|
"commit-1",
|
|
741
1570
|
None,
|
|
742
|
-
&[
|
|
1571
|
+
&[deleted, live],
|
|
743
1572
|
)
|
|
744
1573
|
.await
|
|
745
1574
|
.expect("root should write");
|
|
@@ -760,16 +1589,19 @@ mod tests {
|
|
|
760
1589
|
projection: crate::tracked_state::TrackedStateProjection {
|
|
761
1590
|
columns: vec!["entity_id".to_string()],
|
|
762
1591
|
},
|
|
763
|
-
|
|
1592
|
+
limit: Some(1),
|
|
764
1593
|
},
|
|
765
1594
|
)
|
|
766
1595
|
.await
|
|
767
|
-
.expect("file scan should
|
|
1596
|
+
.expect("limited by-file scan should apply visibility before limit");
|
|
768
1597
|
|
|
769
1598
|
assert_eq!(rows.len(), 1);
|
|
770
1599
|
assert_eq!(
|
|
771
|
-
rows[0]
|
|
772
|
-
|
|
1600
|
+
rows[0]
|
|
1601
|
+
.entity_id
|
|
1602
|
+
.as_single_string_owned()
|
|
1603
|
+
.expect("entity id"),
|
|
1604
|
+
"entity-b"
|
|
773
1605
|
);
|
|
774
1606
|
}
|
|
775
1607
|
|
|
@@ -801,16 +1633,18 @@ mod tests {
|
|
|
801
1633
|
|
|
802
1634
|
let mut reader = tracked_state.reader(storage.clone());
|
|
803
1635
|
let loaded = reader
|
|
804
|
-
.
|
|
1636
|
+
.load_rows_at_commit(
|
|
805
1637
|
"commit-1",
|
|
806
|
-
&TrackedStateRowRequest {
|
|
1638
|
+
&[TrackedStateRowRequest {
|
|
807
1639
|
schema_key: row.schema_key.clone(),
|
|
808
1640
|
entity_id: row.entity_id.clone(),
|
|
809
1641
|
file_id: NullableKeyFilter::Null,
|
|
810
|
-
},
|
|
1642
|
+
}],
|
|
811
1643
|
)
|
|
812
1644
|
.await
|
|
813
1645
|
.expect("row should load")
|
|
1646
|
+
.pop()
|
|
1647
|
+
.flatten()
|
|
814
1648
|
.expect("row should exist");
|
|
815
1649
|
let scanned = reader
|
|
816
1650
|
.scan_rows_at_commit("commit-1", &TrackedStateScanRequest::default())
|
|
@@ -821,6 +1655,53 @@ mod tests {
|
|
|
821
1655
|
assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
|
|
822
1656
|
}
|
|
823
1657
|
|
|
1658
|
+
#[tokio::test]
|
|
1659
|
+
async fn projection_cache_uses_seen_updated_at_not_change_created_at() {
|
|
1660
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1661
|
+
let storage = StorageContext::new(backend.clone());
|
|
1662
|
+
let tracked_state = TrackedStateContext::new();
|
|
1663
|
+
let mut row = row("entity-a", "change-a", "commit-1");
|
|
1664
|
+
row.created_at = "2026-01-01T00:00:00Z".to_string();
|
|
1665
|
+
row.updated_at = "2026-01-02T00:00:00Z".to_string();
|
|
1666
|
+
|
|
1667
|
+
let mut transaction = storage
|
|
1668
|
+
.begin_write_transaction()
|
|
1669
|
+
.await
|
|
1670
|
+
.expect("transaction should open");
|
|
1671
|
+
write_root_for_test(
|
|
1672
|
+
transaction.as_mut(),
|
|
1673
|
+
&tracked_state,
|
|
1674
|
+
"commit-1",
|
|
1675
|
+
None,
|
|
1676
|
+
std::slice::from_ref(&row),
|
|
1677
|
+
)
|
|
1678
|
+
.await
|
|
1679
|
+
.expect("root should write");
|
|
1680
|
+
transaction
|
|
1681
|
+
.commit()
|
|
1682
|
+
.await
|
|
1683
|
+
.expect("transaction should commit");
|
|
1684
|
+
|
|
1685
|
+
let loaded = tracked_state
|
|
1686
|
+
.reader(storage.clone())
|
|
1687
|
+
.load_rows_at_commit(
|
|
1688
|
+
"commit-1",
|
|
1689
|
+
&[TrackedStateRowRequest {
|
|
1690
|
+
schema_key: row.schema_key.clone(),
|
|
1691
|
+
entity_id: row.entity_id.clone(),
|
|
1692
|
+
file_id: NullableKeyFilter::Null,
|
|
1693
|
+
}],
|
|
1694
|
+
)
|
|
1695
|
+
.await
|
|
1696
|
+
.expect("row should load")
|
|
1697
|
+
.pop()
|
|
1698
|
+
.flatten()
|
|
1699
|
+
.expect("row should exist");
|
|
1700
|
+
|
|
1701
|
+
assert_eq!(loaded.created_at, "2026-01-01T00:00:00Z");
|
|
1702
|
+
assert_eq!(loaded.updated_at, "2026-01-02T00:00:00Z");
|
|
1703
|
+
}
|
|
1704
|
+
|
|
824
1705
|
#[tokio::test]
|
|
825
1706
|
async fn projected_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
|
|
826
1707
|
let backend = Arc::new(UnitTestBackend::new());
|
|
@@ -866,9 +1747,9 @@ mod tests {
|
|
|
866
1747
|
}
|
|
867
1748
|
|
|
868
1749
|
async fn seed_merge_roots(
|
|
869
|
-
base_rows: &[
|
|
870
|
-
target_rows: &[
|
|
871
|
-
source_rows: &[
|
|
1750
|
+
base_rows: &[MaterializedTrackedStateRow],
|
|
1751
|
+
target_rows: &[MaterializedTrackedStateRow],
|
|
1752
|
+
source_rows: &[MaterializedTrackedStateRow],
|
|
872
1753
|
) -> (StorageContext, TrackedStateContext) {
|
|
873
1754
|
let backend = Arc::new(UnitTestBackend::new());
|
|
874
1755
|
let storage = StorageContext::new(backend.clone());
|
|
@@ -914,14 +1795,26 @@ mod tests {
|
|
|
914
1795
|
fn merge_patch_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
915
1796
|
plan.patches
|
|
916
1797
|
.iter()
|
|
917
|
-
.map(|entry|
|
|
1798
|
+
.map(|entry| {
|
|
1799
|
+
entry
|
|
1800
|
+
.identity()
|
|
1801
|
+
.entity_id
|
|
1802
|
+
.as_single_string_owned()
|
|
1803
|
+
.expect("identity")
|
|
1804
|
+
})
|
|
918
1805
|
.collect()
|
|
919
1806
|
}
|
|
920
1807
|
|
|
921
1808
|
fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
922
1809
|
plan.conflicts
|
|
923
1810
|
.iter()
|
|
924
|
-
.map(|entry|
|
|
1811
|
+
.map(|entry| {
|
|
1812
|
+
entry
|
|
1813
|
+
.identity
|
|
1814
|
+
.entity_id
|
|
1815
|
+
.as_single_string_owned()
|
|
1816
|
+
.expect("identity")
|
|
1817
|
+
})
|
|
925
1818
|
.collect()
|
|
926
1819
|
}
|
|
927
1820
|
|
|
@@ -930,34 +1823,25 @@ mod tests {
|
|
|
930
1823
|
tracked_state: &TrackedStateContext,
|
|
931
1824
|
commit_id: &str,
|
|
932
1825
|
parent_commit_id: Option<&str>,
|
|
933
|
-
rows: &[
|
|
934
|
-
) -> Result<
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
&mut json_writer,
|
|
944
|
-
commit_id,
|
|
945
|
-
parent_commit_id,
|
|
946
|
-
rows,
|
|
947
|
-
)
|
|
948
|
-
.await?
|
|
949
|
-
};
|
|
950
|
-
writes.apply(transaction).await?;
|
|
951
|
-
Ok(receipt)
|
|
1826
|
+
rows: &[MaterializedTrackedStateRow],
|
|
1827
|
+
) -> Result<(), LixError> {
|
|
1828
|
+
crate::test_support::stage_tracked_root_from_materialized(
|
|
1829
|
+
transaction,
|
|
1830
|
+
tracked_state,
|
|
1831
|
+
commit_id,
|
|
1832
|
+
parent_commit_id,
|
|
1833
|
+
rows,
|
|
1834
|
+
)
|
|
1835
|
+
.await
|
|
952
1836
|
}
|
|
953
1837
|
|
|
954
|
-
fn tombstone(entity_id: &str, change_id: &str, commit_id: &str) ->
|
|
1838
|
+
fn tombstone(entity_id: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
955
1839
|
let mut row = row(entity_id, change_id, commit_id);
|
|
956
1840
|
row.snapshot_content = None;
|
|
957
1841
|
row
|
|
958
1842
|
}
|
|
959
1843
|
|
|
960
|
-
fn row(entity_id: &str, change_id: &str, commit_id: &str) ->
|
|
1844
|
+
fn row(entity_id: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
961
1845
|
row_with_value(entity_id, change_id, commit_id, "value")
|
|
962
1846
|
}
|
|
963
1847
|
|
|
@@ -966,14 +1850,14 @@ mod tests {
|
|
|
966
1850
|
change_id: &str,
|
|
967
1851
|
commit_id: &str,
|
|
968
1852
|
value: &str,
|
|
969
|
-
) ->
|
|
970
|
-
|
|
1853
|
+
) -> MaterializedTrackedStateRow {
|
|
1854
|
+
MaterializedTrackedStateRow {
|
|
971
1855
|
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
972
1856
|
schema_key: "test_schema".to_string(),
|
|
973
1857
|
file_id: None,
|
|
974
1858
|
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
975
1859
|
metadata: None,
|
|
976
|
-
|
|
1860
|
+
deleted: false,
|
|
977
1861
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
978
1862
|
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
979
1863
|
change_id: change_id.to_string(),
|