@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3
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 +304 -320
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
- package/dist/engine-wasm/wasm/lix_engine.js +9 -13
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +103 -14
- package/dist/open-lix.js +3 -0
- package/dist/sqlite/index.js +99 -22
- package/dist-engine-src/README.md +18 -0
- package/dist-engine-src/src/backend/kv.rs +358 -0
- package/dist-engine-src/src/backend/mod.rs +12 -0
- package/dist-engine-src/src/backend/testing.rs +658 -0
- package/dist-engine-src/src/backend/types.rs +96 -0
- package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
- package/dist-engine-src/src/binary_cas/codec.rs +346 -0
- package/dist-engine-src/src/binary_cas/context.rs +139 -0
- package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
- package/dist-engine-src/src/binary_cas/mod.rs +11 -0
- package/dist-engine-src/src/binary_cas/types.rs +121 -0
- 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/context.rs +86 -0
- package/dist-engine-src/src/cel/error.rs +19 -0
- package/dist-engine-src/src/cel/mod.rs +8 -0
- package/dist-engine-src/src/cel/provider.rs +9 -0
- package/dist-engine-src/src/cel/runtime.rs +167 -0
- package/dist-engine-src/src/cel/value.rs +50 -0
- package/dist-engine-src/src/commit_graph/context.rs +901 -0
- package/dist-engine-src/src/commit_graph/mod.rs +11 -0
- package/dist-engine-src/src/commit_graph/types.rs +109 -0
- package/dist-engine-src/src/commit_graph/walker.rs +756 -0
- 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/error.rs +313 -0
- package/dist-engine-src/src/common/fingerprint.rs +3 -0
- package/dist-engine-src/src/common/fs_path.rs +1336 -0
- package/dist-engine-src/src/common/identity.rs +145 -0
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +40 -0
- package/dist-engine-src/src/common/mod.rs +23 -0
- package/dist-engine-src/src/common/types.rs +105 -0
- package/dist-engine-src/src/common/wire.rs +222 -0
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +225 -0
- package/dist-engine-src/src/entity_identity.rs +405 -0
- package/dist-engine-src/src/functions/context.rs +292 -0
- package/dist-engine-src/src/functions/deterministic.rs +113 -0
- package/dist-engine-src/src/functions/mod.rs +18 -0
- package/dist-engine-src/src/functions/provider.rs +130 -0
- package/dist-engine-src/src/functions/state.rs +336 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +558 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +423 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +12 -0
- package/dist-engine-src/src/json_store/store.rs +1109 -0
- package/dist-engine-src/src/json_store/types.rs +217 -0
- package/dist-engine-src/src/lib.rs +62 -0
- package/dist-engine-src/src/live_state/context.rs +2019 -0
- package/dist-engine-src/src/live_state/mod.rs +15 -0
- package/dist-engine-src/src/live_state/overlay.rs +75 -0
- package/dist-engine-src/src/live_state/reader.rs +23 -0
- package/dist-engine-src/src/live_state/types.rs +222 -0
- package/dist-engine-src/src/live_state/visibility.rs +223 -0
- package/dist-engine-src/src/plugin/archive.rs +438 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +619 -0
- package/dist-engine-src/src/plugin/manifest.rs +516 -0
- package/dist-engine-src/src/plugin/materializer.rs +477 -0
- package/dist-engine-src/src/plugin/mod.rs +33 -0
- package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +187 -0
- package/dist-engine-src/src/schema/definition.rs +742 -0
- package/dist-engine-src/src/schema/key.rs +138 -0
- package/dist-engine-src/src/schema/mod.rs +20 -0
- package/dist-engine-src/src/schema/seed.rs +14 -0
- package/dist-engine-src/src/schema/tests.rs +780 -0
- package/dist-engine-src/src/session/context.rs +364 -0
- package/dist-engine-src/src/session/create_version.rs +88 -0
- package/dist-engine-src/src/session/execute.rs +478 -0
- package/dist-engine-src/src/session/merge/analysis.rs +102 -0
- package/dist-engine-src/src/session/merge/apply.rs +23 -0
- package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
- package/dist-engine-src/src/session/merge/mod.rs +11 -0
- package/dist-engine-src/src/session/merge/stats.rs +65 -0
- package/dist-engine-src/src/session/merge/version.rs +427 -0
- package/dist-engine-src/src/session/mod.rs +27 -0
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +109 -0
- package/dist-engine-src/src/sql2/change_provider.rs +331 -0
- package/dist-engine-src/src/sql2/classify.rs +182 -0
- package/dist-engine-src/src/sql2/context.rs +311 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
- package/dist-engine-src/src/sql2/error.rs +216 -0
- package/dist-engine-src/src/sql2/execute.rs +3440 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
- package/dist-engine-src/src/sql2/history_projection.rs +56 -0
- package/dist-engine-src/src/sql2/history_provider.rs +412 -0
- package/dist-engine-src/src/sql2/history_route.rs +657 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
- package/dist-engine-src/src/sql2/mod.rs +46 -0
- 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 +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +63 -0
- package/dist-engine-src/src/sql2/record_batch.rs +17 -0
- package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
- package/dist-engine-src/src/sql2/runtime.rs +60 -0
- package/dist-engine-src/src/sql2/session.rs +132 -0
- package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
- package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
- package/dist-engine-src/src/sql2/version_scope.rs +394 -0
- package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
- package/dist-engine-src/src/storage/context.rs +356 -0
- package/dist-engine-src/src/storage/mod.rs +14 -0
- package/dist-engine-src/src/storage/read_scope.rs +88 -0
- package/dist-engine-src/src/storage/types.rs +501 -0
- package/dist-engine-src/src/storage_bench.rs +4863 -0
- package/dist-engine-src/src/test_support.rs +228 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
- package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
- package/dist-engine-src/src/tracked_state/context.rs +1867 -0
- package/dist-engine-src/src/tracked_state/diff.rs +686 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +492 -0
- package/dist-engine-src/src/tracked_state/mod.rs +32 -0
- package/dist-engine-src/src/tracked_state/storage.rs +375 -0
- package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
- package/dist-engine-src/src/tracked_state/types.rs +231 -0
- package/dist-engine-src/src/transaction/commit.rs +1484 -0
- package/dist-engine-src/src/transaction/context.rs +1548 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
- package/dist-engine-src/src/transaction/mod.rs +13 -0
- package/dist-engine-src/src/transaction/normalization.rs +890 -0
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
- package/dist-engine-src/src/transaction/staging.rs +1731 -0
- package/dist-engine-src/src/transaction/types.rs +460 -0
- package/dist-engine-src/src/transaction/validation.rs +5830 -0
- package/dist-engine-src/src/untracked_state/codec.rs +307 -0
- package/dist-engine-src/src/untracked_state/context.rs +98 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
- package/dist-engine-src/src/untracked_state/mod.rs +15 -0
- package/dist-engine-src/src/untracked_state/storage.rs +396 -0
- package/dist-engine-src/src/untracked_state/types.rs +146 -0
- package/dist-engine-src/src/version/context.rs +40 -0
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +13 -0
- package/dist-engine-src/src/version/refs.rs +330 -0
- package/dist-engine-src/src/version/stage_rows.rs +67 -0
- package/dist-engine-src/src/version/types.rs +21 -0
- package/dist-engine-src/src/wasm/mod.rs +60 -0
- package/package.json +68 -64
|
@@ -0,0 +1,1867 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
|
|
3
|
+
use crate::commit_store::CommitStoreContext;
|
|
4
|
+
use crate::storage::{StorageReader, StorageWriteSet};
|
|
5
|
+
use crate::tracked_state::by_file_index::ByFileIndex;
|
|
6
|
+
use crate::tracked_state::codec::{encode_key_ref, encode_value_ref};
|
|
7
|
+
use crate::tracked_state::diff::{diff_commits, TrackedStateDiff, TrackedStateDiffRequest};
|
|
8
|
+
use crate::tracked_state::materialize_index_entries;
|
|
9
|
+
use crate::tracked_state::merge::{self, TrackedStateMergePlan};
|
|
10
|
+
use crate::tracked_state::storage;
|
|
11
|
+
use crate::tracked_state::storage::DeltaJsonPackIndexesRef;
|
|
12
|
+
use crate::tracked_state::tree::TrackedStateTree;
|
|
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,
|
|
20
|
+
};
|
|
21
|
+
use crate::LixError;
|
|
22
|
+
|
|
23
|
+
/// Factory for tracked-state readers, delta writers, and projection-root materializers.
|
|
24
|
+
///
|
|
25
|
+
/// Tracked state is stored as content-addressed roots. Version refs
|
|
26
|
+
/// choose which commit/root to read; this context only owns root operations.
|
|
27
|
+
#[derive(Clone)]
|
|
28
|
+
pub(crate) struct TrackedStateContext {
|
|
29
|
+
tree: TrackedStateTree,
|
|
30
|
+
commit_store: CommitStoreContext,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl TrackedStateContext {
|
|
34
|
+
pub(crate) fn new() -> Self {
|
|
35
|
+
Self {
|
|
36
|
+
tree: TrackedStateTree::new(),
|
|
37
|
+
commit_store: CommitStoreContext::new(),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Creates a commit-id-addressed tracked-state reader.
|
|
42
|
+
pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
|
|
43
|
+
where
|
|
44
|
+
S: StorageReader,
|
|
45
|
+
{
|
|
46
|
+
TrackedStateStoreReader {
|
|
47
|
+
store,
|
|
48
|
+
tree: self.tree.clone(),
|
|
49
|
+
commit_store: self.commit_store,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
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
|
+
{
|
|
62
|
+
TrackedStateWriter {
|
|
63
|
+
tree: self.tree.clone(),
|
|
64
|
+
store,
|
|
65
|
+
writes,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
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>
|
|
79
|
+
where
|
|
80
|
+
S: StorageReader + ?Sized,
|
|
81
|
+
{
|
|
82
|
+
TrackedStateMaterializer {
|
|
83
|
+
tracked_state: self,
|
|
84
|
+
store,
|
|
85
|
+
writes,
|
|
86
|
+
commit_store,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Store-backed tracked-state reader created by `TrackedStateContext`.
|
|
92
|
+
pub(crate) struct TrackedStateStoreReader<S> {
|
|
93
|
+
store: S,
|
|
94
|
+
tree: TrackedStateTree,
|
|
95
|
+
commit_store: CommitStoreContext,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl<S> TrackedStateStoreReader<S>
|
|
99
|
+
where
|
|
100
|
+
S: StorageReader,
|
|
101
|
+
{
|
|
102
|
+
pub(crate) async fn scan_rows_at_commit(
|
|
103
|
+
&mut self,
|
|
104
|
+
commit_id: &str,
|
|
105
|
+
request: &TrackedStateScanRequest,
|
|
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
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
self.projection_entries_at_commit(commit_id, &tree_scan_request_from_tracked(request))
|
|
135
|
+
.await?
|
|
136
|
+
};
|
|
137
|
+
let projection = crate::tracked_state::TrackedMaterializationProjection::from_columns(
|
|
138
|
+
&request.projection.columns,
|
|
139
|
+
);
|
|
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);
|
|
146
|
+
}
|
|
147
|
+
Ok(rows)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pub(crate) async fn load_rows_at_commit(
|
|
151
|
+
&mut self,
|
|
152
|
+
commit_id: &str,
|
|
153
|
+
requests: &[TrackedStateRowRequest],
|
|
154
|
+
) -> Result<Vec<Option<MaterializedTrackedStateRow>>, LixError> {
|
|
155
|
+
if requests.is_empty() {
|
|
156
|
+
return Ok(Vec::new());
|
|
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)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pub(crate) async fn diff_commits(
|
|
187
|
+
&mut self,
|
|
188
|
+
left_commit_id: &str,
|
|
189
|
+
right_commit_id: &str,
|
|
190
|
+
request: &TrackedStateDiffRequest,
|
|
191
|
+
) -> Result<TrackedStateDiff, LixError> {
|
|
192
|
+
diff_commits(self, left_commit_id, right_commit_id, request).await
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pub(crate) async fn diff_tree_entries_at_commits(
|
|
196
|
+
&mut self,
|
|
197
|
+
left_commit_id: &str,
|
|
198
|
+
right_commit_id: &str,
|
|
199
|
+
request: &TrackedStateTreeScanRequest,
|
|
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)
|
|
268
|
+
.await?;
|
|
269
|
+
let right_delta_ids = self
|
|
270
|
+
.delta_commit_ids_since_projection_root(right_commit_id)
|
|
271
|
+
.await?;
|
|
272
|
+
let left_base_commit_id = self
|
|
273
|
+
.projection_base_commit_id(left_commit_id, &left_delta_ids)
|
|
274
|
+
.await?;
|
|
275
|
+
let right_base_commit_id = self
|
|
276
|
+
.projection_base_commit_id(right_commit_id, &right_delta_ids)
|
|
277
|
+
.await?;
|
|
278
|
+
if left_base_commit_id != right_base_commit_id {
|
|
279
|
+
return Ok(None);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if right_delta_ids.starts_with(&left_delta_ids) {
|
|
283
|
+
let suffix = &right_delta_ids[left_delta_ids.len()..];
|
|
284
|
+
return self
|
|
285
|
+
.diff_pending_delta_suffix_from_base(left_commit_id, suffix, request, true)
|
|
286
|
+
.await
|
|
287
|
+
.map(Some);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if left_delta_ids.starts_with(&right_delta_ids) {
|
|
291
|
+
let suffix = &left_delta_ids[right_delta_ids.len()..];
|
|
292
|
+
return self
|
|
293
|
+
.diff_pending_delta_suffix_from_base(right_commit_id, suffix, request, false)
|
|
294
|
+
.await
|
|
295
|
+
.map(Some);
|
|
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();
|
|
352
|
+
Ok(entries)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
pub(crate) async fn materialize_tree_values(
|
|
356
|
+
&mut self,
|
|
357
|
+
entries: Vec<(TrackedStateKey, TrackedStateIndexValue)>,
|
|
358
|
+
) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
|
|
359
|
+
materialize_index_entries(
|
|
360
|
+
&mut self.store,
|
|
361
|
+
entries,
|
|
362
|
+
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
363
|
+
)
|
|
364
|
+
.await
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async fn scan_rows_at_commit_by_file_index(
|
|
368
|
+
&mut self,
|
|
369
|
+
primary_root_id: &crate::tracked_state::types::TrackedStateRootId,
|
|
370
|
+
by_file_root_id: &crate::tracked_state::types::TrackedStateRootId,
|
|
371
|
+
request: &TrackedStateScanRequest,
|
|
372
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
373
|
+
let by_file_request = ByFileIndex::scan_request_from_tracked(request);
|
|
374
|
+
let index_match_count = self
|
|
375
|
+
.tree
|
|
376
|
+
.count_matching_keys(&mut self.store, by_file_root_id, &by_file_request)
|
|
377
|
+
.await?;
|
|
378
|
+
let primary_row_count = self
|
|
379
|
+
.tree
|
|
380
|
+
.row_count(&mut self.store, primary_root_id)
|
|
381
|
+
.await?;
|
|
382
|
+
if index_match_count * 20 > primary_row_count {
|
|
383
|
+
let rows = self
|
|
384
|
+
.tree
|
|
385
|
+
.scan(
|
|
386
|
+
&mut self.store,
|
|
387
|
+
primary_root_id,
|
|
388
|
+
&tree_scan_request_from_tracked(request),
|
|
389
|
+
)
|
|
390
|
+
.await?;
|
|
391
|
+
return Ok(rows);
|
|
392
|
+
}
|
|
393
|
+
let index_rows = self
|
|
394
|
+
.tree
|
|
395
|
+
.scan(&mut self.store, by_file_root_id, &by_file_request)
|
|
396
|
+
.await?;
|
|
397
|
+
let mut rows = Vec::new();
|
|
398
|
+
let tree_request = tree_scan_request_from_tracked(request);
|
|
399
|
+
let needs_payloads = scan_needs_json_payloads(request);
|
|
400
|
+
if needs_payloads {
|
|
401
|
+
let mut primary_keys = Vec::with_capacity(index_rows.len());
|
|
402
|
+
for (index_key, _) in index_rows {
|
|
403
|
+
if let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) {
|
|
404
|
+
primary_keys.push(primary_key);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let primary_values = self
|
|
408
|
+
.tree
|
|
409
|
+
.get_many(&mut self.store, primary_root_id, &primary_keys)
|
|
410
|
+
.await?;
|
|
411
|
+
for (primary_key, value) in primary_keys.into_iter().zip(primary_values) {
|
|
412
|
+
let Some(value) = value else {
|
|
413
|
+
continue;
|
|
414
|
+
};
|
|
415
|
+
if !tree_request.matches(&primary_key, &value) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
rows.push((primary_key, value));
|
|
419
|
+
}
|
|
420
|
+
return Ok(rows);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (index_key, index_value) in index_rows {
|
|
424
|
+
let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) else {
|
|
425
|
+
continue;
|
|
426
|
+
};
|
|
427
|
+
let value = index_value;
|
|
428
|
+
if tree_request.matches(&primary_key, &value) {
|
|
429
|
+
rows.push((primary_key, value));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
Ok(rows)
|
|
433
|
+
}
|
|
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
|
+
|
|
683
|
+
/// Plans a three-way merge by diffing both heads against the same base.
|
|
684
|
+
///
|
|
685
|
+
/// `target_commit_id` is the destination root that should keep its own
|
|
686
|
+
/// changes. `source_commit_id` is the incoming root whose non-conflicting
|
|
687
|
+
/// changes should be applied.
|
|
688
|
+
#[allow(dead_code)]
|
|
689
|
+
pub(crate) async fn plan_merge(
|
|
690
|
+
&mut self,
|
|
691
|
+
base_commit_id: &str,
|
|
692
|
+
target_commit_id: &str,
|
|
693
|
+
source_commit_id: &str,
|
|
694
|
+
request: &TrackedStateDiffRequest,
|
|
695
|
+
) -> Result<TrackedStateMergePlan, LixError> {
|
|
696
|
+
let target_diff = self
|
|
697
|
+
.diff_commits(base_commit_id, target_commit_id, request)
|
|
698
|
+
.await?;
|
|
699
|
+
let source_diff = self
|
|
700
|
+
.diff_commits(base_commit_id, source_commit_id, request)
|
|
701
|
+
.await?;
|
|
702
|
+
merge::plan_merge(&target_diff, &source_diff)
|
|
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
|
+
}
|
|
712
|
+
|
|
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(
|
|
726
|
+
&mut self,
|
|
727
|
+
commit_id: &str,
|
|
728
|
+
) -> Result<TrackedStateWriteReport, LixError> {
|
|
729
|
+
crate::tracked_state::materializer::materialize_root_at(self, commit_id).await
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
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
|
+
}
|
|
752
|
+
|
|
753
|
+
pub(crate) async fn stage_delta_with_json_pack_indexes(
|
|
754
|
+
&mut self,
|
|
755
|
+
commit_id: &str,
|
|
756
|
+
_parent_commit_id: Option<&str>,
|
|
757
|
+
deltas: &[TrackedStateDeltaRef<'_>],
|
|
758
|
+
json_pack_indexes: DeltaJsonPackIndexesRef<'_>,
|
|
759
|
+
) -> Result<TrackedStateWriteReport, LixError> {
|
|
760
|
+
storage::stage_delta_pack_refs_with_json_pack_indexes(
|
|
761
|
+
self.writes,
|
|
762
|
+
commit_id,
|
|
763
|
+
deltas,
|
|
764
|
+
json_pack_indexes,
|
|
765
|
+
)?;
|
|
766
|
+
Ok(TrackedStateWriteReport {
|
|
767
|
+
commit_id: commit_id.to_string(),
|
|
768
|
+
changed_rows: deltas.len(),
|
|
769
|
+
primary_chunk_puts: 0,
|
|
770
|
+
by_file_chunk_puts: 0,
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
pub(crate) async fn stage_projection_root<'a, I>(
|
|
775
|
+
&mut self,
|
|
776
|
+
commit_id: &str,
|
|
777
|
+
parent_commit_id: Option<&str>,
|
|
778
|
+
deltas: I,
|
|
779
|
+
) -> Result<TrackedStateWriteReport, LixError>
|
|
780
|
+
where
|
|
781
|
+
I: IntoIterator<Item = TrackedStateDeltaRef<'a>>,
|
|
782
|
+
{
|
|
783
|
+
let deltas = deltas.into_iter().collect::<Vec<_>>();
|
|
784
|
+
let base_root = match parent_commit_id {
|
|
785
|
+
Some(parent_commit_id) => {
|
|
786
|
+
let Some(root) = self.tree.load_root(self.store, parent_commit_id).await? else {
|
|
787
|
+
return Err(LixError::new(
|
|
788
|
+
"LIX_ERROR_UNKNOWN",
|
|
789
|
+
format!(
|
|
790
|
+
"tracked-state parent root for commit '{parent_commit_id}' is missing"
|
|
791
|
+
),
|
|
792
|
+
));
|
|
793
|
+
};
|
|
794
|
+
Some(root)
|
|
795
|
+
}
|
|
796
|
+
None => None,
|
|
797
|
+
};
|
|
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),
|
|
816
|
+
));
|
|
817
|
+
}
|
|
818
|
+
let result = self
|
|
819
|
+
.tree
|
|
820
|
+
.apply_mutations(
|
|
821
|
+
self.store,
|
|
822
|
+
self.writes,
|
|
823
|
+
base_root.as_ref(),
|
|
824
|
+
mutations,
|
|
825
|
+
Some(commit_id),
|
|
826
|
+
)
|
|
827
|
+
.await?;
|
|
828
|
+
|
|
829
|
+
let by_file_base_root = match parent_commit_id {
|
|
830
|
+
Some(parent_commit_id) => {
|
|
831
|
+
storage::load_by_file_root(self.store, parent_commit_id).await?
|
|
832
|
+
}
|
|
833
|
+
None => None,
|
|
834
|
+
};
|
|
835
|
+
let concrete_file_deltas = deltas
|
|
836
|
+
.iter()
|
|
837
|
+
.filter(|delta| delta.change.file_id.is_some())
|
|
838
|
+
.collect::<Vec<_>>();
|
|
839
|
+
let by_file_chunk_puts = if concrete_file_deltas.is_empty() {
|
|
840
|
+
if let Some(by_file_base_root) = by_file_base_root.as_ref() {
|
|
841
|
+
storage::stage_by_file_root(self.writes, commit_id, by_file_base_root);
|
|
842
|
+
}
|
|
843
|
+
0
|
|
844
|
+
} else {
|
|
845
|
+
let mut by_file_mutations = Vec::with_capacity(concrete_file_deltas.len());
|
|
846
|
+
for delta in concrete_file_deltas {
|
|
847
|
+
let key = TrackedStateKeyRef {
|
|
848
|
+
schema_key: delta.change.schema_key,
|
|
849
|
+
file_id: delta.change.file_id,
|
|
850
|
+
entity_id: delta.change.entity_id,
|
|
851
|
+
};
|
|
852
|
+
let header_value = crate::tracked_state::types::TrackedStateIndexValueRef {
|
|
853
|
+
change_locator: delta.locator,
|
|
854
|
+
deleted: delta.change.snapshot_ref.is_none(),
|
|
855
|
+
snapshot_ref: None,
|
|
856
|
+
metadata_ref: None,
|
|
857
|
+
created_at: delta.created_at,
|
|
858
|
+
updated_at: delta.updated_at,
|
|
859
|
+
};
|
|
860
|
+
by_file_mutations.push(TrackedStateMutation::put_encoded(
|
|
861
|
+
ByFileIndex::encode_key_ref(key),
|
|
862
|
+
ByFileIndex::encode_header_value_ref(header_value),
|
|
863
|
+
));
|
|
864
|
+
}
|
|
865
|
+
let by_file_result = self
|
|
866
|
+
.tree
|
|
867
|
+
.apply_mutations(
|
|
868
|
+
self.store,
|
|
869
|
+
self.writes,
|
|
870
|
+
by_file_base_root.as_ref(),
|
|
871
|
+
by_file_mutations,
|
|
872
|
+
None,
|
|
873
|
+
)
|
|
874
|
+
.await?;
|
|
875
|
+
storage::stage_by_file_root(self.writes, commit_id, &by_file_result.root_id);
|
|
876
|
+
by_file_result.chunk_count
|
|
877
|
+
};
|
|
878
|
+
Ok(TrackedStateWriteReport {
|
|
879
|
+
commit_id: commit_id.to_string(),
|
|
880
|
+
changed_rows: deltas.len(),
|
|
881
|
+
primary_chunk_puts: result.chunk_count,
|
|
882
|
+
by_file_chunk_puts,
|
|
883
|
+
})
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
888
|
+
pub(crate) struct TrackedStateWriteReport {
|
|
889
|
+
pub(crate) commit_id: String,
|
|
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
|
+
)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
fn tree_scan_request_from_tracked(
|
|
903
|
+
request: &TrackedStateScanRequest,
|
|
904
|
+
) -> TrackedStateTreeScanRequest {
|
|
905
|
+
TrackedStateTreeScanRequest {
|
|
906
|
+
schema_keys: request.filter.schema_keys.clone(),
|
|
907
|
+
entity_ids: request.filter.entity_ids.clone(),
|
|
908
|
+
file_ids: request.filter.file_ids.clone(),
|
|
909
|
+
include_tombstones: request.filter.include_tombstones,
|
|
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,
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fn scan_needs_json_payloads(request: &TrackedStateScanRequest) -> bool {
|
|
918
|
+
if request.projection.columns.is_empty() {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
request
|
|
922
|
+
.projection
|
|
923
|
+
.columns
|
|
924
|
+
.iter()
|
|
925
|
+
.any(|column| column == "snapshot_content" || column == "metadata")
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
fn tracked_key_from_request(request: &TrackedStateRowRequest) -> Result<TrackedStateKey, LixError> {
|
|
929
|
+
let file_id = match &request.file_id {
|
|
930
|
+
crate::NullableKeyFilter::Null => None,
|
|
931
|
+
crate::NullableKeyFilter::Value(value) => Some(value.clone()),
|
|
932
|
+
crate::NullableKeyFilter::Any => {
|
|
933
|
+
return Err(LixError::new(
|
|
934
|
+
"LIX_ERROR_UNKNOWN",
|
|
935
|
+
"tracked-state tree exact lookup requires a concrete file_id filter",
|
|
936
|
+
))
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
Ok(TrackedStateKey {
|
|
940
|
+
schema_key: request.schema_key.clone(),
|
|
941
|
+
file_id,
|
|
942
|
+
entity_id: request.entity_id.clone(),
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
#[cfg(test)]
|
|
947
|
+
mod tests {
|
|
948
|
+
use std::sync::Arc;
|
|
949
|
+
|
|
950
|
+
use super::*;
|
|
951
|
+
use crate::backend::{testing::UnitTestBackend, Backend};
|
|
952
|
+
use crate::storage::{StorageContext, StorageWriteTransaction};
|
|
953
|
+
use crate::NullableKeyFilter;
|
|
954
|
+
|
|
955
|
+
#[tokio::test]
|
|
956
|
+
async fn stage_delta_does_not_require_parent_projection_root() {
|
|
957
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
958
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
959
|
+
let tracked_state = TrackedStateContext::new();
|
|
960
|
+
let mut transaction = storage
|
|
961
|
+
.begin_write_transaction()
|
|
962
|
+
.await
|
|
963
|
+
.expect("transaction should open");
|
|
964
|
+
|
|
965
|
+
write_root_for_test(
|
|
966
|
+
transaction.as_mut(),
|
|
967
|
+
&tracked_state,
|
|
968
|
+
"commit-child",
|
|
969
|
+
Some("missing-parent"),
|
|
970
|
+
&[row("entity-child", "change-child", "commit-child")],
|
|
971
|
+
)
|
|
972
|
+
.await
|
|
973
|
+
.expect("delta pack staging should not require a parent projection root");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
#[tokio::test]
|
|
977
|
+
async fn plan_merge_from_roots_applies_source_only_change() {
|
|
978
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
979
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
980
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
981
|
+
&[row_with_value(
|
|
982
|
+
"entity-a",
|
|
983
|
+
"change-source",
|
|
984
|
+
"source",
|
|
985
|
+
"source",
|
|
986
|
+
)],
|
|
987
|
+
)
|
|
988
|
+
.await;
|
|
989
|
+
|
|
990
|
+
let plan = tracked_state
|
|
991
|
+
.reader(storage.clone())
|
|
992
|
+
.plan_merge(
|
|
993
|
+
"base",
|
|
994
|
+
"target",
|
|
995
|
+
"source",
|
|
996
|
+
&TrackedStateDiffRequest::default(),
|
|
997
|
+
)
|
|
998
|
+
.await
|
|
999
|
+
.expect("merge should plan");
|
|
1000
|
+
|
|
1001
|
+
assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
|
|
1002
|
+
assert!(plan.conflicts.is_empty());
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
#[tokio::test]
|
|
1006
|
+
async fn plan_merge_from_roots_keeps_target_only_change() {
|
|
1007
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
1008
|
+
&[row("entity-a", "change-base", "base")],
|
|
1009
|
+
&[row("entity-a", "change-target", "target")],
|
|
1010
|
+
&[row("entity-a", "change-base", "base")],
|
|
1011
|
+
)
|
|
1012
|
+
.await;
|
|
1013
|
+
|
|
1014
|
+
let plan = tracked_state
|
|
1015
|
+
.reader(storage.clone())
|
|
1016
|
+
.plan_merge(
|
|
1017
|
+
"base",
|
|
1018
|
+
"target",
|
|
1019
|
+
"source",
|
|
1020
|
+
&TrackedStateDiffRequest::default(),
|
|
1021
|
+
)
|
|
1022
|
+
.await
|
|
1023
|
+
.expect("merge should plan");
|
|
1024
|
+
|
|
1025
|
+
assert!(plan.patches.is_empty());
|
|
1026
|
+
assert!(plan.conflicts.is_empty());
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
#[tokio::test]
|
|
1030
|
+
async fn plan_merge_from_roots_reports_divergent_modification_conflict() {
|
|
1031
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
1032
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
1033
|
+
&[row_with_value(
|
|
1034
|
+
"entity-a",
|
|
1035
|
+
"change-target",
|
|
1036
|
+
"target",
|
|
1037
|
+
"target",
|
|
1038
|
+
)],
|
|
1039
|
+
&[row_with_value(
|
|
1040
|
+
"entity-a",
|
|
1041
|
+
"change-source",
|
|
1042
|
+
"source",
|
|
1043
|
+
"source",
|
|
1044
|
+
)],
|
|
1045
|
+
)
|
|
1046
|
+
.await;
|
|
1047
|
+
|
|
1048
|
+
let plan = tracked_state
|
|
1049
|
+
.reader(storage.clone())
|
|
1050
|
+
.plan_merge(
|
|
1051
|
+
"base",
|
|
1052
|
+
"target",
|
|
1053
|
+
"source",
|
|
1054
|
+
&TrackedStateDiffRequest::default(),
|
|
1055
|
+
)
|
|
1056
|
+
.await
|
|
1057
|
+
.expect("merge should plan");
|
|
1058
|
+
|
|
1059
|
+
assert!(plan.patches.is_empty());
|
|
1060
|
+
assert_eq!(merge_conflict_ids(&plan), vec!["entity-a"]);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
#[tokio::test]
|
|
1064
|
+
async fn plan_merge_from_roots_applies_source_tombstone() {
|
|
1065
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
1066
|
+
&[row("entity-a", "change-base", "base")],
|
|
1067
|
+
&[row("entity-a", "change-base", "base")],
|
|
1068
|
+
&[tombstone("entity-a", "change-source-delete", "source")],
|
|
1069
|
+
)
|
|
1070
|
+
.await;
|
|
1071
|
+
|
|
1072
|
+
let plan = tracked_state
|
|
1073
|
+
.reader(storage.clone())
|
|
1074
|
+
.plan_merge(
|
|
1075
|
+
"base",
|
|
1076
|
+
"target",
|
|
1077
|
+
"source",
|
|
1078
|
+
&TrackedStateDiffRequest::default(),
|
|
1079
|
+
)
|
|
1080
|
+
.await
|
|
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");
|
|
1199
|
+
|
|
1200
|
+
assert_eq!(header_rows[0].snapshot_content, None);
|
|
1201
|
+
assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
#[tokio::test]
|
|
1205
|
+
async fn null_file_rows_do_not_stage_by_file_index() {
|
|
1206
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1207
|
+
let storage = StorageContext::new(backend.clone());
|
|
1208
|
+
let tracked_state = TrackedStateContext::new();
|
|
1209
|
+
let row = row("entity-a", "change-a", "commit-1");
|
|
1210
|
+
|
|
1211
|
+
let mut transaction = storage
|
|
1212
|
+
.begin_write_transaction()
|
|
1213
|
+
.await
|
|
1214
|
+
.expect("transaction should open");
|
|
1215
|
+
write_root_for_test(
|
|
1216
|
+
transaction.as_mut(),
|
|
1217
|
+
&tracked_state,
|
|
1218
|
+
"commit-1",
|
|
1219
|
+
None,
|
|
1220
|
+
std::slice::from_ref(&row),
|
|
1221
|
+
)
|
|
1222
|
+
.await
|
|
1223
|
+
.expect("root should write");
|
|
1224
|
+
transaction
|
|
1225
|
+
.commit()
|
|
1226
|
+
.await
|
|
1227
|
+
.expect("transaction should commit");
|
|
1228
|
+
|
|
1229
|
+
let by_file_root = storage::load_by_file_root(&mut storage.clone(), "commit-1")
|
|
1230
|
+
.await
|
|
1231
|
+
.expect("by-file root lookup should load");
|
|
1232
|
+
assert!(by_file_root.is_none());
|
|
1233
|
+
|
|
1234
|
+
let rows = tracked_state
|
|
1235
|
+
.reader(storage.clone())
|
|
1236
|
+
.scan_rows_at_commit(
|
|
1237
|
+
"commit-1",
|
|
1238
|
+
&TrackedStateScanRequest {
|
|
1239
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1240
|
+
file_ids: vec![NullableKeyFilter::Null],
|
|
1241
|
+
..Default::default()
|
|
1242
|
+
},
|
|
1243
|
+
..Default::default()
|
|
1244
|
+
},
|
|
1245
|
+
)
|
|
1246
|
+
.await
|
|
1247
|
+
.expect("null file scan should fall back to primary tree");
|
|
1248
|
+
|
|
1249
|
+
assert_eq!(rows.len(), 1);
|
|
1250
|
+
assert_eq!(
|
|
1251
|
+
rows[0]
|
|
1252
|
+
.entity_id
|
|
1253
|
+
.as_single_string_owned()
|
|
1254
|
+
.expect("entity id"),
|
|
1255
|
+
"entity-a"
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
#[tokio::test]
|
|
1260
|
+
async fn mixed_null_and_concrete_file_scan_uses_primary_tree() {
|
|
1261
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1262
|
+
let storage = StorageContext::new(backend.clone());
|
|
1263
|
+
let tracked_state = TrackedStateContext::new();
|
|
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());
|
|
1267
|
+
|
|
1268
|
+
let mut transaction = storage
|
|
1269
|
+
.begin_write_transaction()
|
|
1270
|
+
.await
|
|
1271
|
+
.expect("transaction should open");
|
|
1272
|
+
write_root_for_test(
|
|
1273
|
+
transaction.as_mut(),
|
|
1274
|
+
&tracked_state,
|
|
1275
|
+
"commit-1",
|
|
1276
|
+
None,
|
|
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],
|
|
1341
|
+
)
|
|
1342
|
+
.await
|
|
1343
|
+
.expect("root should write");
|
|
1344
|
+
transaction
|
|
1345
|
+
.commit()
|
|
1346
|
+
.await
|
|
1347
|
+
.expect("transaction should commit");
|
|
1348
|
+
|
|
1349
|
+
let rows = tracked_state
|
|
1350
|
+
.reader(storage.clone())
|
|
1351
|
+
.scan_rows_at_commit(
|
|
1352
|
+
"commit-1",
|
|
1353
|
+
&TrackedStateScanRequest {
|
|
1354
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1355
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1356
|
+
..Default::default()
|
|
1357
|
+
},
|
|
1358
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
1359
|
+
columns: vec!["entity_id".to_string()],
|
|
1360
|
+
},
|
|
1361
|
+
..Default::default()
|
|
1362
|
+
},
|
|
1363
|
+
)
|
|
1364
|
+
.await
|
|
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())
|
|
1532
|
+
.scan_rows_at_commit(
|
|
1533
|
+
"commit-1",
|
|
1534
|
+
&TrackedStateScanRequest {
|
|
1535
|
+
limit: Some(1),
|
|
1536
|
+
..Default::default()
|
|
1537
|
+
},
|
|
1538
|
+
)
|
|
1539
|
+
.await
|
|
1540
|
+
.expect("limited scan should apply visibility before limit");
|
|
1541
|
+
|
|
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
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
#[tokio::test]
|
|
1553
|
+
async fn by_file_scan_limit_applies_after_tombstone_visibility() {
|
|
1554
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1555
|
+
let storage = StorageContext::new(backend.clone());
|
|
1556
|
+
let tracked_state = TrackedStateContext::new();
|
|
1557
|
+
let mut deleted = tombstone("entity-a", "change-delete", "commit-1");
|
|
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());
|
|
1561
|
+
|
|
1562
|
+
let mut transaction = storage
|
|
1563
|
+
.begin_write_transaction()
|
|
1564
|
+
.await
|
|
1565
|
+
.expect("transaction should open");
|
|
1566
|
+
write_root_for_test(
|
|
1567
|
+
transaction.as_mut(),
|
|
1568
|
+
&tracked_state,
|
|
1569
|
+
"commit-1",
|
|
1570
|
+
None,
|
|
1571
|
+
&[deleted, live],
|
|
1572
|
+
)
|
|
1573
|
+
.await
|
|
1574
|
+
.expect("root should write");
|
|
1575
|
+
transaction
|
|
1576
|
+
.commit()
|
|
1577
|
+
.await
|
|
1578
|
+
.expect("transaction should commit");
|
|
1579
|
+
|
|
1580
|
+
let rows = tracked_state
|
|
1581
|
+
.reader(storage.clone())
|
|
1582
|
+
.scan_rows_at_commit(
|
|
1583
|
+
"commit-1",
|
|
1584
|
+
&TrackedStateScanRequest {
|
|
1585
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
1586
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
1587
|
+
..Default::default()
|
|
1588
|
+
},
|
|
1589
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
1590
|
+
columns: vec!["entity_id".to_string()],
|
|
1591
|
+
},
|
|
1592
|
+
limit: Some(1),
|
|
1593
|
+
},
|
|
1594
|
+
)
|
|
1595
|
+
.await
|
|
1596
|
+
.expect("limited by-file scan should apply visibility before limit");
|
|
1597
|
+
|
|
1598
|
+
assert_eq!(rows.len(), 1);
|
|
1599
|
+
assert_eq!(
|
|
1600
|
+
rows[0]
|
|
1601
|
+
.entity_id
|
|
1602
|
+
.as_single_string_owned()
|
|
1603
|
+
.expect("entity id"),
|
|
1604
|
+
"entity-b"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
#[tokio::test]
|
|
1609
|
+
async fn reads_resolve_json_snapshot_refs() {
|
|
1610
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1611
|
+
let storage = StorageContext::new(backend.clone());
|
|
1612
|
+
let tracked_state = TrackedStateContext::new();
|
|
1613
|
+
let large_value = "x".repeat(1536);
|
|
1614
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
1615
|
+
|
|
1616
|
+
let mut transaction = storage
|
|
1617
|
+
.begin_write_transaction()
|
|
1618
|
+
.await
|
|
1619
|
+
.expect("transaction should open");
|
|
1620
|
+
write_root_for_test(
|
|
1621
|
+
transaction.as_mut(),
|
|
1622
|
+
&tracked_state,
|
|
1623
|
+
"commit-1",
|
|
1624
|
+
None,
|
|
1625
|
+
std::slice::from_ref(&row),
|
|
1626
|
+
)
|
|
1627
|
+
.await
|
|
1628
|
+
.expect("root should write");
|
|
1629
|
+
transaction
|
|
1630
|
+
.commit()
|
|
1631
|
+
.await
|
|
1632
|
+
.expect("transaction should commit");
|
|
1633
|
+
|
|
1634
|
+
let mut reader = tracked_state.reader(storage.clone());
|
|
1635
|
+
let loaded = reader
|
|
1636
|
+
.load_rows_at_commit(
|
|
1637
|
+
"commit-1",
|
|
1638
|
+
&[TrackedStateRowRequest {
|
|
1639
|
+
schema_key: row.schema_key.clone(),
|
|
1640
|
+
entity_id: row.entity_id.clone(),
|
|
1641
|
+
file_id: NullableKeyFilter::Null,
|
|
1642
|
+
}],
|
|
1643
|
+
)
|
|
1644
|
+
.await
|
|
1645
|
+
.expect("row should load")
|
|
1646
|
+
.pop()
|
|
1647
|
+
.flatten()
|
|
1648
|
+
.expect("row should exist");
|
|
1649
|
+
let scanned = reader
|
|
1650
|
+
.scan_rows_at_commit("commit-1", &TrackedStateScanRequest::default())
|
|
1651
|
+
.await
|
|
1652
|
+
.expect("rows should scan");
|
|
1653
|
+
|
|
1654
|
+
assert_eq!(loaded.snapshot_content, row.snapshot_content);
|
|
1655
|
+
assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
|
|
1656
|
+
}
|
|
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
|
+
|
|
1705
|
+
#[tokio::test]
|
|
1706
|
+
async fn projected_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
|
|
1707
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1708
|
+
let storage = StorageContext::new(backend.clone());
|
|
1709
|
+
let tracked_state = TrackedStateContext::new();
|
|
1710
|
+
let large_value = "x".repeat(1536);
|
|
1711
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
1712
|
+
|
|
1713
|
+
let mut transaction = storage
|
|
1714
|
+
.begin_write_transaction()
|
|
1715
|
+
.await
|
|
1716
|
+
.expect("transaction should open");
|
|
1717
|
+
write_root_for_test(
|
|
1718
|
+
transaction.as_mut(),
|
|
1719
|
+
&tracked_state,
|
|
1720
|
+
"commit-1",
|
|
1721
|
+
None,
|
|
1722
|
+
std::slice::from_ref(&row),
|
|
1723
|
+
)
|
|
1724
|
+
.await
|
|
1725
|
+
.expect("root should write");
|
|
1726
|
+
transaction
|
|
1727
|
+
.commit()
|
|
1728
|
+
.await
|
|
1729
|
+
.expect("transaction should commit");
|
|
1730
|
+
|
|
1731
|
+
let rows = tracked_state
|
|
1732
|
+
.reader(storage.clone())
|
|
1733
|
+
.scan_rows_at_commit(
|
|
1734
|
+
"commit-1",
|
|
1735
|
+
&TrackedStateScanRequest {
|
|
1736
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
1737
|
+
columns: vec!["entity_id".to_string()],
|
|
1738
|
+
},
|
|
1739
|
+
..Default::default()
|
|
1740
|
+
},
|
|
1741
|
+
)
|
|
1742
|
+
.await
|
|
1743
|
+
.expect("rows should scan");
|
|
1744
|
+
|
|
1745
|
+
assert_eq!(rows.len(), 1);
|
|
1746
|
+
assert_eq!(rows[0].snapshot_content, None);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async fn seed_merge_roots(
|
|
1750
|
+
base_rows: &[MaterializedTrackedStateRow],
|
|
1751
|
+
target_rows: &[MaterializedTrackedStateRow],
|
|
1752
|
+
source_rows: &[MaterializedTrackedStateRow],
|
|
1753
|
+
) -> (StorageContext, TrackedStateContext) {
|
|
1754
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
1755
|
+
let storage = StorageContext::new(backend.clone());
|
|
1756
|
+
let tracked_state = TrackedStateContext::new();
|
|
1757
|
+
let mut transaction = storage
|
|
1758
|
+
.begin_write_transaction()
|
|
1759
|
+
.await
|
|
1760
|
+
.expect("transaction should open");
|
|
1761
|
+
write_root_for_test(
|
|
1762
|
+
transaction.as_mut(),
|
|
1763
|
+
&tracked_state,
|
|
1764
|
+
"base",
|
|
1765
|
+
None,
|
|
1766
|
+
base_rows,
|
|
1767
|
+
)
|
|
1768
|
+
.await
|
|
1769
|
+
.expect("base root should write");
|
|
1770
|
+
write_root_for_test(
|
|
1771
|
+
transaction.as_mut(),
|
|
1772
|
+
&tracked_state,
|
|
1773
|
+
"target",
|
|
1774
|
+
None,
|
|
1775
|
+
target_rows,
|
|
1776
|
+
)
|
|
1777
|
+
.await
|
|
1778
|
+
.expect("target root should write");
|
|
1779
|
+
write_root_for_test(
|
|
1780
|
+
transaction.as_mut(),
|
|
1781
|
+
&tracked_state,
|
|
1782
|
+
"source",
|
|
1783
|
+
None,
|
|
1784
|
+
source_rows,
|
|
1785
|
+
)
|
|
1786
|
+
.await
|
|
1787
|
+
.expect("source root should write");
|
|
1788
|
+
transaction
|
|
1789
|
+
.commit()
|
|
1790
|
+
.await
|
|
1791
|
+
.expect("transaction should commit");
|
|
1792
|
+
(storage, tracked_state)
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
fn merge_patch_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
1796
|
+
plan.patches
|
|
1797
|
+
.iter()
|
|
1798
|
+
.map(|entry| {
|
|
1799
|
+
entry
|
|
1800
|
+
.identity()
|
|
1801
|
+
.entity_id
|
|
1802
|
+
.as_single_string_owned()
|
|
1803
|
+
.expect("identity")
|
|
1804
|
+
})
|
|
1805
|
+
.collect()
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
1809
|
+
plan.conflicts
|
|
1810
|
+
.iter()
|
|
1811
|
+
.map(|entry| {
|
|
1812
|
+
entry
|
|
1813
|
+
.identity
|
|
1814
|
+
.entity_id
|
|
1815
|
+
.as_single_string_owned()
|
|
1816
|
+
.expect("identity")
|
|
1817
|
+
})
|
|
1818
|
+
.collect()
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
async fn write_root_for_test(
|
|
1822
|
+
transaction: &mut dyn StorageWriteTransaction,
|
|
1823
|
+
tracked_state: &TrackedStateContext,
|
|
1824
|
+
commit_id: &str,
|
|
1825
|
+
parent_commit_id: Option<&str>,
|
|
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
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
fn tombstone(entity_id: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
1839
|
+
let mut row = row(entity_id, change_id, commit_id);
|
|
1840
|
+
row.snapshot_content = None;
|
|
1841
|
+
row
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
fn row(entity_id: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
|
|
1845
|
+
row_with_value(entity_id, change_id, commit_id, "value")
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
fn row_with_value(
|
|
1849
|
+
entity_id: &str,
|
|
1850
|
+
change_id: &str,
|
|
1851
|
+
commit_id: &str,
|
|
1852
|
+
value: &str,
|
|
1853
|
+
) -> MaterializedTrackedStateRow {
|
|
1854
|
+
MaterializedTrackedStateRow {
|
|
1855
|
+
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
1856
|
+
schema_key: "test_schema".to_string(),
|
|
1857
|
+
file_id: None,
|
|
1858
|
+
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
1859
|
+
metadata: None,
|
|
1860
|
+
deleted: false,
|
|
1861
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1862
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1863
|
+
change_id: change_id.to_string(),
|
|
1864
|
+
commit_id: commit_id.to_string(),
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|