@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.2
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 +305 -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/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 +127 -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/changelog/codec.rs +321 -0
- package/dist-engine-src/src/changelog/context.rs +92 -0
- package/dist-engine-src/src/changelog/materialization.rs +121 -0
- package/dist-engine-src/src/changelog/mod.rs +13 -0
- package/dist-engine-src/src/changelog/reader.rs +20 -0
- package/dist-engine-src/src/changelog/storage.rs +220 -0
- package/dist-engine-src/src/changelog/types.rs +38 -0
- package/dist-engine-src/src/commit_graph/context.rs +1588 -0
- package/dist-engine-src/src/commit_graph/mod.rs +12 -0
- package/dist-engine-src/src/commit_graph/types.rs +145 -0
- package/dist-engine-src/src/commit_graph/walker.rs +780 -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 +135 -0
- package/dist-engine-src/src/common/metadata.rs +35 -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/engine.rs +239 -0
- package/dist-engine-src/src/entity_identity.rs +285 -0
- package/dist-engine-src/src/functions/context.rs +327 -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 +363 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +505 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +129 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +9 -0
- package/dist-engine-src/src/json_store/store.rs +236 -0
- package/dist-engine-src/src/json_store/types.rs +52 -0
- package/dist-engine-src/src/lib.rs +61 -0
- package/dist-engine-src/src/live_state/context.rs +2241 -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 +239 -0
- package/dist-engine-src/src/live_state/visibility.rs +218 -0
- package/dist-engine-src/src/plugin/archive.rs +441 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +637 -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 +119 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
- package/dist-engine-src/src/schema/definition.json +157 -0
- package/dist-engine-src/src/schema/definition.rs +636 -0
- package/dist-engine-src/src/schema/key.rs +206 -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 +739 -0
- package/dist-engine-src/src/schema_registry.rs +294 -0
- package/dist-engine-src/src/session/context.rs +366 -0
- package/dist-engine-src/src/session/create_version.rs +80 -0
- package/dist-engine-src/src/session/execute.rs +447 -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 +62 -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 +437 -0
- package/dist-engine-src/src/session/mod.rs +25 -0
- package/dist-engine-src/src/session/switch_version.rs +121 -0
- package/dist-engine-src/src/sql2/change_provider.rs +337 -0
- package/dist-engine-src/src/sql2/classify.rs +147 -0
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
- package/dist-engine-src/src/sql2/context.rs +307 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
- package/dist-engine-src/src/sql2/error.rs +196 -0
- package/dist-engine-src/src/sql2/execute.rs +3379 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
- package/dist-engine-src/src/sql2/history_projection.rs +80 -0
- package/dist-engine-src/src/sql2/history_provider.rs +418 -0
- package/dist-engine-src/src/sql2/history_route.rs +643 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
- package/dist-engine-src/src/sql2/mod.rs +43 -0
- package/dist-engine-src/src/sql2/read_only.rs +65 -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 +135 -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_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1187 -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 +3406 -0
- package/dist-engine-src/src/test_support.rs +81 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
- package/dist-engine-src/src/tracked_state/codec.rs +747 -0
- package/dist-engine-src/src/tracked_state/context.rs +983 -0
- package/dist-engine-src/src/tracked_state/diff.rs +494 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
- package/dist-engine-src/src/tracked_state/merge.rs +474 -0
- package/dist-engine-src/src/tracked_state/mod.rs +31 -0
- package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
- package/dist-engine-src/src/tracked_state/storage.rs +243 -0
- package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
- package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
- package/dist-engine-src/src/tracked_state/types.rs +61 -0
- package/dist-engine-src/src/transaction/commit.rs +1224 -0
- package/dist-engine-src/src/transaction/context.rs +1307 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
- package/dist-engine-src/src/transaction/mod.rs +11 -0
- package/dist-engine-src/src/transaction/normalization.rs +1026 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
- package/dist-engine-src/src/transaction/staging.rs +1436 -0
- package/dist-engine-src/src/transaction/types.rs +351 -0
- package/dist-engine-src/src/transaction/validation.rs +4811 -0
- package/dist-engine-src/src/untracked_state/codec.rs +363 -0
- package/dist-engine-src/src/untracked_state/context.rs +82 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
- package/dist-engine-src/src/untracked_state/mod.rs +17 -0
- package/dist-engine-src/src/untracked_state/storage.rs +348 -0
- package/dist-engine-src/src/untracked_state/types.rs +96 -0
- package/dist-engine-src/src/version/context.rs +52 -0
- package/dist-engine-src/src/version/mod.rs +12 -0
- package/dist-engine-src/src/version/refs.rs +421 -0
- package/dist-engine-src/src/version/stage_rows.rs +71 -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,2241 @@
|
|
|
1
|
+
use async_trait::async_trait;
|
|
2
|
+
use tokio::sync::Mutex;
|
|
3
|
+
|
|
4
|
+
use crate::commit_graph::{CommitGraphCommit, CommitGraphContext};
|
|
5
|
+
use crate::live_state::visibility;
|
|
6
|
+
use crate::live_state::{
|
|
7
|
+
LiveStateFilter, LiveStateReader, LiveStateRow, LiveStateRowRequest, LiveStateScanRequest,
|
|
8
|
+
};
|
|
9
|
+
use crate::storage::{StorageReader, StorageWriteSet};
|
|
10
|
+
use crate::tracked_state::{
|
|
11
|
+
TrackedStateContext, TrackedStateFilter, TrackedStateProjection, TrackedStateRow,
|
|
12
|
+
TrackedStateRowRequest, TrackedStateScanRequest,
|
|
13
|
+
};
|
|
14
|
+
use crate::untracked_state::{
|
|
15
|
+
canonicalize_materialized_row, MaterializedUntrackedStateRow, UntrackedStateContext,
|
|
16
|
+
UntrackedStateIdentity, UntrackedStateRowRequest, UntrackedStateScanRequest,
|
|
17
|
+
};
|
|
18
|
+
use crate::version::VERSION_REF_SCHEMA_KEY;
|
|
19
|
+
use crate::LixError;
|
|
20
|
+
use crate::GLOBAL_VERSION_ID;
|
|
21
|
+
|
|
22
|
+
/// Serving facade for visible live-state readers and writers.
|
|
23
|
+
///
|
|
24
|
+
/// Live state composes the rebuildable tracked projection with the durable
|
|
25
|
+
/// untracked local overlay. Lower stores own persistence; this facade owns the
|
|
26
|
+
/// visibility rule.
|
|
27
|
+
pub(crate) struct LiveStateContext {
|
|
28
|
+
tracked_state: TrackedStateContext,
|
|
29
|
+
untracked_state: UntrackedStateContext,
|
|
30
|
+
commit_graph: CommitGraphContext,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl LiveStateContext {
|
|
34
|
+
pub(crate) fn new(
|
|
35
|
+
tracked_state: TrackedStateContext,
|
|
36
|
+
untracked_state: UntrackedStateContext,
|
|
37
|
+
commit_graph: CommitGraphContext,
|
|
38
|
+
) -> Self {
|
|
39
|
+
Self {
|
|
40
|
+
tracked_state,
|
|
41
|
+
untracked_state,
|
|
42
|
+
commit_graph,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Creates a visible live-state reader over a caller-provided KV store.
|
|
47
|
+
pub(crate) fn reader<S>(&self, store: S) -> LiveStateStoreReader<S>
|
|
48
|
+
where
|
|
49
|
+
S: StorageReader,
|
|
50
|
+
{
|
|
51
|
+
LiveStateStoreReader {
|
|
52
|
+
store: Mutex::new(store),
|
|
53
|
+
tracked_state: self.tracked_state.clone(),
|
|
54
|
+
untracked_state: self.untracked_state,
|
|
55
|
+
commit_graph: self.commit_graph.clone(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Creates a visible live-state writer over a caller-provided KV reader.
|
|
60
|
+
///
|
|
61
|
+
/// The writer owns the tracked/untracked routing rule: tracked rows update
|
|
62
|
+
/// the tracked projection and clear matching untracked overlay rows, while
|
|
63
|
+
/// untracked rows update only the local untracked overlay.
|
|
64
|
+
pub(crate) fn writer<S>(&self, store: S) -> LiveStateWriter<S>
|
|
65
|
+
where
|
|
66
|
+
S: StorageReader,
|
|
67
|
+
{
|
|
68
|
+
LiveStateWriter {
|
|
69
|
+
store,
|
|
70
|
+
tracked_state: self.tracked_state.clone(),
|
|
71
|
+
untracked_state: self.untracked_state,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Visible live-state reader backed by a caller-provided KV store.
|
|
77
|
+
pub(crate) struct LiveStateStoreReader<S> {
|
|
78
|
+
store: Mutex<S>,
|
|
79
|
+
tracked_state: TrackedStateContext,
|
|
80
|
+
untracked_state: UntrackedStateContext,
|
|
81
|
+
commit_graph: CommitGraphContext,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<S> LiveStateStoreReader<S>
|
|
85
|
+
where
|
|
86
|
+
S: StorageReader,
|
|
87
|
+
{
|
|
88
|
+
pub(crate) async fn scan_rows(
|
|
89
|
+
&self,
|
|
90
|
+
request: &LiveStateScanRequest,
|
|
91
|
+
) -> Result<Vec<LiveStateRow>, LixError> {
|
|
92
|
+
let mut store = self.store.lock().await;
|
|
93
|
+
let scope = scan_scope(&mut *store, &self.untracked_state, request).await?;
|
|
94
|
+
let mut tracked_rows = Vec::new();
|
|
95
|
+
for version_id in &scope.storage_version_ids {
|
|
96
|
+
let Some(commit_id) =
|
|
97
|
+
load_version_ref_commit_id(&mut *store, &self.untracked_state, version_id).await?
|
|
98
|
+
else {
|
|
99
|
+
continue;
|
|
100
|
+
};
|
|
101
|
+
let tracked_request = tracked_scan_request_from_live(request);
|
|
102
|
+
let source = tracked_source_from_version_id(version_id);
|
|
103
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
104
|
+
tracked_rows.extend(
|
|
105
|
+
self.tracked_state
|
|
106
|
+
.reader(store)
|
|
107
|
+
.scan_rows_at_commit(&commit_id, &tracked_request)
|
|
108
|
+
.await?
|
|
109
|
+
.into_iter()
|
|
110
|
+
.map(|row| project_tracked_row(row, version_id, source)),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let untracked_rows = {
|
|
115
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
116
|
+
self.untracked_state
|
|
117
|
+
.reader(store)
|
|
118
|
+
.scan_rows(&untracked_scan_request_from_live(
|
|
119
|
+
request,
|
|
120
|
+
&scope.storage_version_ids,
|
|
121
|
+
))
|
|
122
|
+
.await?
|
|
123
|
+
}
|
|
124
|
+
.into_iter()
|
|
125
|
+
.map(LiveStateRow::from)
|
|
126
|
+
.collect::<Vec<_>>();
|
|
127
|
+
|
|
128
|
+
let mut commit_rows = if scope.includes_commit_graph_projection {
|
|
129
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
130
|
+
self.commit_graph
|
|
131
|
+
.reader(store)
|
|
132
|
+
.all_commits()
|
|
133
|
+
.await?
|
|
134
|
+
.into_iter()
|
|
135
|
+
.map(live_state_row_from_commit)
|
|
136
|
+
.collect::<Vec<_>>()
|
|
137
|
+
} else {
|
|
138
|
+
Vec::new()
|
|
139
|
+
};
|
|
140
|
+
commit_rows.retain(|row| live_state_row_matches_filter(row, &request.filter));
|
|
141
|
+
|
|
142
|
+
let mut rows =
|
|
143
|
+
crate::live_state::overlay::overlay_untracked_rows(tracked_rows, untracked_rows);
|
|
144
|
+
rows.extend(commit_rows);
|
|
145
|
+
rows = visibility::resolve_scan_rows(
|
|
146
|
+
rows,
|
|
147
|
+
&scope.projection_version_ids,
|
|
148
|
+
request.filter.include_tombstones,
|
|
149
|
+
);
|
|
150
|
+
if let Some(limit) = request.limit {
|
|
151
|
+
rows.truncate(limit);
|
|
152
|
+
}
|
|
153
|
+
Ok(rows)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub(crate) async fn load_row(
|
|
157
|
+
&self,
|
|
158
|
+
request: &LiveStateRowRequest,
|
|
159
|
+
) -> Result<Option<LiveStateRow>, LixError> {
|
|
160
|
+
let mut store = self.store.lock().await;
|
|
161
|
+
if !version_ref_exists(&mut *store, &self.untracked_state, &request.version_id).await? {
|
|
162
|
+
return Ok(None);
|
|
163
|
+
}
|
|
164
|
+
if request.schema_key == COMMIT_SCHEMA_KEY {
|
|
165
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
166
|
+
return self.load_commit_row(store, request).await;
|
|
167
|
+
}
|
|
168
|
+
for candidate in load_row_candidates(request) {
|
|
169
|
+
match candidate.source {
|
|
170
|
+
LiveStateLookupSource::Untracked => {
|
|
171
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
172
|
+
if let Some(row) = self
|
|
173
|
+
.untracked_state
|
|
174
|
+
.reader(store)
|
|
175
|
+
.load_row(&untracked_row_request_from_live(
|
|
176
|
+
request,
|
|
177
|
+
&candidate.version_id,
|
|
178
|
+
))
|
|
179
|
+
.await?
|
|
180
|
+
{
|
|
181
|
+
return Ok(Some(visibility::project_loaded_row(
|
|
182
|
+
LiveStateRow::from(row),
|
|
183
|
+
&request.version_id,
|
|
184
|
+
&candidate.version_id,
|
|
185
|
+
)));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
LiveStateLookupSource::Tracked => {
|
|
189
|
+
let Some(commit_id) = load_version_ref_commit_id(
|
|
190
|
+
&mut *store,
|
|
191
|
+
&self.untracked_state,
|
|
192
|
+
&candidate.version_id,
|
|
193
|
+
)
|
|
194
|
+
.await?
|
|
195
|
+
else {
|
|
196
|
+
continue;
|
|
197
|
+
};
|
|
198
|
+
let store: &mut dyn StorageReader = &mut *store;
|
|
199
|
+
if let Some(row) = self
|
|
200
|
+
.tracked_state
|
|
201
|
+
.reader(store)
|
|
202
|
+
.load_row_at_commit(&commit_id, &tracked_row_request_from_live(request))
|
|
203
|
+
.await?
|
|
204
|
+
{
|
|
205
|
+
return Ok(Some(project_tracked_row(
|
|
206
|
+
row,
|
|
207
|
+
&request.version_id,
|
|
208
|
+
tracked_source_from_version_id(&candidate.version_id),
|
|
209
|
+
)));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
Ok(None)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async fn load_commit_row(
|
|
218
|
+
&self,
|
|
219
|
+
store: &mut dyn StorageReader,
|
|
220
|
+
request: &LiveStateRowRequest,
|
|
221
|
+
) -> Result<Option<LiveStateRow>, LixError> {
|
|
222
|
+
if !nullable_filter_matches(&request.file_id, &None) {
|
|
223
|
+
return Ok(None);
|
|
224
|
+
}
|
|
225
|
+
let Some(commit) = self
|
|
226
|
+
.commit_graph
|
|
227
|
+
.reader(store)
|
|
228
|
+
.load_commit(&request.entity_id.as_string()?)
|
|
229
|
+
.await?
|
|
230
|
+
else {
|
|
231
|
+
return Ok(None);
|
|
232
|
+
};
|
|
233
|
+
let row = live_state_row_from_commit(commit);
|
|
234
|
+
Ok(Some(visibility::project_loaded_row(
|
|
235
|
+
row,
|
|
236
|
+
&request.version_id,
|
|
237
|
+
GLOBAL_VERSION_ID,
|
|
238
|
+
)))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[async_trait]
|
|
243
|
+
impl<S> LiveStateReader for LiveStateStoreReader<S>
|
|
244
|
+
where
|
|
245
|
+
S: StorageReader + Sync,
|
|
246
|
+
{
|
|
247
|
+
async fn scan_rows(
|
|
248
|
+
&self,
|
|
249
|
+
request: &LiveStateScanRequest,
|
|
250
|
+
) -> Result<Vec<LiveStateRow>, LixError> {
|
|
251
|
+
LiveStateStoreReader::scan_rows(self, request).await
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async fn load_row(
|
|
255
|
+
&self,
|
|
256
|
+
request: &LiveStateRowRequest,
|
|
257
|
+
) -> Result<Option<LiveStateRow>, LixError> {
|
|
258
|
+
LiveStateStoreReader::load_row(self, request).await
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Writer for visible live-state rows over a caller-provided KV reader.
|
|
263
|
+
pub(crate) struct LiveStateWriter<S> {
|
|
264
|
+
store: S,
|
|
265
|
+
tracked_state: TrackedStateContext,
|
|
266
|
+
untracked_state: UntrackedStateContext,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
impl<S> LiveStateWriter<S>
|
|
270
|
+
where
|
|
271
|
+
S: StorageReader,
|
|
272
|
+
{
|
|
273
|
+
pub(crate) async fn stage_rows(
|
|
274
|
+
&mut self,
|
|
275
|
+
writes: &mut StorageWriteSet,
|
|
276
|
+
json_writer: &mut crate::json_store::JsonStoreWriter,
|
|
277
|
+
rows: &[LiveStateRow],
|
|
278
|
+
) -> Result<(), LixError> {
|
|
279
|
+
let (tracked_rows, untracked_rows): (Vec<_>, Vec<_>) =
|
|
280
|
+
rows.iter().partition(|row| !row.untracked);
|
|
281
|
+
|
|
282
|
+
if !untracked_rows.is_empty() {
|
|
283
|
+
let untracked_rows = untracked_rows
|
|
284
|
+
.into_iter()
|
|
285
|
+
.map(MaterializedUntrackedStateRow::from)
|
|
286
|
+
.collect::<Vec<_>>();
|
|
287
|
+
let canonical_rows = untracked_rows
|
|
288
|
+
.iter()
|
|
289
|
+
.map(|row| canonicalize_materialized_row(writes, json_writer, row))
|
|
290
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
291
|
+
self.untracked_state
|
|
292
|
+
.writer(writes)
|
|
293
|
+
.stage_rows(&canonical_rows)?;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if tracked_rows.is_empty() {
|
|
297
|
+
return Ok(());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let identities = tracked_rows
|
|
301
|
+
.iter()
|
|
302
|
+
.map(|row| {
|
|
303
|
+
Ok(UntrackedStateIdentity {
|
|
304
|
+
version_id: row.version_id.clone(),
|
|
305
|
+
schema_key: row.schema_key.clone(),
|
|
306
|
+
entity_id: row.entity_id.clone(),
|
|
307
|
+
file_id: row.file_id.clone(),
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
.collect::<Result<Vec<_>, LixError>>()?;
|
|
311
|
+
self.untracked_state
|
|
312
|
+
.writer(writes)
|
|
313
|
+
.stage_delete_rows(&identities);
|
|
314
|
+
|
|
315
|
+
for (commit_id, rows) in grouped_live_rows_by_commit(&tracked_rows)? {
|
|
316
|
+
let parent_commit_id = parent_commit_id_for_commit_rows(commit_id, &rows)?;
|
|
317
|
+
validate_root_local_write_batch(commit_id, &rows)?;
|
|
318
|
+
// Commit graph facts live in the changelog/commit_graph projection.
|
|
319
|
+
// They are present in the write batch so the tracked root can inherit
|
|
320
|
+
// parent metadata, but they are not stored as version entities.
|
|
321
|
+
let root_rows = rows
|
|
322
|
+
.iter()
|
|
323
|
+
.filter(|row| row.schema_key != COMMIT_SCHEMA_KEY)
|
|
324
|
+
.map(|row| TrackedStateRow::try_from(*row))
|
|
325
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
326
|
+
self.tracked_state
|
|
327
|
+
.writer()
|
|
328
|
+
.stage_root(
|
|
329
|
+
&mut self.store,
|
|
330
|
+
writes,
|
|
331
|
+
json_writer,
|
|
332
|
+
commit_id,
|
|
333
|
+
parent_commit_id.as_deref(),
|
|
334
|
+
&root_rows,
|
|
335
|
+
)
|
|
336
|
+
.await?;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
Ok(())
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn tracked_scan_request_from_live(request: &LiveStateScanRequest) -> TrackedStateScanRequest {
|
|
344
|
+
let mut columns = request.projection.columns.clone();
|
|
345
|
+
if !columns.is_empty() && !columns.iter().any(|column| column == "snapshot_content") {
|
|
346
|
+
columns.push("snapshot_content".to_string());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
TrackedStateScanRequest {
|
|
350
|
+
filter: TrackedStateFilter {
|
|
351
|
+
schema_keys: request.filter.schema_keys.clone(),
|
|
352
|
+
entity_ids: request.filter.entity_ids.clone(),
|
|
353
|
+
file_ids: request.filter.file_ids.clone(),
|
|
354
|
+
// Scan tombstones internally so version-local tombstones can hide
|
|
355
|
+
// global fallback rows before the serving facade filters them.
|
|
356
|
+
include_tombstones: true,
|
|
357
|
+
},
|
|
358
|
+
projection: TrackedStateProjection { columns },
|
|
359
|
+
limit: None,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn untracked_scan_request_from_live(
|
|
364
|
+
request: &LiveStateScanRequest,
|
|
365
|
+
version_ids: &[String],
|
|
366
|
+
) -> UntrackedStateScanRequest {
|
|
367
|
+
let mut filter: crate::untracked_state::UntrackedStateFilter = request.filter.clone().into();
|
|
368
|
+
filter.version_ids = version_ids.to_vec();
|
|
369
|
+
UntrackedStateScanRequest {
|
|
370
|
+
filter,
|
|
371
|
+
projection: Default::default(),
|
|
372
|
+
limit: None,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
377
|
+
struct LiveStateScanScope {
|
|
378
|
+
storage_version_ids: Vec<String>,
|
|
379
|
+
projection_version_ids: Vec<String>,
|
|
380
|
+
includes_commit_graph_projection: bool,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async fn scan_scope(
|
|
384
|
+
store: &mut dyn StorageReader,
|
|
385
|
+
untracked_state: &UntrackedStateContext,
|
|
386
|
+
request: &LiveStateScanRequest,
|
|
387
|
+
) -> Result<LiveStateScanScope, LixError> {
|
|
388
|
+
if request.filter.version_ids.is_empty() {
|
|
389
|
+
return Ok(LiveStateScanScope {
|
|
390
|
+
storage_version_ids: all_version_ref_ids(store, untracked_state).await?,
|
|
391
|
+
projection_version_ids: Vec::new(),
|
|
392
|
+
includes_commit_graph_projection: true,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let mut projection_version_ids = Vec::new();
|
|
397
|
+
for version_id in &request.filter.version_ids {
|
|
398
|
+
if version_ref_exists(store, untracked_state, version_id).await? {
|
|
399
|
+
projection_version_ids.push(version_id.clone());
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let storage_version_ids = visibility::expanded_version_ids(&projection_version_ids);
|
|
404
|
+
Ok(LiveStateScanScope {
|
|
405
|
+
storage_version_ids,
|
|
406
|
+
includes_commit_graph_projection: !projection_version_ids.is_empty(),
|
|
407
|
+
projection_version_ids,
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async fn all_version_ref_ids(
|
|
412
|
+
store: &mut dyn StorageReader,
|
|
413
|
+
untracked_state: &UntrackedStateContext,
|
|
414
|
+
) -> Result<Vec<String>, LixError> {
|
|
415
|
+
let rows = untracked_state
|
|
416
|
+
.reader(store)
|
|
417
|
+
.scan_rows(&UntrackedStateScanRequest {
|
|
418
|
+
filter: crate::untracked_state::UntrackedStateFilter {
|
|
419
|
+
schema_keys: vec![VERSION_REF_SCHEMA_KEY.to_string()],
|
|
420
|
+
version_ids: vec![GLOBAL_VERSION_ID.to_string()],
|
|
421
|
+
..Default::default()
|
|
422
|
+
},
|
|
423
|
+
..Default::default()
|
|
424
|
+
})
|
|
425
|
+
.await?;
|
|
426
|
+
rows.into_iter()
|
|
427
|
+
.map(|row| row.entity_id.as_string())
|
|
428
|
+
.collect()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async fn load_version_ref_commit_id(
|
|
432
|
+
store: &mut dyn StorageReader,
|
|
433
|
+
untracked_state: &UntrackedStateContext,
|
|
434
|
+
version_id: &str,
|
|
435
|
+
) -> Result<Option<String>, LixError> {
|
|
436
|
+
let Some(row) = untracked_state
|
|
437
|
+
.reader(store)
|
|
438
|
+
.load_row(&UntrackedStateRowRequest {
|
|
439
|
+
schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
|
|
440
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
441
|
+
entity_id: crate::entity_identity::EntityIdentity::single(version_id),
|
|
442
|
+
file_id: crate::NullableKeyFilter::Null,
|
|
443
|
+
})
|
|
444
|
+
.await?
|
|
445
|
+
else {
|
|
446
|
+
return Ok(None);
|
|
447
|
+
};
|
|
448
|
+
let Some(snapshot_content) = row.snapshot_content.as_deref() else {
|
|
449
|
+
return Ok(None);
|
|
450
|
+
};
|
|
451
|
+
let snapshot =
|
|
452
|
+
serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
|
|
453
|
+
LixError::new(
|
|
454
|
+
"LIX_ERROR_UNKNOWN",
|
|
455
|
+
format!("live_state version-ref snapshot parse failed: {error}"),
|
|
456
|
+
)
|
|
457
|
+
})?;
|
|
458
|
+
Ok(snapshot
|
|
459
|
+
.get("commit_id")
|
|
460
|
+
.and_then(serde_json::Value::as_str)
|
|
461
|
+
.map(str::to_string))
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async fn version_ref_exists(
|
|
465
|
+
store: &mut dyn StorageReader,
|
|
466
|
+
untracked_state: &UntrackedStateContext,
|
|
467
|
+
version_id: &str,
|
|
468
|
+
) -> Result<bool, LixError> {
|
|
469
|
+
Ok(
|
|
470
|
+
load_version_ref_commit_id(store, untracked_state, version_id)
|
|
471
|
+
.await?
|
|
472
|
+
.is_some(),
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const COMMIT_SCHEMA_KEY: &str = "lix_commit";
|
|
477
|
+
|
|
478
|
+
fn live_state_row_from_commit(commit: CommitGraphCommit) -> LiveStateRow {
|
|
479
|
+
let change = commit.change;
|
|
480
|
+
LiveStateRow {
|
|
481
|
+
entity_id: change.entity_id,
|
|
482
|
+
schema_key: change.schema_key,
|
|
483
|
+
file_id: change.file_id,
|
|
484
|
+
snapshot_content: change.snapshot_content,
|
|
485
|
+
metadata: change.metadata,
|
|
486
|
+
schema_version: change.schema_version,
|
|
487
|
+
created_at: change.created_at.clone(),
|
|
488
|
+
updated_at: change.created_at,
|
|
489
|
+
global: true,
|
|
490
|
+
change_id: Some(change.id),
|
|
491
|
+
commit_id: Some(commit.commit_id),
|
|
492
|
+
untracked: false,
|
|
493
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
498
|
+
enum TrackedRowSource {
|
|
499
|
+
Global,
|
|
500
|
+
Version,
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fn tracked_source_from_version_id(version_id: &str) -> TrackedRowSource {
|
|
504
|
+
if version_id == GLOBAL_VERSION_ID {
|
|
505
|
+
TrackedRowSource::Global
|
|
506
|
+
} else {
|
|
507
|
+
TrackedRowSource::Version
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
fn project_tracked_row(
|
|
512
|
+
row: TrackedStateRow,
|
|
513
|
+
view_version_id: &str,
|
|
514
|
+
source: TrackedRowSource,
|
|
515
|
+
) -> LiveStateRow {
|
|
516
|
+
LiveStateRow {
|
|
517
|
+
entity_id: row.entity_id,
|
|
518
|
+
schema_key: row.schema_key,
|
|
519
|
+
file_id: row.file_id,
|
|
520
|
+
snapshot_content: row.snapshot_content,
|
|
521
|
+
metadata: row.metadata,
|
|
522
|
+
schema_version: row.schema_version,
|
|
523
|
+
created_at: row.created_at,
|
|
524
|
+
updated_at: row.updated_at,
|
|
525
|
+
global: source == TrackedRowSource::Global,
|
|
526
|
+
change_id: Some(row.change_id),
|
|
527
|
+
commit_id: Some(row.commit_id),
|
|
528
|
+
untracked: false,
|
|
529
|
+
version_id: view_version_id.to_string(),
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fn live_state_row_matches_filter(row: &LiveStateRow, filter: &LiveStateFilter) -> bool {
|
|
534
|
+
if !filter.schema_keys.is_empty() && !filter.schema_keys.contains(&row.schema_key) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
if !filter.entity_ids.is_empty() && !filter.entity_ids.contains(&row.entity_id) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
if !filter.file_ids.is_empty()
|
|
541
|
+
&& !filter
|
|
542
|
+
.file_ids
|
|
543
|
+
.iter()
|
|
544
|
+
.any(|filter| nullable_filter_matches(filter, &row.file_id))
|
|
545
|
+
{
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
true
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
fn nullable_filter_matches(
|
|
552
|
+
filter: &crate::NullableKeyFilter<String>,
|
|
553
|
+
value: &Option<String>,
|
|
554
|
+
) -> bool {
|
|
555
|
+
match filter {
|
|
556
|
+
crate::NullableKeyFilter::Any => true,
|
|
557
|
+
crate::NullableKeyFilter::Null => value.is_none(),
|
|
558
|
+
crate::NullableKeyFilter::Value(expected) => value.as_ref() == Some(expected),
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
fn grouped_live_rows_by_commit<'a>(
|
|
563
|
+
rows: &[&'a LiveStateRow],
|
|
564
|
+
) -> Result<Vec<(&'a str, Vec<&'a LiveStateRow>)>, LixError> {
|
|
565
|
+
let mut grouped = Vec::<(&str, Vec<&LiveStateRow>)>::new();
|
|
566
|
+
for row in rows {
|
|
567
|
+
let commit_id = row.commit_id.as_deref().ok_or_else(|| {
|
|
568
|
+
LixError::new(
|
|
569
|
+
"LIX_ERROR_UNKNOWN",
|
|
570
|
+
"tracked live-state row is missing commit_id before tracked root write",
|
|
571
|
+
)
|
|
572
|
+
})?;
|
|
573
|
+
if let Some((_, bucket)) = grouped
|
|
574
|
+
.iter_mut()
|
|
575
|
+
.find(|(existing_commit_id, _)| *existing_commit_id == commit_id)
|
|
576
|
+
{
|
|
577
|
+
bucket.push(*row);
|
|
578
|
+
} else {
|
|
579
|
+
grouped.push((commit_id, vec![*row]));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
Ok(grouped)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
586
|
+
struct RootWriteScope {
|
|
587
|
+
version_id: String,
|
|
588
|
+
global: bool,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fn validate_root_local_write_batch(
|
|
592
|
+
commit_id: &str,
|
|
593
|
+
rows: &[&LiveStateRow],
|
|
594
|
+
) -> Result<(), LixError> {
|
|
595
|
+
let mut root_scope = None::<RootWriteScope>;
|
|
596
|
+
for row in rows
|
|
597
|
+
.iter()
|
|
598
|
+
.copied()
|
|
599
|
+
.filter(|row| row.schema_key != COMMIT_SCHEMA_KEY)
|
|
600
|
+
{
|
|
601
|
+
let scope = RootWriteScope {
|
|
602
|
+
version_id: row.version_id.clone(),
|
|
603
|
+
global: row.global,
|
|
604
|
+
};
|
|
605
|
+
if row.global != (row.version_id == GLOBAL_VERSION_ID) {
|
|
606
|
+
return Err(LixError::new(
|
|
607
|
+
"LIX_ERROR_UNKNOWN",
|
|
608
|
+
format!(
|
|
609
|
+
"tracked root write for commit '{commit_id}' has invalid storage scope: version_id='{}', global={}",
|
|
610
|
+
row.version_id, row.global
|
|
611
|
+
),
|
|
612
|
+
));
|
|
613
|
+
}
|
|
614
|
+
if let Some(existing) = &root_scope {
|
|
615
|
+
if existing != &scope {
|
|
616
|
+
return Err(LixError::new(
|
|
617
|
+
"LIX_ERROR_UNKNOWN",
|
|
618
|
+
format!(
|
|
619
|
+
"tracked root write for commit '{commit_id}' mixes multiple storage scopes"
|
|
620
|
+
),
|
|
621
|
+
));
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
root_scope = Some(scope);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
Ok(())
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn parent_commit_id_for_commit_rows(
|
|
631
|
+
commit_id: &str,
|
|
632
|
+
rows: &[&LiveStateRow],
|
|
633
|
+
) -> Result<Option<String>, LixError> {
|
|
634
|
+
let Some(row) = rows.iter().find(|row| {
|
|
635
|
+
row.schema_key == COMMIT_SCHEMA_KEY
|
|
636
|
+
&& row
|
|
637
|
+
.entity_id
|
|
638
|
+
.as_string()
|
|
639
|
+
.is_ok_and(|entity_id| entity_id == commit_id)
|
|
640
|
+
}) else {
|
|
641
|
+
return Ok(None);
|
|
642
|
+
};
|
|
643
|
+
parent_commit_id_from_commit_row(row)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn parent_commit_id_from_commit_row(row: &&LiveStateRow) -> Result<Option<String>, LixError> {
|
|
647
|
+
let snapshot = serde_json::from_str::<serde_json::Value>(
|
|
648
|
+
row.snapshot_content.as_deref().ok_or_else(|| {
|
|
649
|
+
LixError::new(
|
|
650
|
+
"LIX_ERROR_UNKNOWN",
|
|
651
|
+
"tracked root commit row is missing snapshot_content",
|
|
652
|
+
)
|
|
653
|
+
})?,
|
|
654
|
+
)
|
|
655
|
+
.map_err(|error| {
|
|
656
|
+
LixError::new(
|
|
657
|
+
"LIX_ERROR_UNKNOWN",
|
|
658
|
+
format!("tracked root commit snapshot parse failed: {error}"),
|
|
659
|
+
)
|
|
660
|
+
})?;
|
|
661
|
+
let Some(parent_commit_ids_value) = snapshot.get("parent_commit_ids") else {
|
|
662
|
+
return Err(LixError::new(
|
|
663
|
+
"LIX_ERROR_UNKNOWN",
|
|
664
|
+
"tracked root commit row is missing parent_commit_ids",
|
|
665
|
+
));
|
|
666
|
+
};
|
|
667
|
+
let Some(parent_commit_ids_array) = parent_commit_ids_value.as_array() else {
|
|
668
|
+
return Err(LixError::new(
|
|
669
|
+
"LIX_ERROR_UNKNOWN",
|
|
670
|
+
"tracked root commit parent_commit_ids must be an array",
|
|
671
|
+
));
|
|
672
|
+
};
|
|
673
|
+
let parent_commit_ids = parent_commit_ids_array
|
|
674
|
+
.iter()
|
|
675
|
+
.map(|value| {
|
|
676
|
+
value.as_str().map(str::to_string).ok_or_else(|| {
|
|
677
|
+
LixError::new(
|
|
678
|
+
"LIX_ERROR_UNKNOWN",
|
|
679
|
+
"tracked root commit parent_commit_ids must contain strings",
|
|
680
|
+
)
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
684
|
+
|
|
685
|
+
// Tracked roots inherit from the first parent. Merge commits record
|
|
686
|
+
// additional parents for graph ancestry, but the merge operation has
|
|
687
|
+
// already materialized the source-side rows as target-version writes.
|
|
688
|
+
Ok(parent_commit_ids.into_iter().next())
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
692
|
+
enum LiveStateLookupSource {
|
|
693
|
+
Untracked,
|
|
694
|
+
Tracked,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
698
|
+
struct LiveStateLookupCandidate {
|
|
699
|
+
source: LiveStateLookupSource,
|
|
700
|
+
version_id: String,
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fn load_row_candidates(request: &LiveStateRowRequest) -> Vec<LiveStateLookupCandidate> {
|
|
704
|
+
let mut candidates = vec![
|
|
705
|
+
LiveStateLookupCandidate {
|
|
706
|
+
source: LiveStateLookupSource::Untracked,
|
|
707
|
+
version_id: request.version_id.clone(),
|
|
708
|
+
},
|
|
709
|
+
LiveStateLookupCandidate {
|
|
710
|
+
source: LiveStateLookupSource::Tracked,
|
|
711
|
+
version_id: request.version_id.clone(),
|
|
712
|
+
},
|
|
713
|
+
];
|
|
714
|
+
|
|
715
|
+
if request.version_id != GLOBAL_VERSION_ID {
|
|
716
|
+
candidates.extend([
|
|
717
|
+
LiveStateLookupCandidate {
|
|
718
|
+
source: LiveStateLookupSource::Untracked,
|
|
719
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
720
|
+
},
|
|
721
|
+
LiveStateLookupCandidate {
|
|
722
|
+
source: LiveStateLookupSource::Tracked,
|
|
723
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
724
|
+
},
|
|
725
|
+
]);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
candidates
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
fn untracked_row_request_from_live(
|
|
732
|
+
request: &LiveStateRowRequest,
|
|
733
|
+
version_id: &str,
|
|
734
|
+
) -> crate::untracked_state::UntrackedStateRowRequest {
|
|
735
|
+
crate::untracked_state::UntrackedStateRowRequest {
|
|
736
|
+
schema_key: request.schema_key.clone(),
|
|
737
|
+
version_id: version_id.to_string(),
|
|
738
|
+
entity_id: request.entity_id.clone(),
|
|
739
|
+
file_id: request.file_id.clone(),
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
fn tracked_row_request_from_live(request: &LiveStateRowRequest) -> TrackedStateRowRequest {
|
|
744
|
+
TrackedStateRowRequest {
|
|
745
|
+
schema_key: request.schema_key.clone(),
|
|
746
|
+
entity_id: request.entity_id.clone(),
|
|
747
|
+
file_id: request.file_id.clone(),
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#[cfg(test)]
|
|
752
|
+
mod tests {
|
|
753
|
+
use std::sync::Arc;
|
|
754
|
+
|
|
755
|
+
use super::*;
|
|
756
|
+
use crate::backend::{testing::UnitTestBackend, Backend};
|
|
757
|
+
use crate::changelog::{canonicalize_materialized_change, MaterializedCanonicalChange};
|
|
758
|
+
use crate::entity_identity::EntityIdentity;
|
|
759
|
+
use crate::json_store::JsonStoreContext;
|
|
760
|
+
use crate::live_state::LiveStateFilter;
|
|
761
|
+
use crate::storage::{StorageContext, StorageWriteTransaction};
|
|
762
|
+
use crate::tracked_state::TrackedStateScanRequest;
|
|
763
|
+
use crate::untracked_state::{MaterializedUntrackedStateRow, UntrackedStateContext};
|
|
764
|
+
use crate::NullableKeyFilter;
|
|
765
|
+
use serde_json::json;
|
|
766
|
+
|
|
767
|
+
fn live_state_context() -> LiveStateContext {
|
|
768
|
+
LiveStateContext::new(
|
|
769
|
+
crate::tracked_state::TrackedStateContext::new(),
|
|
770
|
+
crate::untracked_state::UntrackedStateContext::new(),
|
|
771
|
+
crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async fn write_untracked_rows_to_store(
|
|
776
|
+
store: &mut (impl StorageWriteTransaction + ?Sized),
|
|
777
|
+
rows: &[MaterializedUntrackedStateRow],
|
|
778
|
+
) {
|
|
779
|
+
let mut writes = StorageWriteSet::new();
|
|
780
|
+
let canonical_rows = {
|
|
781
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
782
|
+
rows.iter()
|
|
783
|
+
.map(|row| canonicalize_materialized_row(&mut writes, &mut json_writer, row))
|
|
784
|
+
.collect::<Result<Vec<_>, _>>()
|
|
785
|
+
.expect("untracked rows should canonicalize")
|
|
786
|
+
};
|
|
787
|
+
UntrackedStateContext::new()
|
|
788
|
+
.writer(&mut writes)
|
|
789
|
+
.stage_rows(&canonical_rows)
|
|
790
|
+
.expect("untracked rows should write");
|
|
791
|
+
writes
|
|
792
|
+
.apply(store)
|
|
793
|
+
.await
|
|
794
|
+
.expect("untracked rows should apply");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
#[tokio::test]
|
|
798
|
+
async fn live_state_overlays_untracked_rows() {
|
|
799
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
800
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
801
|
+
let live_state = live_state_context();
|
|
802
|
+
|
|
803
|
+
let mut transaction = storage
|
|
804
|
+
.begin_write_transaction()
|
|
805
|
+
.await
|
|
806
|
+
.expect("transaction should open");
|
|
807
|
+
{
|
|
808
|
+
let mut writes = StorageWriteSet::new();
|
|
809
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
810
|
+
{
|
|
811
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
812
|
+
writer
|
|
813
|
+
.stage_rows(
|
|
814
|
+
&mut writes,
|
|
815
|
+
&mut json_writer,
|
|
816
|
+
&[tracked_row_with_commit(
|
|
817
|
+
"tracked-value",
|
|
818
|
+
Some("change-tracked"),
|
|
819
|
+
"commit-tracked",
|
|
820
|
+
)],
|
|
821
|
+
)
|
|
822
|
+
.await
|
|
823
|
+
.expect("tracked row should stage");
|
|
824
|
+
}
|
|
825
|
+
writes
|
|
826
|
+
.apply(&mut transaction.as_mut())
|
|
827
|
+
.await
|
|
828
|
+
.expect("tracked row should apply");
|
|
829
|
+
}
|
|
830
|
+
write_untracked_rows_to_store(
|
|
831
|
+
transaction.as_mut(),
|
|
832
|
+
&[
|
|
833
|
+
version_ref_row("global", "commit-tracked"),
|
|
834
|
+
untracked_row("untracked-value"),
|
|
835
|
+
],
|
|
836
|
+
)
|
|
837
|
+
.await;
|
|
838
|
+
transaction.commit().await.expect("commit should persist");
|
|
839
|
+
|
|
840
|
+
let rows = scan_selected_tab_at(&live_state, storage.clone(), "global", false)
|
|
841
|
+
.await
|
|
842
|
+
.expect("scan should succeed");
|
|
843
|
+
assert_eq!(rows.len(), 1);
|
|
844
|
+
assert_eq!(
|
|
845
|
+
rows[0].snapshot_content.as_deref(),
|
|
846
|
+
Some("{\"value\":\"untracked-value\"}")
|
|
847
|
+
);
|
|
848
|
+
assert!(rows[0].untracked);
|
|
849
|
+
assert_eq!(rows[0].change_id, None);
|
|
850
|
+
|
|
851
|
+
let loaded = live_state
|
|
852
|
+
.reader(storage.clone())
|
|
853
|
+
.load_row(&LiveStateRowRequest {
|
|
854
|
+
schema_key: "lix_key_value".to_string(),
|
|
855
|
+
version_id: "global".to_string(),
|
|
856
|
+
entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
|
|
857
|
+
file_id: NullableKeyFilter::Null,
|
|
858
|
+
})
|
|
859
|
+
.await
|
|
860
|
+
.expect("load should succeed")
|
|
861
|
+
.expect("overlay row should be visible");
|
|
862
|
+
assert!(loaded.untracked);
|
|
863
|
+
assert_eq!(
|
|
864
|
+
loaded.snapshot_content.as_deref(),
|
|
865
|
+
Some("{\"value\":\"untracked-value\"}")
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
#[tokio::test]
|
|
870
|
+
async fn tracked_row_is_visible_without_untracked_overlay() {
|
|
871
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
872
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
873
|
+
let live_state = live_state_context();
|
|
874
|
+
|
|
875
|
+
let mut transaction = storage
|
|
876
|
+
.begin_write_transaction()
|
|
877
|
+
.await
|
|
878
|
+
.expect("transaction should open");
|
|
879
|
+
{
|
|
880
|
+
let mut writes = StorageWriteSet::new();
|
|
881
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
882
|
+
{
|
|
883
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
884
|
+
writer
|
|
885
|
+
.stage_rows(
|
|
886
|
+
&mut writes,
|
|
887
|
+
&mut json_writer,
|
|
888
|
+
&[tracked_row_with_commit(
|
|
889
|
+
"tracked-value",
|
|
890
|
+
Some("change-tracked"),
|
|
891
|
+
"commit-tracked",
|
|
892
|
+
)],
|
|
893
|
+
)
|
|
894
|
+
.await
|
|
895
|
+
.expect("tracked row should stage");
|
|
896
|
+
}
|
|
897
|
+
writes
|
|
898
|
+
.apply(&mut transaction.as_mut())
|
|
899
|
+
.await
|
|
900
|
+
.expect("tracked row should apply");
|
|
901
|
+
}
|
|
902
|
+
write_untracked_rows_to_store(
|
|
903
|
+
transaction.as_mut(),
|
|
904
|
+
&[version_ref_row("global", "commit-tracked")],
|
|
905
|
+
)
|
|
906
|
+
.await;
|
|
907
|
+
transaction.commit().await.expect("commit should persist");
|
|
908
|
+
|
|
909
|
+
let loaded = load_selected_tab(&live_state, storage.clone())
|
|
910
|
+
.await
|
|
911
|
+
.expect("load should succeed")
|
|
912
|
+
.expect("tracked row should be visible");
|
|
913
|
+
assert!(!loaded.untracked);
|
|
914
|
+
assert_eq!(loaded.change_id.as_deref(), Some("change-tracked"));
|
|
915
|
+
assert_eq!(
|
|
916
|
+
loaded.snapshot_content.as_deref(),
|
|
917
|
+
Some("{\"value\":\"tracked-value\"}")
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#[tokio::test]
|
|
922
|
+
async fn deleting_untracked_row_reveals_tracked_row() {
|
|
923
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
924
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
925
|
+
let live_state = live_state_context();
|
|
926
|
+
|
|
927
|
+
let mut transaction = storage
|
|
928
|
+
.begin_write_transaction()
|
|
929
|
+
.await
|
|
930
|
+
.expect("transaction should open");
|
|
931
|
+
{
|
|
932
|
+
let mut writes = StorageWriteSet::new();
|
|
933
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
934
|
+
{
|
|
935
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
936
|
+
writer
|
|
937
|
+
.stage_rows(
|
|
938
|
+
&mut writes,
|
|
939
|
+
&mut json_writer,
|
|
940
|
+
&[tracked_row_with_commit(
|
|
941
|
+
"tracked-value",
|
|
942
|
+
Some("change-tracked"),
|
|
943
|
+
"commit-tracked",
|
|
944
|
+
)],
|
|
945
|
+
)
|
|
946
|
+
.await
|
|
947
|
+
.expect("tracked row should stage");
|
|
948
|
+
}
|
|
949
|
+
writes
|
|
950
|
+
.apply(&mut transaction.as_mut())
|
|
951
|
+
.await
|
|
952
|
+
.expect("tracked row should apply");
|
|
953
|
+
}
|
|
954
|
+
write_untracked_rows_to_store(
|
|
955
|
+
transaction.as_mut(),
|
|
956
|
+
&[
|
|
957
|
+
version_ref_row("global", "commit-tracked"),
|
|
958
|
+
untracked_row("untracked-value"),
|
|
959
|
+
],
|
|
960
|
+
)
|
|
961
|
+
.await;
|
|
962
|
+
{
|
|
963
|
+
let mut writes = StorageWriteSet::new();
|
|
964
|
+
UntrackedStateContext::new()
|
|
965
|
+
.writer(&mut writes)
|
|
966
|
+
.stage_delete_rows(&[crate::untracked_state::UntrackedStateIdentity {
|
|
967
|
+
version_id: "global".to_string(),
|
|
968
|
+
schema_key: "lix_key_value".to_string(),
|
|
969
|
+
entity_id: EntityIdentity::single("selected-tab"),
|
|
970
|
+
file_id: None,
|
|
971
|
+
}]);
|
|
972
|
+
writes
|
|
973
|
+
.apply(&mut transaction.as_mut())
|
|
974
|
+
.await
|
|
975
|
+
.expect("untracked row should delete");
|
|
976
|
+
}
|
|
977
|
+
transaction.commit().await.expect("commit should persist");
|
|
978
|
+
|
|
979
|
+
let loaded = load_selected_tab(&live_state, storage.clone())
|
|
980
|
+
.await
|
|
981
|
+
.expect("load should succeed")
|
|
982
|
+
.expect("tracked row should be visible again");
|
|
983
|
+
assert!(!loaded.untracked);
|
|
984
|
+
assert_eq!(loaded.change_id.as_deref(), Some("change-tracked"));
|
|
985
|
+
assert_eq!(
|
|
986
|
+
loaded.snapshot_content.as_deref(),
|
|
987
|
+
Some("{\"value\":\"tracked-value\"}")
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
#[tokio::test]
|
|
992
|
+
async fn load_row_falls_back_to_global_tracked_row_for_requested_version() {
|
|
993
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
994
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
995
|
+
let live_state = live_state_context();
|
|
996
|
+
|
|
997
|
+
let mut transaction = storage
|
|
998
|
+
.begin_write_transaction()
|
|
999
|
+
.await
|
|
1000
|
+
.expect("transaction should open");
|
|
1001
|
+
{
|
|
1002
|
+
let rows = [tracked_row_with_commit(
|
|
1003
|
+
"global-tracked",
|
|
1004
|
+
Some("change-global"),
|
|
1005
|
+
"commit-global",
|
|
1006
|
+
)];
|
|
1007
|
+
let mut writes = StorageWriteSet::new();
|
|
1008
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1009
|
+
{
|
|
1010
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1011
|
+
writer
|
|
1012
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1013
|
+
.await
|
|
1014
|
+
.expect("tracked row should stage");
|
|
1015
|
+
}
|
|
1016
|
+
writes
|
|
1017
|
+
.apply(&mut transaction.as_mut())
|
|
1018
|
+
.await
|
|
1019
|
+
.expect("tracked row should apply");
|
|
1020
|
+
}
|
|
1021
|
+
write_untracked_rows_to_store(
|
|
1022
|
+
transaction.as_mut(),
|
|
1023
|
+
&[
|
|
1024
|
+
version_ref_row("global", "commit-global"),
|
|
1025
|
+
version_ref_row("version-a", "commit-version-a"),
|
|
1026
|
+
],
|
|
1027
|
+
)
|
|
1028
|
+
.await;
|
|
1029
|
+
transaction.commit().await.expect("commit should persist");
|
|
1030
|
+
|
|
1031
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
|
|
1032
|
+
.await
|
|
1033
|
+
.expect("load should succeed")
|
|
1034
|
+
.expect("global row should be visible for requested version");
|
|
1035
|
+
|
|
1036
|
+
assert_eq!(loaded.version_id, "version-a");
|
|
1037
|
+
assert!(loaded.global);
|
|
1038
|
+
assert!(!loaded.untracked);
|
|
1039
|
+
assert_eq!(
|
|
1040
|
+
loaded.snapshot_content.as_deref(),
|
|
1041
|
+
Some("{\"value\":\"global-tracked\"}")
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#[tokio::test]
|
|
1046
|
+
async fn main_sees_global_row_by_reading_global_root_separately() {
|
|
1047
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1048
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1049
|
+
let tracked_state = TrackedStateContext::new();
|
|
1050
|
+
let live_state = LiveStateContext::new(
|
|
1051
|
+
tracked_state.clone(),
|
|
1052
|
+
UntrackedStateContext::new(),
|
|
1053
|
+
crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
let mut transaction = storage
|
|
1057
|
+
.begin_write_transaction()
|
|
1058
|
+
.await
|
|
1059
|
+
.expect("transaction should open");
|
|
1060
|
+
{
|
|
1061
|
+
let rows = [tracked_row_with_commit(
|
|
1062
|
+
"global-tracked",
|
|
1063
|
+
Some("change-global"),
|
|
1064
|
+
"commit-global",
|
|
1065
|
+
)];
|
|
1066
|
+
let mut writes = StorageWriteSet::new();
|
|
1067
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1068
|
+
{
|
|
1069
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1070
|
+
writer
|
|
1071
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1072
|
+
.await
|
|
1073
|
+
.expect("global tracked row should stage");
|
|
1074
|
+
}
|
|
1075
|
+
writes
|
|
1076
|
+
.apply(&mut transaction.as_mut())
|
|
1077
|
+
.await
|
|
1078
|
+
.expect("global tracked row should apply");
|
|
1079
|
+
}
|
|
1080
|
+
write_untracked_rows_to_store(
|
|
1081
|
+
transaction.as_mut(),
|
|
1082
|
+
&[
|
|
1083
|
+
version_ref_row("global", "commit-global"),
|
|
1084
|
+
version_ref_row("main", "commit-main"),
|
|
1085
|
+
],
|
|
1086
|
+
)
|
|
1087
|
+
.await;
|
|
1088
|
+
transaction.commit().await.expect("commit should persist");
|
|
1089
|
+
|
|
1090
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "main")
|
|
1091
|
+
.await
|
|
1092
|
+
.expect("load should succeed")
|
|
1093
|
+
.expect("global row should be projected into main");
|
|
1094
|
+
assert_eq!(loaded.version_id, "main");
|
|
1095
|
+
assert!(loaded.global);
|
|
1096
|
+
assert_eq!(
|
|
1097
|
+
loaded.snapshot_content.as_deref(),
|
|
1098
|
+
Some("{\"value\":\"global-tracked\"}")
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
let main_root_rows =
|
|
1102
|
+
scan_tracked_root(&tracked_state, storage.clone(), "commit-main").await;
|
|
1103
|
+
assert_eq!(
|
|
1104
|
+
main_root_rows.len(),
|
|
1105
|
+
0,
|
|
1106
|
+
"global fallback must come from the global root, not a copied main root row"
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
#[tokio::test]
|
|
1111
|
+
async fn load_row_prefers_requested_version_over_global() {
|
|
1112
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1113
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1114
|
+
let live_state = live_state_context();
|
|
1115
|
+
|
|
1116
|
+
let mut transaction = storage
|
|
1117
|
+
.begin_write_transaction()
|
|
1118
|
+
.await
|
|
1119
|
+
.expect("transaction should open");
|
|
1120
|
+
{
|
|
1121
|
+
let rows = [
|
|
1122
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1123
|
+
tracked_row_at_with_commit(
|
|
1124
|
+
"version-a",
|
|
1125
|
+
"version-tracked",
|
|
1126
|
+
Some("change-version"),
|
|
1127
|
+
"commit-version",
|
|
1128
|
+
),
|
|
1129
|
+
];
|
|
1130
|
+
let mut writes = StorageWriteSet::new();
|
|
1131
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1132
|
+
{
|
|
1133
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1134
|
+
writer
|
|
1135
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1136
|
+
.await
|
|
1137
|
+
.expect("tracked rows should stage");
|
|
1138
|
+
}
|
|
1139
|
+
writes
|
|
1140
|
+
.apply(&mut transaction.as_mut())
|
|
1141
|
+
.await
|
|
1142
|
+
.expect("tracked rows should apply");
|
|
1143
|
+
}
|
|
1144
|
+
write_untracked_rows_to_store(
|
|
1145
|
+
transaction.as_mut(),
|
|
1146
|
+
&[
|
|
1147
|
+
version_ref_row("global", "commit-global"),
|
|
1148
|
+
version_ref_row("version-a", "commit-version"),
|
|
1149
|
+
],
|
|
1150
|
+
)
|
|
1151
|
+
.await;
|
|
1152
|
+
transaction.commit().await.expect("commit should persist");
|
|
1153
|
+
|
|
1154
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
|
|
1155
|
+
.await
|
|
1156
|
+
.expect("load should succeed")
|
|
1157
|
+
.expect("version row should be visible");
|
|
1158
|
+
|
|
1159
|
+
assert_eq!(loaded.version_id, "version-a");
|
|
1160
|
+
assert!(!loaded.untracked);
|
|
1161
|
+
assert_eq!(
|
|
1162
|
+
loaded.snapshot_content.as_deref(),
|
|
1163
|
+
Some("{\"value\":\"version-tracked\"}")
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
#[tokio::test]
|
|
1168
|
+
async fn main_override_hides_global_row() {
|
|
1169
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1170
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1171
|
+
let live_state = live_state_context();
|
|
1172
|
+
|
|
1173
|
+
let mut transaction = storage
|
|
1174
|
+
.begin_write_transaction()
|
|
1175
|
+
.await
|
|
1176
|
+
.expect("transaction should open");
|
|
1177
|
+
{
|
|
1178
|
+
let rows = [
|
|
1179
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1180
|
+
tracked_row_at_with_commit(
|
|
1181
|
+
"main",
|
|
1182
|
+
"main-tracked",
|
|
1183
|
+
Some("change-main"),
|
|
1184
|
+
"commit-main",
|
|
1185
|
+
),
|
|
1186
|
+
];
|
|
1187
|
+
let mut writes = StorageWriteSet::new();
|
|
1188
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1189
|
+
{
|
|
1190
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1191
|
+
writer
|
|
1192
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1193
|
+
.await
|
|
1194
|
+
.expect("tracked rows should stage");
|
|
1195
|
+
}
|
|
1196
|
+
writes
|
|
1197
|
+
.apply(&mut transaction.as_mut())
|
|
1198
|
+
.await
|
|
1199
|
+
.expect("tracked rows should apply");
|
|
1200
|
+
}
|
|
1201
|
+
write_untracked_rows_to_store(
|
|
1202
|
+
transaction.as_mut(),
|
|
1203
|
+
&[
|
|
1204
|
+
version_ref_row("global", "commit-global"),
|
|
1205
|
+
version_ref_row("main", "commit-main"),
|
|
1206
|
+
],
|
|
1207
|
+
)
|
|
1208
|
+
.await;
|
|
1209
|
+
transaction.commit().await.expect("commit should persist");
|
|
1210
|
+
|
|
1211
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "main")
|
|
1212
|
+
.await
|
|
1213
|
+
.expect("load should succeed")
|
|
1214
|
+
.expect("main row should be visible");
|
|
1215
|
+
|
|
1216
|
+
assert_eq!(loaded.version_id, "main");
|
|
1217
|
+
assert!(!loaded.global);
|
|
1218
|
+
assert_eq!(
|
|
1219
|
+
loaded.snapshot_content.as_deref(),
|
|
1220
|
+
Some("{\"value\":\"main-tracked\"}")
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
#[tokio::test]
|
|
1225
|
+
async fn load_row_prefers_requested_untracked_over_requested_tracked_and_global_rows() {
|
|
1226
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1227
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1228
|
+
let live_state = live_state_context();
|
|
1229
|
+
|
|
1230
|
+
let mut transaction = storage
|
|
1231
|
+
.begin_write_transaction()
|
|
1232
|
+
.await
|
|
1233
|
+
.expect("transaction should open");
|
|
1234
|
+
{
|
|
1235
|
+
let rows = [
|
|
1236
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1237
|
+
tracked_row_at_with_commit(
|
|
1238
|
+
"version-a",
|
|
1239
|
+
"version-tracked",
|
|
1240
|
+
Some("change-version"),
|
|
1241
|
+
"commit-version",
|
|
1242
|
+
),
|
|
1243
|
+
];
|
|
1244
|
+
let mut writes = StorageWriteSet::new();
|
|
1245
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1246
|
+
{
|
|
1247
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1248
|
+
writer
|
|
1249
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1250
|
+
.await
|
|
1251
|
+
.expect("tracked rows should stage");
|
|
1252
|
+
}
|
|
1253
|
+
writes
|
|
1254
|
+
.apply(&mut transaction.as_mut())
|
|
1255
|
+
.await
|
|
1256
|
+
.expect("tracked rows should apply");
|
|
1257
|
+
}
|
|
1258
|
+
write_untracked_rows_to_store(
|
|
1259
|
+
transaction.as_mut(),
|
|
1260
|
+
&[
|
|
1261
|
+
version_ref_row("global", "commit-global"),
|
|
1262
|
+
version_ref_row("version-a", "commit-version"),
|
|
1263
|
+
untracked_row_at("global", "global-untracked"),
|
|
1264
|
+
untracked_row_at("version-a", "version-untracked"),
|
|
1265
|
+
],
|
|
1266
|
+
)
|
|
1267
|
+
.await;
|
|
1268
|
+
transaction.commit().await.expect("commit should persist");
|
|
1269
|
+
|
|
1270
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
|
|
1271
|
+
.await
|
|
1272
|
+
.expect("load should succeed")
|
|
1273
|
+
.expect("version untracked row should be visible");
|
|
1274
|
+
|
|
1275
|
+
assert_eq!(loaded.version_id, "version-a");
|
|
1276
|
+
assert!(loaded.untracked);
|
|
1277
|
+
assert_eq!(
|
|
1278
|
+
loaded.snapshot_content.as_deref(),
|
|
1279
|
+
Some("{\"value\":\"version-untracked\"}")
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
#[tokio::test]
|
|
1284
|
+
async fn scan_rows_overlays_requested_version_over_global() {
|
|
1285
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1286
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1287
|
+
let live_state = live_state_context();
|
|
1288
|
+
|
|
1289
|
+
let mut transaction = storage
|
|
1290
|
+
.begin_write_transaction()
|
|
1291
|
+
.await
|
|
1292
|
+
.expect("transaction should open");
|
|
1293
|
+
{
|
|
1294
|
+
let rows = [
|
|
1295
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1296
|
+
tracked_row_at_with_commit(
|
|
1297
|
+
"version-a",
|
|
1298
|
+
"version-tracked",
|
|
1299
|
+
Some("change-version"),
|
|
1300
|
+
"commit-version",
|
|
1301
|
+
),
|
|
1302
|
+
];
|
|
1303
|
+
let mut writes = StorageWriteSet::new();
|
|
1304
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1305
|
+
{
|
|
1306
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1307
|
+
writer
|
|
1308
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1309
|
+
.await
|
|
1310
|
+
.expect("rows should stage");
|
|
1311
|
+
}
|
|
1312
|
+
writes
|
|
1313
|
+
.apply(&mut transaction.as_mut())
|
|
1314
|
+
.await
|
|
1315
|
+
.expect("rows should apply");
|
|
1316
|
+
}
|
|
1317
|
+
write_untracked_rows_to_store(
|
|
1318
|
+
transaction.as_mut(),
|
|
1319
|
+
&[
|
|
1320
|
+
version_ref_row("global", "commit-global"),
|
|
1321
|
+
version_ref_row("version-a", "commit-version"),
|
|
1322
|
+
],
|
|
1323
|
+
)
|
|
1324
|
+
.await;
|
|
1325
|
+
transaction.commit().await.expect("commit should persist");
|
|
1326
|
+
|
|
1327
|
+
let rows = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
|
|
1328
|
+
.await
|
|
1329
|
+
.expect("scan should succeed");
|
|
1330
|
+
|
|
1331
|
+
assert_eq!(rows.len(), 1);
|
|
1332
|
+
assert_eq!(rows[0].version_id, "version-a");
|
|
1333
|
+
assert_eq!(
|
|
1334
|
+
rows[0].snapshot_content.as_deref(),
|
|
1335
|
+
Some("{\"value\":\"version-tracked\"}")
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
#[tokio::test]
|
|
1340
|
+
async fn scan_rows_projects_global_row_into_requested_version() {
|
|
1341
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1342
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1343
|
+
let live_state = live_state_context();
|
|
1344
|
+
|
|
1345
|
+
let mut transaction = storage
|
|
1346
|
+
.begin_write_transaction()
|
|
1347
|
+
.await
|
|
1348
|
+
.expect("transaction should open");
|
|
1349
|
+
{
|
|
1350
|
+
let rows = [tracked_row_with_commit(
|
|
1351
|
+
"global-tracked",
|
|
1352
|
+
Some("change-global"),
|
|
1353
|
+
"commit-global",
|
|
1354
|
+
)];
|
|
1355
|
+
let mut writes = StorageWriteSet::new();
|
|
1356
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1357
|
+
{
|
|
1358
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1359
|
+
writer
|
|
1360
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1361
|
+
.await
|
|
1362
|
+
.expect("rows should stage");
|
|
1363
|
+
}
|
|
1364
|
+
writes
|
|
1365
|
+
.apply(&mut transaction.as_mut())
|
|
1366
|
+
.await
|
|
1367
|
+
.expect("rows should apply");
|
|
1368
|
+
}
|
|
1369
|
+
write_untracked_rows_to_store(
|
|
1370
|
+
transaction.as_mut(),
|
|
1371
|
+
&[
|
|
1372
|
+
version_ref_row("global", "commit-global"),
|
|
1373
|
+
version_ref_row("version-a", "commit-version-a"),
|
|
1374
|
+
],
|
|
1375
|
+
)
|
|
1376
|
+
.await;
|
|
1377
|
+
transaction.commit().await.expect("commit should persist");
|
|
1378
|
+
|
|
1379
|
+
let rows = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
|
|
1380
|
+
.await
|
|
1381
|
+
.expect("scan should succeed");
|
|
1382
|
+
|
|
1383
|
+
assert_eq!(rows.len(), 1);
|
|
1384
|
+
assert_eq!(rows[0].version_id, "version-a");
|
|
1385
|
+
assert!(rows[0].global);
|
|
1386
|
+
assert_eq!(
|
|
1387
|
+
rows[0].snapshot_content.as_deref(),
|
|
1388
|
+
Some("{\"value\":\"global-tracked\"}")
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
#[tokio::test]
|
|
1393
|
+
async fn scan_rows_does_not_project_global_rows_into_missing_version() {
|
|
1394
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1395
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1396
|
+
let live_state = live_state_context();
|
|
1397
|
+
|
|
1398
|
+
let mut transaction = storage
|
|
1399
|
+
.begin_write_transaction()
|
|
1400
|
+
.await
|
|
1401
|
+
.expect("transaction should open");
|
|
1402
|
+
{
|
|
1403
|
+
let rows = [tracked_row_with_commit(
|
|
1404
|
+
"global-tracked",
|
|
1405
|
+
Some("change-global"),
|
|
1406
|
+
"commit-global",
|
|
1407
|
+
)];
|
|
1408
|
+
let mut writes = StorageWriteSet::new();
|
|
1409
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1410
|
+
{
|
|
1411
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1412
|
+
writer
|
|
1413
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1414
|
+
.await
|
|
1415
|
+
.expect("tracked row should stage");
|
|
1416
|
+
}
|
|
1417
|
+
writes
|
|
1418
|
+
.apply(&mut transaction.as_mut())
|
|
1419
|
+
.await
|
|
1420
|
+
.expect("tracked row should apply");
|
|
1421
|
+
}
|
|
1422
|
+
write_untracked_rows_to_store(
|
|
1423
|
+
transaction.as_mut(),
|
|
1424
|
+
&[version_ref_row("global", "commit-global")],
|
|
1425
|
+
)
|
|
1426
|
+
.await;
|
|
1427
|
+
transaction.commit().await.expect("commit should persist");
|
|
1428
|
+
|
|
1429
|
+
let rows = scan_selected_tab_at(&live_state, storage.clone(), "missing-version", false)
|
|
1430
|
+
.await
|
|
1431
|
+
.expect("scan should succeed");
|
|
1432
|
+
|
|
1433
|
+
assert_eq!(
|
|
1434
|
+
rows.len(),
|
|
1435
|
+
0,
|
|
1436
|
+
"global rows must not be projected into a missing version scope"
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
#[tokio::test]
|
|
1441
|
+
async fn winning_tombstone_hides_row_unless_tombstones_are_included() {
|
|
1442
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1443
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1444
|
+
let live_state = live_state_context();
|
|
1445
|
+
|
|
1446
|
+
let mut transaction = storage
|
|
1447
|
+
.begin_write_transaction()
|
|
1448
|
+
.await
|
|
1449
|
+
.expect("transaction should open");
|
|
1450
|
+
{
|
|
1451
|
+
let rows = [
|
|
1452
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1453
|
+
tombstone_tracked_row_at_with_commit(
|
|
1454
|
+
"version-a",
|
|
1455
|
+
Some("change-tombstone"),
|
|
1456
|
+
"commit-version",
|
|
1457
|
+
),
|
|
1458
|
+
];
|
|
1459
|
+
let mut writes = StorageWriteSet::new();
|
|
1460
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1461
|
+
{
|
|
1462
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1463
|
+
writer
|
|
1464
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1465
|
+
.await
|
|
1466
|
+
.expect("rows should stage");
|
|
1467
|
+
}
|
|
1468
|
+
writes
|
|
1469
|
+
.apply(&mut transaction.as_mut())
|
|
1470
|
+
.await
|
|
1471
|
+
.expect("rows should apply");
|
|
1472
|
+
}
|
|
1473
|
+
write_untracked_rows_to_store(
|
|
1474
|
+
transaction.as_mut(),
|
|
1475
|
+
&[
|
|
1476
|
+
version_ref_row("global", "commit-global"),
|
|
1477
|
+
version_ref_row("version-a", "commit-version"),
|
|
1478
|
+
],
|
|
1479
|
+
)
|
|
1480
|
+
.await;
|
|
1481
|
+
transaction.commit().await.expect("commit should persist");
|
|
1482
|
+
|
|
1483
|
+
let hidden = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
|
|
1484
|
+
.await
|
|
1485
|
+
.expect("scan should succeed");
|
|
1486
|
+
assert_eq!(hidden.len(), 0);
|
|
1487
|
+
|
|
1488
|
+
let with_tombstone = scan_selected_tab_at(&live_state, storage.clone(), "version-a", true)
|
|
1489
|
+
.await
|
|
1490
|
+
.expect("scan should succeed");
|
|
1491
|
+
assert_eq!(with_tombstone.len(), 1);
|
|
1492
|
+
assert_eq!(with_tombstone[0].version_id, "version-a");
|
|
1493
|
+
assert_eq!(with_tombstone[0].snapshot_content, None);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
#[tokio::test]
|
|
1497
|
+
async fn main_tombstone_hides_global_row() {
|
|
1498
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1499
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1500
|
+
let live_state = live_state_context();
|
|
1501
|
+
|
|
1502
|
+
let mut transaction = storage
|
|
1503
|
+
.begin_write_transaction()
|
|
1504
|
+
.await
|
|
1505
|
+
.expect("transaction should open");
|
|
1506
|
+
{
|
|
1507
|
+
let rows = [
|
|
1508
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1509
|
+
tombstone_tracked_row_at_with_commit(
|
|
1510
|
+
"main",
|
|
1511
|
+
Some("change-main-tombstone"),
|
|
1512
|
+
"commit-main",
|
|
1513
|
+
),
|
|
1514
|
+
];
|
|
1515
|
+
let mut writes = StorageWriteSet::new();
|
|
1516
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1517
|
+
{
|
|
1518
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1519
|
+
writer
|
|
1520
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1521
|
+
.await
|
|
1522
|
+
.expect("tracked rows should stage");
|
|
1523
|
+
}
|
|
1524
|
+
writes
|
|
1525
|
+
.apply(&mut transaction.as_mut())
|
|
1526
|
+
.await
|
|
1527
|
+
.expect("tracked rows should apply");
|
|
1528
|
+
}
|
|
1529
|
+
write_untracked_rows_to_store(
|
|
1530
|
+
transaction.as_mut(),
|
|
1531
|
+
&[
|
|
1532
|
+
version_ref_row("global", "commit-global"),
|
|
1533
|
+
version_ref_row("main", "commit-main"),
|
|
1534
|
+
],
|
|
1535
|
+
)
|
|
1536
|
+
.await;
|
|
1537
|
+
transaction.commit().await.expect("commit should persist");
|
|
1538
|
+
|
|
1539
|
+
let hidden = scan_selected_tab_at(&live_state, storage.clone(), "main", false)
|
|
1540
|
+
.await
|
|
1541
|
+
.expect("scan should succeed");
|
|
1542
|
+
assert_eq!(hidden.len(), 0);
|
|
1543
|
+
|
|
1544
|
+
let tombstones = scan_selected_tab_at(&live_state, storage.clone(), "main", true)
|
|
1545
|
+
.await
|
|
1546
|
+
.expect("scan should succeed");
|
|
1547
|
+
assert_eq!(tombstones.len(), 1);
|
|
1548
|
+
assert_eq!(tombstones[0].version_id, "main");
|
|
1549
|
+
assert!(!tombstones[0].global);
|
|
1550
|
+
assert_eq!(tombstones[0].snapshot_content, None);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
#[tokio::test]
|
|
1554
|
+
async fn scan_rows_projects_commit_graph_facts_as_global_rows() {
|
|
1555
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1556
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1557
|
+
let live_state = live_state_context();
|
|
1558
|
+
append_commit_change(storage.clone(), "commit-a").await;
|
|
1559
|
+
write_version_refs(storage.clone(), &[version_ref_row("version-a", "commit-a")]).await;
|
|
1560
|
+
|
|
1561
|
+
let rows = live_state
|
|
1562
|
+
.reader(storage.clone())
|
|
1563
|
+
.scan_rows(&LiveStateScanRequest {
|
|
1564
|
+
filter: LiveStateFilter {
|
|
1565
|
+
schema_keys: vec![COMMIT_SCHEMA_KEY.to_string()],
|
|
1566
|
+
version_ids: vec!["version-a".to_string()],
|
|
1567
|
+
..LiveStateFilter::default()
|
|
1568
|
+
},
|
|
1569
|
+
..LiveStateScanRequest::default()
|
|
1570
|
+
})
|
|
1571
|
+
.await
|
|
1572
|
+
.expect("commit rows should scan");
|
|
1573
|
+
|
|
1574
|
+
assert_eq!(rows.len(), 1);
|
|
1575
|
+
assert_eq!(rows[0].entity_id.as_string().as_deref(), Ok("commit-a"));
|
|
1576
|
+
assert_eq!(rows[0].schema_key, COMMIT_SCHEMA_KEY);
|
|
1577
|
+
assert_eq!(rows[0].version_id, "version-a");
|
|
1578
|
+
assert!(rows[0].global);
|
|
1579
|
+
assert!(!rows[0].untracked);
|
|
1580
|
+
assert_eq!(rows[0].change_id.as_deref(), Some("change-commit-a"));
|
|
1581
|
+
assert_eq!(rows[0].commit_id.as_deref(), Some("commit-a"));
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
#[tokio::test]
|
|
1585
|
+
async fn load_row_reads_commit_graph_fact() {
|
|
1586
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1587
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1588
|
+
let live_state = live_state_context();
|
|
1589
|
+
append_commit_change(storage.clone(), "commit-a").await;
|
|
1590
|
+
write_version_refs(storage.clone(), &[version_ref_row("version-a", "commit-a")]).await;
|
|
1591
|
+
|
|
1592
|
+
let row = live_state
|
|
1593
|
+
.reader(storage.clone())
|
|
1594
|
+
.load_row(&LiveStateRowRequest {
|
|
1595
|
+
schema_key: COMMIT_SCHEMA_KEY.to_string(),
|
|
1596
|
+
version_id: "version-a".to_string(),
|
|
1597
|
+
entity_id: crate::entity_identity::EntityIdentity::single("commit-a"),
|
|
1598
|
+
file_id: NullableKeyFilter::Null,
|
|
1599
|
+
})
|
|
1600
|
+
.await
|
|
1601
|
+
.expect("commit row should load")
|
|
1602
|
+
.expect("commit row should exist");
|
|
1603
|
+
|
|
1604
|
+
assert_eq!(row.entity_id.as_string().as_deref(), Ok("commit-a"));
|
|
1605
|
+
assert_eq!(row.version_id, "version-a");
|
|
1606
|
+
assert!(row.global);
|
|
1607
|
+
assert_eq!(row.change_id.as_deref(), Some("change-commit-a"));
|
|
1608
|
+
assert_eq!(row.commit_id.as_deref(), Some("commit-a"));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
#[tokio::test]
|
|
1612
|
+
async fn load_commit_row_does_not_project_into_missing_version() {
|
|
1613
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1614
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1615
|
+
let live_state = live_state_context();
|
|
1616
|
+
append_commit_change(storage.clone(), "commit-a").await;
|
|
1617
|
+
|
|
1618
|
+
let row = live_state
|
|
1619
|
+
.reader(storage.clone())
|
|
1620
|
+
.load_row(&LiveStateRowRequest {
|
|
1621
|
+
schema_key: COMMIT_SCHEMA_KEY.to_string(),
|
|
1622
|
+
version_id: "missing-version".to_string(),
|
|
1623
|
+
entity_id: crate::entity_identity::EntityIdentity::single("commit-a"),
|
|
1624
|
+
file_id: NullableKeyFilter::Null,
|
|
1625
|
+
})
|
|
1626
|
+
.await
|
|
1627
|
+
.expect("commit row load should succeed");
|
|
1628
|
+
|
|
1629
|
+
assert_eq!(
|
|
1630
|
+
row, None,
|
|
1631
|
+
"commit rows must not be projected into a missing version scope"
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
#[tokio::test]
|
|
1636
|
+
async fn writer_rejects_tracked_root_batches_that_mix_global_and_version_rows() {
|
|
1637
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1638
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1639
|
+
let live_state = live_state_context();
|
|
1640
|
+
let mut transaction = storage
|
|
1641
|
+
.begin_write_transaction()
|
|
1642
|
+
.await
|
|
1643
|
+
.expect("transaction should open");
|
|
1644
|
+
|
|
1645
|
+
let error = {
|
|
1646
|
+
let rows = [
|
|
1647
|
+
tracked_row_at_with_commit(
|
|
1648
|
+
"global",
|
|
1649
|
+
"global-row",
|
|
1650
|
+
Some("change-global"),
|
|
1651
|
+
"commit-shared",
|
|
1652
|
+
),
|
|
1653
|
+
tracked_row_at_with_commit(
|
|
1654
|
+
"version-a",
|
|
1655
|
+
"version-row",
|
|
1656
|
+
Some("change-version"),
|
|
1657
|
+
"commit-shared",
|
|
1658
|
+
),
|
|
1659
|
+
];
|
|
1660
|
+
let mut writes = StorageWriteSet::new();
|
|
1661
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1662
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1663
|
+
writer
|
|
1664
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1665
|
+
.await
|
|
1666
|
+
}
|
|
1667
|
+
.expect_err("one tracked root must not mix global and version rows");
|
|
1668
|
+
|
|
1669
|
+
assert!(
|
|
1670
|
+
error.message.contains("mixes multiple storage scopes"),
|
|
1671
|
+
"unexpected error: {error:?}"
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
#[tokio::test]
|
|
1676
|
+
async fn writer_rejects_tracked_rows_with_invalid_storage_scope() {
|
|
1677
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1678
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1679
|
+
let live_state = live_state_context();
|
|
1680
|
+
let mut invalid_row =
|
|
1681
|
+
tracked_row_at_with_commit("version-a", "bad-row", Some("change-bad"), "commit-bad");
|
|
1682
|
+
invalid_row.global = true;
|
|
1683
|
+
let mut transaction = storage
|
|
1684
|
+
.begin_write_transaction()
|
|
1685
|
+
.await
|
|
1686
|
+
.expect("transaction should open");
|
|
1687
|
+
|
|
1688
|
+
let error = {
|
|
1689
|
+
let rows = [invalid_row];
|
|
1690
|
+
let mut writes = StorageWriteSet::new();
|
|
1691
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1692
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1693
|
+
writer
|
|
1694
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1695
|
+
.await
|
|
1696
|
+
}
|
|
1697
|
+
.expect_err("global rows must be stored in the global root only");
|
|
1698
|
+
|
|
1699
|
+
assert!(
|
|
1700
|
+
error.message.contains("invalid storage scope"),
|
|
1701
|
+
"unexpected error: {error:?}"
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
#[tokio::test]
|
|
1706
|
+
async fn writer_allows_commit_fact_to_share_the_touched_version_commit_id() {
|
|
1707
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1708
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1709
|
+
let live_state = live_state_context();
|
|
1710
|
+
let mut transaction = storage
|
|
1711
|
+
.begin_write_transaction()
|
|
1712
|
+
.await
|
|
1713
|
+
.expect("transaction should open");
|
|
1714
|
+
|
|
1715
|
+
{
|
|
1716
|
+
let rows = [
|
|
1717
|
+
tracked_row_at_with_commit(
|
|
1718
|
+
"version-a",
|
|
1719
|
+
"version-row",
|
|
1720
|
+
Some("change-version"),
|
|
1721
|
+
"commit-version",
|
|
1722
|
+
),
|
|
1723
|
+
commit_live_state_row("commit-version"),
|
|
1724
|
+
];
|
|
1725
|
+
let mut writes = StorageWriteSet::new();
|
|
1726
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1727
|
+
{
|
|
1728
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1729
|
+
writer
|
|
1730
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1731
|
+
.await
|
|
1732
|
+
.expect("commit facts are changelog projections, not root-local rows");
|
|
1733
|
+
}
|
|
1734
|
+
writes
|
|
1735
|
+
.apply(&mut transaction.as_mut())
|
|
1736
|
+
.await
|
|
1737
|
+
.expect("commit fact rows should apply");
|
|
1738
|
+
}
|
|
1739
|
+
write_untracked_rows_to_store(
|
|
1740
|
+
transaction.as_mut(),
|
|
1741
|
+
&[version_ref_row("version-a", "commit-version")],
|
|
1742
|
+
)
|
|
1743
|
+
.await;
|
|
1744
|
+
transaction.commit().await.expect("commit should persist");
|
|
1745
|
+
|
|
1746
|
+
let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
|
|
1747
|
+
.await
|
|
1748
|
+
.expect("load should succeed")
|
|
1749
|
+
.expect("version row should be visible");
|
|
1750
|
+
assert_eq!(
|
|
1751
|
+
loaded.snapshot_content.as_deref(),
|
|
1752
|
+
Some("{\"value\":\"version-row\"}")
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
#[tokio::test]
|
|
1757
|
+
async fn writer_uses_first_parent_as_merge_root_base() {
|
|
1758
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1759
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1760
|
+
let live_state = live_state_context();
|
|
1761
|
+
let mut seed_transaction = storage
|
|
1762
|
+
.begin_write_transaction()
|
|
1763
|
+
.await
|
|
1764
|
+
.expect("seed transaction should open");
|
|
1765
|
+
let mut writes = StorageWriteSet::new();
|
|
1766
|
+
{
|
|
1767
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1768
|
+
TrackedStateContext::new()
|
|
1769
|
+
.writer()
|
|
1770
|
+
.stage_root(
|
|
1771
|
+
&mut seed_transaction.as_mut(),
|
|
1772
|
+
&mut writes,
|
|
1773
|
+
&mut json_writer,
|
|
1774
|
+
"parent-left",
|
|
1775
|
+
None,
|
|
1776
|
+
&[],
|
|
1777
|
+
)
|
|
1778
|
+
.await
|
|
1779
|
+
.expect("first parent root should exist");
|
|
1780
|
+
}
|
|
1781
|
+
writes
|
|
1782
|
+
.apply(&mut seed_transaction.as_mut())
|
|
1783
|
+
.await
|
|
1784
|
+
.expect("first parent root should apply");
|
|
1785
|
+
seed_transaction
|
|
1786
|
+
.commit()
|
|
1787
|
+
.await
|
|
1788
|
+
.expect("seed transaction should commit");
|
|
1789
|
+
|
|
1790
|
+
let mut transaction = storage
|
|
1791
|
+
.begin_write_transaction()
|
|
1792
|
+
.await
|
|
1793
|
+
.expect("transaction should open");
|
|
1794
|
+
|
|
1795
|
+
{
|
|
1796
|
+
let rows = [
|
|
1797
|
+
tracked_row_at_with_commit(
|
|
1798
|
+
"version-a",
|
|
1799
|
+
"version-row",
|
|
1800
|
+
Some("change-version"),
|
|
1801
|
+
"commit-merge",
|
|
1802
|
+
),
|
|
1803
|
+
commit_live_state_row_with_parents(
|
|
1804
|
+
"commit-merge",
|
|
1805
|
+
&["parent-left", "parent-right"],
|
|
1806
|
+
),
|
|
1807
|
+
];
|
|
1808
|
+
let mut writes = StorageWriteSet::new();
|
|
1809
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1810
|
+
{
|
|
1811
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1812
|
+
writer
|
|
1813
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1814
|
+
.await
|
|
1815
|
+
.expect("merge commit should use first parent as tracked-root base");
|
|
1816
|
+
}
|
|
1817
|
+
writes
|
|
1818
|
+
.apply(&mut transaction.as_mut())
|
|
1819
|
+
.await
|
|
1820
|
+
.expect("merge commit rows should apply");
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
#[tokio::test]
|
|
1825
|
+
async fn writer_rejects_commit_root_with_missing_parent_commit_ids() {
|
|
1826
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1827
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1828
|
+
let live_state = live_state_context();
|
|
1829
|
+
let mut transaction = storage
|
|
1830
|
+
.begin_write_transaction()
|
|
1831
|
+
.await
|
|
1832
|
+
.expect("transaction should open");
|
|
1833
|
+
|
|
1834
|
+
let error = {
|
|
1835
|
+
let rows = [
|
|
1836
|
+
tracked_row_at_with_commit(
|
|
1837
|
+
"version-a",
|
|
1838
|
+
"version-row",
|
|
1839
|
+
Some("change-version"),
|
|
1840
|
+
"commit-malformed",
|
|
1841
|
+
),
|
|
1842
|
+
commit_live_state_row_with_snapshot(
|
|
1843
|
+
"commit-malformed",
|
|
1844
|
+
json!({
|
|
1845
|
+
"id": "commit-malformed",
|
|
1846
|
+
"change_set_id": "change-set-commit-malformed",
|
|
1847
|
+
"change_ids": ["change-version"],
|
|
1848
|
+
}),
|
|
1849
|
+
),
|
|
1850
|
+
];
|
|
1851
|
+
let mut writes = StorageWriteSet::new();
|
|
1852
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1853
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1854
|
+
writer
|
|
1855
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1856
|
+
.await
|
|
1857
|
+
}
|
|
1858
|
+
.expect_err("commit roots must declare parent_commit_ids");
|
|
1859
|
+
|
|
1860
|
+
assert!(
|
|
1861
|
+
error.message.contains("missing parent_commit_ids"),
|
|
1862
|
+
"unexpected error: {error:?}"
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
#[tokio::test]
|
|
1867
|
+
async fn writer_rejects_commit_root_with_non_array_parent_commit_ids() {
|
|
1868
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1869
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1870
|
+
let live_state = live_state_context();
|
|
1871
|
+
let mut transaction = storage
|
|
1872
|
+
.begin_write_transaction()
|
|
1873
|
+
.await
|
|
1874
|
+
.expect("transaction should open");
|
|
1875
|
+
|
|
1876
|
+
let error = {
|
|
1877
|
+
let rows = [
|
|
1878
|
+
tracked_row_at_with_commit(
|
|
1879
|
+
"version-a",
|
|
1880
|
+
"version-row",
|
|
1881
|
+
Some("change-version"),
|
|
1882
|
+
"commit-malformed",
|
|
1883
|
+
),
|
|
1884
|
+
commit_live_state_row_with_snapshot(
|
|
1885
|
+
"commit-malformed",
|
|
1886
|
+
json!({
|
|
1887
|
+
"id": "commit-malformed",
|
|
1888
|
+
"change_set_id": "change-set-commit-malformed",
|
|
1889
|
+
"change_ids": ["change-version"],
|
|
1890
|
+
"parent_commit_ids": "parent-1",
|
|
1891
|
+
}),
|
|
1892
|
+
),
|
|
1893
|
+
];
|
|
1894
|
+
let mut writes = StorageWriteSet::new();
|
|
1895
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1896
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1897
|
+
writer
|
|
1898
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1899
|
+
.await
|
|
1900
|
+
}
|
|
1901
|
+
.expect_err("commit root parent_commit_ids must be an array");
|
|
1902
|
+
|
|
1903
|
+
assert!(
|
|
1904
|
+
error.message.contains("parent_commit_ids must be an array"),
|
|
1905
|
+
"unexpected error: {error:?}"
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
#[tokio::test]
|
|
1910
|
+
async fn non_global_root_does_not_store_global_rows() {
|
|
1911
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1912
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1913
|
+
let tracked_state = TrackedStateContext::new();
|
|
1914
|
+
let live_state = LiveStateContext::new(
|
|
1915
|
+
tracked_state.clone(),
|
|
1916
|
+
UntrackedStateContext::new(),
|
|
1917
|
+
crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
|
|
1918
|
+
);
|
|
1919
|
+
let mut transaction = storage
|
|
1920
|
+
.begin_write_transaction()
|
|
1921
|
+
.await
|
|
1922
|
+
.expect("transaction should open");
|
|
1923
|
+
|
|
1924
|
+
{
|
|
1925
|
+
let rows = [
|
|
1926
|
+
tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
|
|
1927
|
+
tracked_row_at_with_commit(
|
|
1928
|
+
"main",
|
|
1929
|
+
"main-tracked",
|
|
1930
|
+
Some("change-main"),
|
|
1931
|
+
"commit-main",
|
|
1932
|
+
),
|
|
1933
|
+
];
|
|
1934
|
+
let mut writes = StorageWriteSet::new();
|
|
1935
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
1936
|
+
{
|
|
1937
|
+
let mut writer = live_state.writer(transaction.as_mut());
|
|
1938
|
+
writer
|
|
1939
|
+
.stage_rows(&mut writes, &mut json_writer, &rows)
|
|
1940
|
+
.await
|
|
1941
|
+
.expect("tracked rows should stage");
|
|
1942
|
+
}
|
|
1943
|
+
writes
|
|
1944
|
+
.apply(&mut transaction.as_mut())
|
|
1945
|
+
.await
|
|
1946
|
+
.expect("tracked rows should apply");
|
|
1947
|
+
}
|
|
1948
|
+
transaction.commit().await.expect("commit should persist");
|
|
1949
|
+
|
|
1950
|
+
let global_root_rows =
|
|
1951
|
+
scan_tracked_root(&tracked_state, storage.clone(), "commit-global").await;
|
|
1952
|
+
assert_eq!(global_root_rows.len(), 1);
|
|
1953
|
+
assert_eq!(
|
|
1954
|
+
global_root_rows[0].snapshot_content.as_deref(),
|
|
1955
|
+
Some("{\"value\":\"global-tracked\"}")
|
|
1956
|
+
);
|
|
1957
|
+
|
|
1958
|
+
let main_root_rows =
|
|
1959
|
+
scan_tracked_root(&tracked_state, storage.clone(), "commit-main").await;
|
|
1960
|
+
assert_eq!(main_root_rows.len(), 1);
|
|
1961
|
+
assert_eq!(
|
|
1962
|
+
main_root_rows[0].snapshot_content.as_deref(),
|
|
1963
|
+
Some("{\"value\":\"main-tracked\"}")
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
async fn load_selected_tab(
|
|
1968
|
+
live_state: &LiveStateContext,
|
|
1969
|
+
storage: StorageContext,
|
|
1970
|
+
) -> Result<Option<LiveStateRow>, LixError> {
|
|
1971
|
+
live_state
|
|
1972
|
+
.reader(storage)
|
|
1973
|
+
.load_row(&LiveStateRowRequest {
|
|
1974
|
+
schema_key: "lix_key_value".to_string(),
|
|
1975
|
+
version_id: "global".to_string(),
|
|
1976
|
+
entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
|
|
1977
|
+
file_id: NullableKeyFilter::Null,
|
|
1978
|
+
})
|
|
1979
|
+
.await
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
async fn load_selected_tab_at(
|
|
1983
|
+
live_state: &LiveStateContext,
|
|
1984
|
+
storage: StorageContext,
|
|
1985
|
+
version_id: &str,
|
|
1986
|
+
) -> Result<Option<LiveStateRow>, LixError> {
|
|
1987
|
+
live_state
|
|
1988
|
+
.reader(storage)
|
|
1989
|
+
.load_row(&LiveStateRowRequest {
|
|
1990
|
+
schema_key: "lix_key_value".to_string(),
|
|
1991
|
+
version_id: version_id.to_string(),
|
|
1992
|
+
entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
|
|
1993
|
+
file_id: NullableKeyFilter::Null,
|
|
1994
|
+
})
|
|
1995
|
+
.await
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
async fn scan_selected_tab_at(
|
|
1999
|
+
live_state: &LiveStateContext,
|
|
2000
|
+
storage: StorageContext,
|
|
2001
|
+
version_id: &str,
|
|
2002
|
+
include_tombstones: bool,
|
|
2003
|
+
) -> Result<Vec<LiveStateRow>, LixError> {
|
|
2004
|
+
live_state
|
|
2005
|
+
.reader(storage)
|
|
2006
|
+
.scan_rows(&LiveStateScanRequest {
|
|
2007
|
+
filter: LiveStateFilter {
|
|
2008
|
+
schema_keys: vec!["lix_key_value".to_string()],
|
|
2009
|
+
entity_ids: vec![crate::entity_identity::EntityIdentity::single(
|
|
2010
|
+
"selected-tab",
|
|
2011
|
+
)],
|
|
2012
|
+
version_ids: vec![version_id.to_string()],
|
|
2013
|
+
file_ids: vec![NullableKeyFilter::Null],
|
|
2014
|
+
include_tombstones,
|
|
2015
|
+
..LiveStateFilter::default()
|
|
2016
|
+
},
|
|
2017
|
+
..LiveStateScanRequest::default()
|
|
2018
|
+
})
|
|
2019
|
+
.await
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
async fn scan_tracked_root(
|
|
2023
|
+
tracked_state: &TrackedStateContext,
|
|
2024
|
+
storage: StorageContext,
|
|
2025
|
+
commit_id: &str,
|
|
2026
|
+
) -> Vec<TrackedStateRow> {
|
|
2027
|
+
tracked_state
|
|
2028
|
+
.reader(storage)
|
|
2029
|
+
.scan_rows_at_commit(
|
|
2030
|
+
commit_id,
|
|
2031
|
+
&TrackedStateScanRequest {
|
|
2032
|
+
filter: TrackedStateFilter {
|
|
2033
|
+
include_tombstones: true,
|
|
2034
|
+
..Default::default()
|
|
2035
|
+
},
|
|
2036
|
+
..Default::default()
|
|
2037
|
+
},
|
|
2038
|
+
)
|
|
2039
|
+
.await
|
|
2040
|
+
.expect("tracked root should scan")
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
fn tracked_row_with_commit(
|
|
2044
|
+
value: &str,
|
|
2045
|
+
change_id: Option<&str>,
|
|
2046
|
+
commit_id: &str,
|
|
2047
|
+
) -> LiveStateRow {
|
|
2048
|
+
tracked_row_at_with_commit("global", value, change_id, commit_id)
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
fn tracked_row_at_with_commit(
|
|
2052
|
+
version_id: &str,
|
|
2053
|
+
value: &str,
|
|
2054
|
+
change_id: Option<&str>,
|
|
2055
|
+
commit_id: &str,
|
|
2056
|
+
) -> LiveStateRow {
|
|
2057
|
+
LiveStateRow {
|
|
2058
|
+
entity_id: identity("selected-tab"),
|
|
2059
|
+
schema_key: "lix_key_value".to_string(),
|
|
2060
|
+
file_id: None,
|
|
2061
|
+
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
2062
|
+
metadata: None,
|
|
2063
|
+
schema_version: "1".to_string(),
|
|
2064
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2065
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2066
|
+
global: version_id == "global",
|
|
2067
|
+
change_id: change_id.map(str::to_string),
|
|
2068
|
+
commit_id: Some(commit_id.to_string()),
|
|
2069
|
+
untracked: false,
|
|
2070
|
+
version_id: version_id.to_string(),
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
fn tombstone_tracked_row_at_with_commit(
|
|
2075
|
+
version_id: &str,
|
|
2076
|
+
change_id: Option<&str>,
|
|
2077
|
+
commit_id: &str,
|
|
2078
|
+
) -> LiveStateRow {
|
|
2079
|
+
LiveStateRow {
|
|
2080
|
+
snapshot_content: None,
|
|
2081
|
+
..tracked_row_at_with_commit(version_id, "ignored", change_id, commit_id)
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
fn untracked_row(value: &str) -> MaterializedUntrackedStateRow {
|
|
2086
|
+
untracked_row_at("global", value)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
fn untracked_row_at(version_id: &str, value: &str) -> MaterializedUntrackedStateRow {
|
|
2090
|
+
MaterializedUntrackedStateRow {
|
|
2091
|
+
entity_id: identity("selected-tab"),
|
|
2092
|
+
schema_key: "lix_key_value".to_string(),
|
|
2093
|
+
file_id: None,
|
|
2094
|
+
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
2095
|
+
metadata: None,
|
|
2096
|
+
schema_version: "1".to_string(),
|
|
2097
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2098
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2099
|
+
global: version_id == "global",
|
|
2100
|
+
version_id: version_id.to_string(),
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
fn version_ref_row(version_id: &str, commit_id: &str) -> MaterializedUntrackedStateRow {
|
|
2105
|
+
MaterializedUntrackedStateRow {
|
|
2106
|
+
entity_id: identity(version_id),
|
|
2107
|
+
schema_key: "lix_version_ref".to_string(),
|
|
2108
|
+
file_id: None,
|
|
2109
|
+
snapshot_content: Some(
|
|
2110
|
+
serde_json::to_string(&json!({
|
|
2111
|
+
"id": version_id,
|
|
2112
|
+
"commit_id": commit_id,
|
|
2113
|
+
}))
|
|
2114
|
+
.expect("version ref should serialize"),
|
|
2115
|
+
),
|
|
2116
|
+
metadata: None,
|
|
2117
|
+
schema_version: "1".to_string(),
|
|
2118
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2119
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2120
|
+
global: true,
|
|
2121
|
+
version_id: "global".to_string(),
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
async fn write_version_refs(storage: StorageContext, refs: &[MaterializedUntrackedStateRow]) {
|
|
2126
|
+
let mut transaction = storage
|
|
2127
|
+
.begin_write_transaction()
|
|
2128
|
+
.await
|
|
2129
|
+
.expect("version-ref transaction should open");
|
|
2130
|
+
let mut writes = StorageWriteSet::new();
|
|
2131
|
+
let canonical_refs = {
|
|
2132
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
2133
|
+
refs.iter()
|
|
2134
|
+
.map(|row| canonicalize_materialized_row(&mut writes, &mut json_writer, row))
|
|
2135
|
+
.collect::<Result<Vec<_>, _>>()
|
|
2136
|
+
.expect("version refs should canonicalize")
|
|
2137
|
+
};
|
|
2138
|
+
UntrackedStateContext::new()
|
|
2139
|
+
.writer(&mut writes)
|
|
2140
|
+
.stage_rows(&canonical_refs)
|
|
2141
|
+
.expect("version refs should write");
|
|
2142
|
+
writes
|
|
2143
|
+
.apply(&mut transaction.as_mut())
|
|
2144
|
+
.await
|
|
2145
|
+
.expect("version refs should apply");
|
|
2146
|
+
transaction
|
|
2147
|
+
.commit()
|
|
2148
|
+
.await
|
|
2149
|
+
.expect("version-ref transaction should commit");
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
fn commit_live_state_row(commit_id: &str) -> LiveStateRow {
|
|
2153
|
+
commit_live_state_row_with_parents(commit_id, &[])
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
fn commit_live_state_row_with_parents(
|
|
2157
|
+
commit_id: &str,
|
|
2158
|
+
parent_commit_ids: &[&str],
|
|
2159
|
+
) -> LiveStateRow {
|
|
2160
|
+
commit_live_state_row_with_snapshot(
|
|
2161
|
+
commit_id,
|
|
2162
|
+
json!({
|
|
2163
|
+
"id": commit_id,
|
|
2164
|
+
"change_set_id": format!("change-set-{commit_id}"),
|
|
2165
|
+
"change_ids": ["change-version"],
|
|
2166
|
+
"parent_commit_ids": parent_commit_ids,
|
|
2167
|
+
}),
|
|
2168
|
+
)
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
fn commit_live_state_row_with_snapshot(
|
|
2172
|
+
commit_id: &str,
|
|
2173
|
+
snapshot: serde_json::Value,
|
|
2174
|
+
) -> LiveStateRow {
|
|
2175
|
+
LiveStateRow {
|
|
2176
|
+
entity_id: identity(commit_id),
|
|
2177
|
+
schema_key: COMMIT_SCHEMA_KEY.to_string(),
|
|
2178
|
+
file_id: None,
|
|
2179
|
+
snapshot_content: Some(
|
|
2180
|
+
serde_json::to_string(&snapshot).expect("commit snapshot should serialize"),
|
|
2181
|
+
),
|
|
2182
|
+
metadata: None,
|
|
2183
|
+
schema_version: "1".to_string(),
|
|
2184
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2185
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2186
|
+
global: true,
|
|
2187
|
+
change_id: Some(format!("change-{commit_id}")),
|
|
2188
|
+
commit_id: Some(commit_id.to_string()),
|
|
2189
|
+
untracked: false,
|
|
2190
|
+
version_id: "global".to_string(),
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
async fn append_commit_change(storage: StorageContext, commit_id: &str) {
|
|
2195
|
+
let changelog = crate::changelog::ChangelogContext::new();
|
|
2196
|
+
let mut transaction = storage
|
|
2197
|
+
.begin_write_transaction()
|
|
2198
|
+
.await
|
|
2199
|
+
.expect("transaction should open");
|
|
2200
|
+
let change = MaterializedCanonicalChange {
|
|
2201
|
+
id: format!("change-{commit_id}"),
|
|
2202
|
+
entity_id: crate::entity_identity::EntityIdentity::single(commit_id),
|
|
2203
|
+
schema_key: COMMIT_SCHEMA_KEY.to_string(),
|
|
2204
|
+
schema_version: "1".to_string(),
|
|
2205
|
+
file_id: None,
|
|
2206
|
+
snapshot_content: Some(
|
|
2207
|
+
serde_json::to_string(&json!({
|
|
2208
|
+
"id": commit_id,
|
|
2209
|
+
"change_set_id": format!("change-set-{commit_id}"),
|
|
2210
|
+
"change_ids": [],
|
|
2211
|
+
"parent_commit_ids": [],
|
|
2212
|
+
}))
|
|
2213
|
+
.expect("commit snapshot should serialize"),
|
|
2214
|
+
),
|
|
2215
|
+
metadata: None,
|
|
2216
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2217
|
+
};
|
|
2218
|
+
let mut writes = StorageWriteSet::new();
|
|
2219
|
+
let canonical_change = {
|
|
2220
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
2221
|
+
canonicalize_materialized_change(&mut writes, &mut json_writer, &change)
|
|
2222
|
+
.expect("commit change should canonicalize")
|
|
2223
|
+
};
|
|
2224
|
+
changelog
|
|
2225
|
+
.writer(&mut writes)
|
|
2226
|
+
.stage_changes(&[canonical_change])
|
|
2227
|
+
.expect("commit change should append");
|
|
2228
|
+
writes
|
|
2229
|
+
.apply(&mut transaction.as_mut())
|
|
2230
|
+
.await
|
|
2231
|
+
.expect("commit change should apply");
|
|
2232
|
+
transaction
|
|
2233
|
+
.commit()
|
|
2234
|
+
.await
|
|
2235
|
+
.expect("transaction should commit");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
fn identity(entity_id: &str) -> EntityIdentity {
|
|
2239
|
+
EntityIdentity::single(entity_id)
|
|
2240
|
+
}
|
|
2241
|
+
}
|