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