@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,983 @@
|
|
|
1
|
+
use crate::commit_graph::CommitGraphContext;
|
|
2
|
+
use crate::json_store::{JsonStoreContext, JsonStoreWriter};
|
|
3
|
+
use crate::storage::{StorageReader, StorageWriteSet};
|
|
4
|
+
use crate::tracked_state::by_file_index::ByFileIndex;
|
|
5
|
+
use crate::tracked_state::diff::{diff_commits, TrackedStateDiff, TrackedStateDiffRequest};
|
|
6
|
+
use crate::tracked_state::materialize_value;
|
|
7
|
+
use crate::tracked_state::merge::{self, TrackedStateMergePlan};
|
|
8
|
+
use crate::tracked_state::rebuild::TrackedStateRebuildReport;
|
|
9
|
+
use crate::tracked_state::storage;
|
|
10
|
+
use crate::tracked_state::tree::TrackedStateTree;
|
|
11
|
+
use crate::tracked_state::tree_types::{
|
|
12
|
+
TrackedStateKey, TrackedStateMutation, TrackedStateTreeScanRequest, TrackedStateValue,
|
|
13
|
+
};
|
|
14
|
+
use crate::tracked_state::{TrackedStateRow, TrackedStateRowRequest, TrackedStateScanRequest};
|
|
15
|
+
use crate::LixError;
|
|
16
|
+
|
|
17
|
+
/// Factory for rebuildable tracked-state readers and writers.
|
|
18
|
+
///
|
|
19
|
+
/// Tracked state is stored as content-addressed roots. Version refs
|
|
20
|
+
/// choose which commit/root to read; this context only owns root operations.
|
|
21
|
+
#[derive(Clone)]
|
|
22
|
+
pub(crate) struct TrackedStateContext {
|
|
23
|
+
tree: TrackedStateTree,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl TrackedStateContext {
|
|
27
|
+
pub(crate) fn new() -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
tree: TrackedStateTree::new(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Creates a commit-id-addressed tracked-state reader.
|
|
34
|
+
pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
|
|
35
|
+
where
|
|
36
|
+
S: StorageReader,
|
|
37
|
+
{
|
|
38
|
+
TrackedStateStoreReader {
|
|
39
|
+
store,
|
|
40
|
+
tree: self.tree.clone(),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Creates a tracked-state writer that stages into a caller-owned write set.
|
|
45
|
+
pub(crate) fn writer(&self) -> TrackedStateWriter {
|
|
46
|
+
TrackedStateWriter {
|
|
47
|
+
tree: self.tree.clone(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Rebuilds tracked state at one commit from commit-graph entities.
|
|
52
|
+
pub(crate) async fn rebuild_state_at_commit<R, S>(
|
|
53
|
+
&self,
|
|
54
|
+
commit_graph: &CommitGraphContext,
|
|
55
|
+
read_store: R,
|
|
56
|
+
tracked_store: &mut S,
|
|
57
|
+
writes: &mut StorageWriteSet,
|
|
58
|
+
json_writer: &mut JsonStoreWriter,
|
|
59
|
+
head_commit_id: &str,
|
|
60
|
+
) -> Result<TrackedStateRebuildReport, LixError>
|
|
61
|
+
where
|
|
62
|
+
R: StorageReader,
|
|
63
|
+
S: StorageReader + ?Sized,
|
|
64
|
+
{
|
|
65
|
+
crate::tracked_state::rebuild::rebuild_state_at_commit(
|
|
66
|
+
self,
|
|
67
|
+
commit_graph,
|
|
68
|
+
read_store,
|
|
69
|
+
tracked_store,
|
|
70
|
+
writes,
|
|
71
|
+
json_writer,
|
|
72
|
+
head_commit_id,
|
|
73
|
+
)
|
|
74
|
+
.await
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Store-backed tracked-state reader created by `TrackedStateContext`.
|
|
79
|
+
pub(crate) struct TrackedStateStoreReader<S> {
|
|
80
|
+
store: S,
|
|
81
|
+
tree: TrackedStateTree,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<S> TrackedStateStoreReader<S>
|
|
85
|
+
where
|
|
86
|
+
S: StorageReader,
|
|
87
|
+
{
|
|
88
|
+
pub(crate) async fn scan_rows_at_commit(
|
|
89
|
+
&mut self,
|
|
90
|
+
commit_id: &str,
|
|
91
|
+
request: &TrackedStateScanRequest,
|
|
92
|
+
) -> Result<Vec<TrackedStateRow>, LixError> {
|
|
93
|
+
let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
|
|
94
|
+
return Ok(Vec::new());
|
|
95
|
+
};
|
|
96
|
+
let rows = if ByFileIndex::should_use(request) {
|
|
97
|
+
let Some(by_file_root_id) =
|
|
98
|
+
storage::load_by_file_root(&mut self.store, commit_id).await?
|
|
99
|
+
else {
|
|
100
|
+
return Ok(Vec::new());
|
|
101
|
+
};
|
|
102
|
+
self.scan_rows_at_commit_by_file_index(&root_id, &by_file_root_id, request)
|
|
103
|
+
.await?
|
|
104
|
+
} else {
|
|
105
|
+
let rows = self
|
|
106
|
+
.tree
|
|
107
|
+
.scan(
|
|
108
|
+
&mut self.store,
|
|
109
|
+
&root_id,
|
|
110
|
+
&tree_scan_request_from_tracked(request),
|
|
111
|
+
)
|
|
112
|
+
.await?;
|
|
113
|
+
rows
|
|
114
|
+
};
|
|
115
|
+
let projection = crate::tracked_state::TrackedMaterializationProjection::from_columns(
|
|
116
|
+
&request.projection.columns,
|
|
117
|
+
);
|
|
118
|
+
let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
|
|
119
|
+
let mut materialized = Vec::with_capacity(rows.len());
|
|
120
|
+
for (key, value) in rows {
|
|
121
|
+
materialized.push(materialize_value(&mut json_reader, key, value, &projection).await?);
|
|
122
|
+
}
|
|
123
|
+
Ok(materialized)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub(crate) async fn load_row_at_commit(
|
|
127
|
+
&mut self,
|
|
128
|
+
commit_id: &str,
|
|
129
|
+
request: &TrackedStateRowRequest,
|
|
130
|
+
) -> Result<Option<TrackedStateRow>, LixError> {
|
|
131
|
+
let key = tracked_key_from_request(request)?;
|
|
132
|
+
let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
|
|
133
|
+
return Ok(None);
|
|
134
|
+
};
|
|
135
|
+
let row = self
|
|
136
|
+
.tree
|
|
137
|
+
.get(&mut self.store, &root_id, &key)
|
|
138
|
+
.await?
|
|
139
|
+
.map(|value| async {
|
|
140
|
+
let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
|
|
141
|
+
materialize_value(
|
|
142
|
+
&mut json_reader,
|
|
143
|
+
key,
|
|
144
|
+
value,
|
|
145
|
+
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
146
|
+
)
|
|
147
|
+
.await
|
|
148
|
+
});
|
|
149
|
+
match row {
|
|
150
|
+
Some(row) => row.await.map(Some),
|
|
151
|
+
None => Ok(None),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub(crate) async fn diff_commits(
|
|
156
|
+
&mut self,
|
|
157
|
+
left_commit_id: &str,
|
|
158
|
+
right_commit_id: &str,
|
|
159
|
+
request: &TrackedStateDiffRequest,
|
|
160
|
+
) -> Result<TrackedStateDiff, LixError> {
|
|
161
|
+
diff_commits(self, left_commit_id, right_commit_id, request).await
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub(crate) async fn diff_tree_entries_at_commits(
|
|
165
|
+
&mut self,
|
|
166
|
+
left_commit_id: &str,
|
|
167
|
+
right_commit_id: &str,
|
|
168
|
+
request: &TrackedStateTreeScanRequest,
|
|
169
|
+
) -> Result<Vec<crate::tracked_state::tree_types::TrackedStateTreeDiffEntry>, LixError> {
|
|
170
|
+
let left_root = self.tree.load_root(&mut self.store, left_commit_id).await?;
|
|
171
|
+
let right_root = self
|
|
172
|
+
.tree
|
|
173
|
+
.load_root(&mut self.store, right_commit_id)
|
|
174
|
+
.await?;
|
|
175
|
+
let entries = self
|
|
176
|
+
.tree
|
|
177
|
+
.diff(
|
|
178
|
+
&mut self.store,
|
|
179
|
+
left_root.as_ref(),
|
|
180
|
+
right_root.as_ref(),
|
|
181
|
+
request,
|
|
182
|
+
)
|
|
183
|
+
.await?;
|
|
184
|
+
Ok(entries)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pub(crate) async fn materialize_tree_value(
|
|
188
|
+
&mut self,
|
|
189
|
+
key: TrackedStateKey,
|
|
190
|
+
value: TrackedStateValue,
|
|
191
|
+
) -> Result<TrackedStateRow, LixError> {
|
|
192
|
+
let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
|
|
193
|
+
materialize_value(
|
|
194
|
+
&mut json_reader,
|
|
195
|
+
key,
|
|
196
|
+
value,
|
|
197
|
+
&crate::tracked_state::TrackedMaterializationProjection::full(),
|
|
198
|
+
)
|
|
199
|
+
.await
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async fn scan_rows_at_commit_by_file_index(
|
|
203
|
+
&mut self,
|
|
204
|
+
primary_root_id: &crate::tracked_state::tree_types::TrackedStateRootId,
|
|
205
|
+
by_file_root_id: &crate::tracked_state::tree_types::TrackedStateRootId,
|
|
206
|
+
request: &TrackedStateScanRequest,
|
|
207
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateValue)>, LixError> {
|
|
208
|
+
let by_file_request = ByFileIndex::scan_request_from_tracked(request);
|
|
209
|
+
let index_match_count = self
|
|
210
|
+
.tree
|
|
211
|
+
.count_matching_keys(&mut self.store, by_file_root_id, &by_file_request)
|
|
212
|
+
.await?;
|
|
213
|
+
let primary_row_count = self
|
|
214
|
+
.tree
|
|
215
|
+
.row_count(&mut self.store, primary_root_id)
|
|
216
|
+
.await?;
|
|
217
|
+
if index_match_count * 20 > primary_row_count {
|
|
218
|
+
let rows = self
|
|
219
|
+
.tree
|
|
220
|
+
.scan(
|
|
221
|
+
&mut self.store,
|
|
222
|
+
primary_root_id,
|
|
223
|
+
&tree_scan_request_from_tracked(request),
|
|
224
|
+
)
|
|
225
|
+
.await?;
|
|
226
|
+
return Ok(rows);
|
|
227
|
+
}
|
|
228
|
+
let index_rows = self
|
|
229
|
+
.tree
|
|
230
|
+
.scan(&mut self.store, by_file_root_id, &by_file_request)
|
|
231
|
+
.await?;
|
|
232
|
+
let mut rows = Vec::new();
|
|
233
|
+
let tree_request = tree_scan_request_from_tracked(request);
|
|
234
|
+
let needs_payloads = scan_needs_json_payloads(request);
|
|
235
|
+
if needs_payloads {
|
|
236
|
+
let mut primary_keys = Vec::with_capacity(index_rows.len());
|
|
237
|
+
for (index_key, _) in index_rows {
|
|
238
|
+
if let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) {
|
|
239
|
+
primary_keys.push(primary_key);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let primary_values = self
|
|
243
|
+
.tree
|
|
244
|
+
.get_many(&mut self.store, primary_root_id, &primary_keys)
|
|
245
|
+
.await?;
|
|
246
|
+
for (primary_key, value) in primary_keys.into_iter().zip(primary_values) {
|
|
247
|
+
if request.limit.is_some_and(|limit| rows.len() >= limit) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
let Some(value) = value else {
|
|
251
|
+
continue;
|
|
252
|
+
};
|
|
253
|
+
if !tree_request.matches(&primary_key, &value) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
rows.push((primary_key, value));
|
|
257
|
+
}
|
|
258
|
+
return Ok(rows);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (index_key, index_value) in index_rows {
|
|
262
|
+
if request.limit.is_some_and(|limit| rows.len() >= limit) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) else {
|
|
266
|
+
continue;
|
|
267
|
+
};
|
|
268
|
+
let value = index_value;
|
|
269
|
+
if tree_request.matches(&primary_key, &value) {
|
|
270
|
+
rows.push((primary_key, value));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
Ok(rows)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Plans a three-way merge by diffing both heads against the same base.
|
|
277
|
+
///
|
|
278
|
+
/// `target_commit_id` is the destination root that should keep its own
|
|
279
|
+
/// changes. `source_commit_id` is the incoming root whose non-conflicting
|
|
280
|
+
/// changes should be applied.
|
|
281
|
+
#[allow(dead_code)]
|
|
282
|
+
pub(crate) async fn plan_merge(
|
|
283
|
+
&mut self,
|
|
284
|
+
base_commit_id: &str,
|
|
285
|
+
target_commit_id: &str,
|
|
286
|
+
source_commit_id: &str,
|
|
287
|
+
request: &TrackedStateDiffRequest,
|
|
288
|
+
) -> Result<TrackedStateMergePlan, LixError> {
|
|
289
|
+
let target_diff = self
|
|
290
|
+
.diff_commits(base_commit_id, target_commit_id, request)
|
|
291
|
+
.await?;
|
|
292
|
+
let source_diff = self
|
|
293
|
+
.diff_commits(base_commit_id, source_commit_id, request)
|
|
294
|
+
.await?;
|
|
295
|
+
merge::plan_merge(&target_diff, &source_diff)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[cfg(test)]
|
|
299
|
+
pub(crate) async fn load_root_for_test(
|
|
300
|
+
&mut self,
|
|
301
|
+
commit_id: &str,
|
|
302
|
+
) -> Result<Option<crate::tracked_state::tree_types::TrackedStateRootId>, LixError> {
|
|
303
|
+
self.tree.load_root(&mut self.store, commit_id).await
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Writer for rebuildable tracked-state roots.
|
|
308
|
+
pub(crate) struct TrackedStateWriter {
|
|
309
|
+
tree: TrackedStateTree,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
impl TrackedStateWriter {
|
|
313
|
+
/// Stages one root for `commit_id` from the provided row set.
|
|
314
|
+
///
|
|
315
|
+
/// `parent_commit_id` is the tracked-state root to layer mutations on top
|
|
316
|
+
/// of. Rebuild passes `None` because it has already materialized the full
|
|
317
|
+
/// entity set for the requested head.
|
|
318
|
+
pub(crate) async fn stage_root(
|
|
319
|
+
&mut self,
|
|
320
|
+
store: &mut (impl StorageReader + ?Sized),
|
|
321
|
+
writes: &mut StorageWriteSet,
|
|
322
|
+
json_writer: &mut JsonStoreWriter,
|
|
323
|
+
commit_id: &str,
|
|
324
|
+
parent_commit_id: Option<&str>,
|
|
325
|
+
rows: &[TrackedStateRow],
|
|
326
|
+
) -> Result<TrackedStateWriteReceipt, LixError> {
|
|
327
|
+
let base_root = match parent_commit_id {
|
|
328
|
+
Some(parent_commit_id) => {
|
|
329
|
+
let Some(root) = self.tree.load_root(store, parent_commit_id).await? else {
|
|
330
|
+
return Err(LixError::new(
|
|
331
|
+
"LIX_ERROR_UNKNOWN",
|
|
332
|
+
format!(
|
|
333
|
+
"tracked-state parent root for commit '{parent_commit_id}' is missing"
|
|
334
|
+
),
|
|
335
|
+
));
|
|
336
|
+
};
|
|
337
|
+
Some(root)
|
|
338
|
+
}
|
|
339
|
+
None => None,
|
|
340
|
+
};
|
|
341
|
+
let mut stored_rows = Vec::with_capacity(rows.len());
|
|
342
|
+
let mut mutations = Vec::with_capacity(rows.len());
|
|
343
|
+
for row in rows {
|
|
344
|
+
let stored_value =
|
|
345
|
+
crate::tracked_state::canonicalize_materialized_row(writes, json_writer, row)?;
|
|
346
|
+
mutations.push(TrackedStateMutation::put(
|
|
347
|
+
TrackedStateKey::from_row(row),
|
|
348
|
+
stored_value.clone(),
|
|
349
|
+
));
|
|
350
|
+
stored_rows.push((row, stored_value));
|
|
351
|
+
}
|
|
352
|
+
let result = self
|
|
353
|
+
.tree
|
|
354
|
+
.apply_mutations(
|
|
355
|
+
store,
|
|
356
|
+
writes,
|
|
357
|
+
base_root.as_ref(),
|
|
358
|
+
mutations,
|
|
359
|
+
Some(commit_id),
|
|
360
|
+
)
|
|
361
|
+
.await?;
|
|
362
|
+
|
|
363
|
+
let by_file_base_root = match parent_commit_id {
|
|
364
|
+
Some(parent_commit_id) => storage::load_by_file_root(store, parent_commit_id)
|
|
365
|
+
.await?
|
|
366
|
+
.ok_or_else(|| {
|
|
367
|
+
LixError::new(
|
|
368
|
+
"LIX_ERROR_UNKNOWN",
|
|
369
|
+
format!(
|
|
370
|
+
"tracked-state by-file parent root for commit '{parent_commit_id}' is missing"
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
})
|
|
374
|
+
.map(Some)?,
|
|
375
|
+
None => None,
|
|
376
|
+
};
|
|
377
|
+
let mut by_file_mutations = Vec::with_capacity(rows.len());
|
|
378
|
+
for (row, stored_value) in &stored_rows {
|
|
379
|
+
by_file_mutations.push(TrackedStateMutation::put(
|
|
380
|
+
ByFileIndex::key_from_row(row),
|
|
381
|
+
ByFileIndex::header_value_from_primary(stored_value),
|
|
382
|
+
));
|
|
383
|
+
}
|
|
384
|
+
let by_file_result = self
|
|
385
|
+
.tree
|
|
386
|
+
.apply_mutations(
|
|
387
|
+
store,
|
|
388
|
+
writes,
|
|
389
|
+
by_file_base_root.as_ref(),
|
|
390
|
+
by_file_mutations,
|
|
391
|
+
None,
|
|
392
|
+
)
|
|
393
|
+
.await?;
|
|
394
|
+
storage::stage_by_file_root(writes, commit_id, &by_file_result.root_id);
|
|
395
|
+
Ok(TrackedStateWriteReceipt {
|
|
396
|
+
commit_id: commit_id.to_string(),
|
|
397
|
+
row_count: result.row_count,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Deletes the root pointer for one commit.
|
|
402
|
+
///
|
|
403
|
+
/// This is intentionally root-scoped, not row-scoped. It is useful for
|
|
404
|
+
/// rebuild/corruption tests where the changelog remains authoritative and
|
|
405
|
+
/// the tracked-state projection must be recreated from the commit id.
|
|
406
|
+
#[cfg(test)]
|
|
407
|
+
pub(crate) fn stage_delete_root_for_rebuild(
|
|
408
|
+
&mut self,
|
|
409
|
+
writes: &mut StorageWriteSet,
|
|
410
|
+
commit_id: &str,
|
|
411
|
+
) {
|
|
412
|
+
storage::stage_delete_root(writes, commit_id)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
417
|
+
pub(crate) struct TrackedStateWriteReceipt {
|
|
418
|
+
pub(crate) commit_id: String,
|
|
419
|
+
pub(crate) row_count: usize,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn tree_scan_request_from_tracked(
|
|
423
|
+
request: &TrackedStateScanRequest,
|
|
424
|
+
) -> TrackedStateTreeScanRequest {
|
|
425
|
+
TrackedStateTreeScanRequest {
|
|
426
|
+
schema_keys: request.filter.schema_keys.clone(),
|
|
427
|
+
entity_ids: request.filter.entity_ids.clone(),
|
|
428
|
+
file_ids: request.filter.file_ids.clone(),
|
|
429
|
+
include_tombstones: request.filter.include_tombstones,
|
|
430
|
+
limit: request.limit,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fn scan_needs_json_payloads(request: &TrackedStateScanRequest) -> bool {
|
|
435
|
+
if request.projection.columns.is_empty() {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
request
|
|
439
|
+
.projection
|
|
440
|
+
.columns
|
|
441
|
+
.iter()
|
|
442
|
+
.any(|column| column == "snapshot_content" || column == "metadata")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
fn tracked_key_from_request(request: &TrackedStateRowRequest) -> Result<TrackedStateKey, LixError> {
|
|
446
|
+
let file_id = match &request.file_id {
|
|
447
|
+
crate::NullableKeyFilter::Null => None,
|
|
448
|
+
crate::NullableKeyFilter::Value(value) => Some(value.clone()),
|
|
449
|
+
crate::NullableKeyFilter::Any => {
|
|
450
|
+
return Err(LixError::new(
|
|
451
|
+
"LIX_ERROR_UNKNOWN",
|
|
452
|
+
"tracked-state tree exact lookup requires a concrete file_id filter",
|
|
453
|
+
))
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
Ok(TrackedStateKey {
|
|
457
|
+
schema_key: request.schema_key.clone(),
|
|
458
|
+
file_id,
|
|
459
|
+
entity_id: request.entity_id.clone(),
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[cfg(test)]
|
|
464
|
+
mod tests {
|
|
465
|
+
use std::sync::Arc;
|
|
466
|
+
|
|
467
|
+
use super::*;
|
|
468
|
+
use crate::backend::{testing::UnitTestBackend, Backend};
|
|
469
|
+
use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
|
|
470
|
+
use crate::NullableKeyFilter;
|
|
471
|
+
|
|
472
|
+
#[tokio::test]
|
|
473
|
+
async fn write_root_rejects_missing_parent_root() {
|
|
474
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
475
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
476
|
+
let tracked_state = TrackedStateContext::new();
|
|
477
|
+
let mut transaction = storage
|
|
478
|
+
.begin_write_transaction()
|
|
479
|
+
.await
|
|
480
|
+
.expect("transaction should open");
|
|
481
|
+
|
|
482
|
+
let error = write_root_for_test(
|
|
483
|
+
transaction.as_mut(),
|
|
484
|
+
&tracked_state,
|
|
485
|
+
"commit-child",
|
|
486
|
+
Some("missing-parent"),
|
|
487
|
+
&[row("entity-child", "change-child", "commit-child")],
|
|
488
|
+
)
|
|
489
|
+
.await
|
|
490
|
+
.expect_err("parent root must exist when parent_commit_id is provided");
|
|
491
|
+
|
|
492
|
+
assert!(
|
|
493
|
+
error.message.contains("parent root") && error.message.contains("missing-parent"),
|
|
494
|
+
"unexpected error: {error:?}"
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[tokio::test]
|
|
499
|
+
async fn plan_merge_from_roots_applies_source_only_change() {
|
|
500
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
501
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
502
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
503
|
+
&[row_with_value(
|
|
504
|
+
"entity-a",
|
|
505
|
+
"change-source",
|
|
506
|
+
"source",
|
|
507
|
+
"source",
|
|
508
|
+
)],
|
|
509
|
+
)
|
|
510
|
+
.await;
|
|
511
|
+
|
|
512
|
+
let plan = tracked_state
|
|
513
|
+
.reader(storage.clone())
|
|
514
|
+
.plan_merge(
|
|
515
|
+
"base",
|
|
516
|
+
"target",
|
|
517
|
+
"source",
|
|
518
|
+
&TrackedStateDiffRequest::default(),
|
|
519
|
+
)
|
|
520
|
+
.await
|
|
521
|
+
.expect("merge should plan");
|
|
522
|
+
|
|
523
|
+
assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
|
|
524
|
+
assert!(plan.conflicts.is_empty());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[tokio::test]
|
|
528
|
+
async fn plan_merge_from_roots_keeps_target_only_change() {
|
|
529
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
530
|
+
&[row("entity-a", "change-base", "base")],
|
|
531
|
+
&[row("entity-a", "change-target", "target")],
|
|
532
|
+
&[row("entity-a", "change-base", "base")],
|
|
533
|
+
)
|
|
534
|
+
.await;
|
|
535
|
+
|
|
536
|
+
let plan = tracked_state
|
|
537
|
+
.reader(storage.clone())
|
|
538
|
+
.plan_merge(
|
|
539
|
+
"base",
|
|
540
|
+
"target",
|
|
541
|
+
"source",
|
|
542
|
+
&TrackedStateDiffRequest::default(),
|
|
543
|
+
)
|
|
544
|
+
.await
|
|
545
|
+
.expect("merge should plan");
|
|
546
|
+
|
|
547
|
+
assert!(plan.patches.is_empty());
|
|
548
|
+
assert!(plan.conflicts.is_empty());
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[tokio::test]
|
|
552
|
+
async fn plan_merge_from_roots_reports_divergent_modification_conflict() {
|
|
553
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
554
|
+
&[row_with_value("entity-a", "change-base", "base", "base")],
|
|
555
|
+
&[row_with_value(
|
|
556
|
+
"entity-a",
|
|
557
|
+
"change-target",
|
|
558
|
+
"target",
|
|
559
|
+
"target",
|
|
560
|
+
)],
|
|
561
|
+
&[row_with_value(
|
|
562
|
+
"entity-a",
|
|
563
|
+
"change-source",
|
|
564
|
+
"source",
|
|
565
|
+
"source",
|
|
566
|
+
)],
|
|
567
|
+
)
|
|
568
|
+
.await;
|
|
569
|
+
|
|
570
|
+
let plan = tracked_state
|
|
571
|
+
.reader(storage.clone())
|
|
572
|
+
.plan_merge(
|
|
573
|
+
"base",
|
|
574
|
+
"target",
|
|
575
|
+
"source",
|
|
576
|
+
&TrackedStateDiffRequest::default(),
|
|
577
|
+
)
|
|
578
|
+
.await
|
|
579
|
+
.expect("merge should plan");
|
|
580
|
+
|
|
581
|
+
assert!(plan.patches.is_empty());
|
|
582
|
+
assert_eq!(merge_conflict_ids(&plan), vec!["entity-a"]);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[tokio::test]
|
|
586
|
+
async fn plan_merge_from_roots_applies_source_tombstone() {
|
|
587
|
+
let (storage, tracked_state) = seed_merge_roots(
|
|
588
|
+
&[row("entity-a", "change-base", "base")],
|
|
589
|
+
&[row("entity-a", "change-base", "base")],
|
|
590
|
+
&[tombstone("entity-a", "change-source-delete", "source")],
|
|
591
|
+
)
|
|
592
|
+
.await;
|
|
593
|
+
|
|
594
|
+
let plan = tracked_state
|
|
595
|
+
.reader(storage.clone())
|
|
596
|
+
.plan_merge(
|
|
597
|
+
"base",
|
|
598
|
+
"target",
|
|
599
|
+
"source",
|
|
600
|
+
&TrackedStateDiffRequest::default(),
|
|
601
|
+
)
|
|
602
|
+
.await
|
|
603
|
+
.expect("merge should plan");
|
|
604
|
+
|
|
605
|
+
assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
|
|
606
|
+
assert_eq!(plan.patches[0].projected_row().snapshot_content, None);
|
|
607
|
+
assert_eq!(plan.patches[0].change_id(), "change-source-delete");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#[tokio::test]
|
|
611
|
+
async fn scan_rows_by_file_uses_file_index_shape() {
|
|
612
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
613
|
+
let storage = StorageContext::new(backend.clone());
|
|
614
|
+
let tracked_state = TrackedStateContext::new();
|
|
615
|
+
let mut file_a = row("entity-a", "change-a", "commit-1");
|
|
616
|
+
file_a.file_id = Some("file-a.json".to_string());
|
|
617
|
+
let mut file_b = row("entity-b", "change-b", "commit-1");
|
|
618
|
+
file_b.file_id = Some("file-b.json".to_string());
|
|
619
|
+
|
|
620
|
+
let mut transaction = storage
|
|
621
|
+
.begin_write_transaction()
|
|
622
|
+
.await
|
|
623
|
+
.expect("transaction should open");
|
|
624
|
+
write_root_for_test(
|
|
625
|
+
transaction.as_mut(),
|
|
626
|
+
&tracked_state,
|
|
627
|
+
"commit-1",
|
|
628
|
+
None,
|
|
629
|
+
&[file_a, file_b],
|
|
630
|
+
)
|
|
631
|
+
.await
|
|
632
|
+
.expect("root should write");
|
|
633
|
+
transaction
|
|
634
|
+
.commit()
|
|
635
|
+
.await
|
|
636
|
+
.expect("transaction should commit");
|
|
637
|
+
|
|
638
|
+
let rows = tracked_state
|
|
639
|
+
.reader(storage.clone())
|
|
640
|
+
.scan_rows_at_commit(
|
|
641
|
+
"commit-1",
|
|
642
|
+
&TrackedStateScanRequest {
|
|
643
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
644
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
645
|
+
..Default::default()
|
|
646
|
+
},
|
|
647
|
+
..Default::default()
|
|
648
|
+
},
|
|
649
|
+
)
|
|
650
|
+
.await
|
|
651
|
+
.expect("file scan should read through index");
|
|
652
|
+
|
|
653
|
+
assert_eq!(rows.len(), 1);
|
|
654
|
+
assert_eq!(
|
|
655
|
+
rows[0].entity_id.as_string().expect("entity id"),
|
|
656
|
+
"entity-a"
|
|
657
|
+
);
|
|
658
|
+
assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
#[tokio::test]
|
|
662
|
+
async fn by_file_header_index_fetches_primary_payload_only_when_requested() {
|
|
663
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
664
|
+
let storage = StorageContext::new(backend.clone());
|
|
665
|
+
let tracked_state = TrackedStateContext::new();
|
|
666
|
+
let mut row = row("entity-a", "change-a", "commit-1");
|
|
667
|
+
row.file_id = Some("file-a.json".to_string());
|
|
668
|
+
let expected_snapshot = row.snapshot_content.clone();
|
|
669
|
+
|
|
670
|
+
let mut transaction = storage
|
|
671
|
+
.begin_write_transaction()
|
|
672
|
+
.await
|
|
673
|
+
.expect("transaction should open");
|
|
674
|
+
write_root_for_test(
|
|
675
|
+
transaction.as_mut(),
|
|
676
|
+
&tracked_state,
|
|
677
|
+
"commit-1",
|
|
678
|
+
None,
|
|
679
|
+
std::slice::from_ref(&row),
|
|
680
|
+
)
|
|
681
|
+
.await
|
|
682
|
+
.expect("root should write");
|
|
683
|
+
transaction
|
|
684
|
+
.commit()
|
|
685
|
+
.await
|
|
686
|
+
.expect("transaction should commit");
|
|
687
|
+
|
|
688
|
+
let mut reader = tracked_state.reader(storage.clone());
|
|
689
|
+
let header_rows = reader
|
|
690
|
+
.scan_rows_at_commit(
|
|
691
|
+
"commit-1",
|
|
692
|
+
&TrackedStateScanRequest {
|
|
693
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
694
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
695
|
+
..Default::default()
|
|
696
|
+
},
|
|
697
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
698
|
+
columns: vec!["entity_id".to_string()],
|
|
699
|
+
},
|
|
700
|
+
..Default::default()
|
|
701
|
+
},
|
|
702
|
+
)
|
|
703
|
+
.await
|
|
704
|
+
.expect("header scan should read through by-file index");
|
|
705
|
+
let full_rows = reader
|
|
706
|
+
.scan_rows_at_commit(
|
|
707
|
+
"commit-1",
|
|
708
|
+
&TrackedStateScanRequest {
|
|
709
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
710
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
711
|
+
..Default::default()
|
|
712
|
+
},
|
|
713
|
+
..Default::default()
|
|
714
|
+
},
|
|
715
|
+
)
|
|
716
|
+
.await
|
|
717
|
+
.expect("full scan should fetch primary payload");
|
|
718
|
+
|
|
719
|
+
assert_eq!(header_rows[0].snapshot_content, None);
|
|
720
|
+
assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
#[tokio::test]
|
|
724
|
+
async fn by_file_header_index_filters_tombstones_without_payload_sentinel() {
|
|
725
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
726
|
+
let storage = StorageContext::new(backend.clone());
|
|
727
|
+
let tracked_state = TrackedStateContext::new();
|
|
728
|
+
let mut live = row("entity-live", "change-live", "commit-1");
|
|
729
|
+
live.file_id = Some("file-a.json".to_string());
|
|
730
|
+
let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
|
|
731
|
+
deleted.file_id = Some("file-a.json".to_string());
|
|
732
|
+
|
|
733
|
+
let mut transaction = storage
|
|
734
|
+
.begin_write_transaction()
|
|
735
|
+
.await
|
|
736
|
+
.expect("transaction should open");
|
|
737
|
+
write_root_for_test(
|
|
738
|
+
transaction.as_mut(),
|
|
739
|
+
&tracked_state,
|
|
740
|
+
"commit-1",
|
|
741
|
+
None,
|
|
742
|
+
&[live, deleted],
|
|
743
|
+
)
|
|
744
|
+
.await
|
|
745
|
+
.expect("root should write");
|
|
746
|
+
transaction
|
|
747
|
+
.commit()
|
|
748
|
+
.await
|
|
749
|
+
.expect("transaction should commit");
|
|
750
|
+
|
|
751
|
+
let rows = tracked_state
|
|
752
|
+
.reader(storage.clone())
|
|
753
|
+
.scan_rows_at_commit(
|
|
754
|
+
"commit-1",
|
|
755
|
+
&TrackedStateScanRequest {
|
|
756
|
+
filter: crate::tracked_state::TrackedStateFilter {
|
|
757
|
+
file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
|
|
758
|
+
..Default::default()
|
|
759
|
+
},
|
|
760
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
761
|
+
columns: vec!["entity_id".to_string()],
|
|
762
|
+
},
|
|
763
|
+
..Default::default()
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
.await
|
|
767
|
+
.expect("file scan should read through index");
|
|
768
|
+
|
|
769
|
+
assert_eq!(rows.len(), 1);
|
|
770
|
+
assert_eq!(
|
|
771
|
+
rows[0].entity_id.as_string().expect("entity id"),
|
|
772
|
+
"entity-live"
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#[tokio::test]
|
|
777
|
+
async fn reads_resolve_json_snapshot_refs() {
|
|
778
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
779
|
+
let storage = StorageContext::new(backend.clone());
|
|
780
|
+
let tracked_state = TrackedStateContext::new();
|
|
781
|
+
let large_value = "x".repeat(1536);
|
|
782
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
783
|
+
|
|
784
|
+
let mut transaction = storage
|
|
785
|
+
.begin_write_transaction()
|
|
786
|
+
.await
|
|
787
|
+
.expect("transaction should open");
|
|
788
|
+
write_root_for_test(
|
|
789
|
+
transaction.as_mut(),
|
|
790
|
+
&tracked_state,
|
|
791
|
+
"commit-1",
|
|
792
|
+
None,
|
|
793
|
+
std::slice::from_ref(&row),
|
|
794
|
+
)
|
|
795
|
+
.await
|
|
796
|
+
.expect("root should write");
|
|
797
|
+
transaction
|
|
798
|
+
.commit()
|
|
799
|
+
.await
|
|
800
|
+
.expect("transaction should commit");
|
|
801
|
+
|
|
802
|
+
let mut reader = tracked_state.reader(storage.clone());
|
|
803
|
+
let loaded = reader
|
|
804
|
+
.load_row_at_commit(
|
|
805
|
+
"commit-1",
|
|
806
|
+
&TrackedStateRowRequest {
|
|
807
|
+
schema_key: row.schema_key.clone(),
|
|
808
|
+
entity_id: row.entity_id.clone(),
|
|
809
|
+
file_id: NullableKeyFilter::Null,
|
|
810
|
+
},
|
|
811
|
+
)
|
|
812
|
+
.await
|
|
813
|
+
.expect("row should load")
|
|
814
|
+
.expect("row should exist");
|
|
815
|
+
let scanned = reader
|
|
816
|
+
.scan_rows_at_commit("commit-1", &TrackedStateScanRequest::default())
|
|
817
|
+
.await
|
|
818
|
+
.expect("rows should scan");
|
|
819
|
+
|
|
820
|
+
assert_eq!(loaded.snapshot_content, row.snapshot_content);
|
|
821
|
+
assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
#[tokio::test]
|
|
825
|
+
async fn projected_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
|
|
826
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
827
|
+
let storage = StorageContext::new(backend.clone());
|
|
828
|
+
let tracked_state = TrackedStateContext::new();
|
|
829
|
+
let large_value = "x".repeat(1536);
|
|
830
|
+
let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
|
|
831
|
+
|
|
832
|
+
let mut transaction = storage
|
|
833
|
+
.begin_write_transaction()
|
|
834
|
+
.await
|
|
835
|
+
.expect("transaction should open");
|
|
836
|
+
write_root_for_test(
|
|
837
|
+
transaction.as_mut(),
|
|
838
|
+
&tracked_state,
|
|
839
|
+
"commit-1",
|
|
840
|
+
None,
|
|
841
|
+
std::slice::from_ref(&row),
|
|
842
|
+
)
|
|
843
|
+
.await
|
|
844
|
+
.expect("root should write");
|
|
845
|
+
transaction
|
|
846
|
+
.commit()
|
|
847
|
+
.await
|
|
848
|
+
.expect("transaction should commit");
|
|
849
|
+
|
|
850
|
+
let rows = tracked_state
|
|
851
|
+
.reader(storage.clone())
|
|
852
|
+
.scan_rows_at_commit(
|
|
853
|
+
"commit-1",
|
|
854
|
+
&TrackedStateScanRequest {
|
|
855
|
+
projection: crate::tracked_state::TrackedStateProjection {
|
|
856
|
+
columns: vec!["entity_id".to_string()],
|
|
857
|
+
},
|
|
858
|
+
..Default::default()
|
|
859
|
+
},
|
|
860
|
+
)
|
|
861
|
+
.await
|
|
862
|
+
.expect("rows should scan");
|
|
863
|
+
|
|
864
|
+
assert_eq!(rows.len(), 1);
|
|
865
|
+
assert_eq!(rows[0].snapshot_content, None);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async fn seed_merge_roots(
|
|
869
|
+
base_rows: &[TrackedStateRow],
|
|
870
|
+
target_rows: &[TrackedStateRow],
|
|
871
|
+
source_rows: &[TrackedStateRow],
|
|
872
|
+
) -> (StorageContext, TrackedStateContext) {
|
|
873
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
874
|
+
let storage = StorageContext::new(backend.clone());
|
|
875
|
+
let tracked_state = TrackedStateContext::new();
|
|
876
|
+
let mut transaction = storage
|
|
877
|
+
.begin_write_transaction()
|
|
878
|
+
.await
|
|
879
|
+
.expect("transaction should open");
|
|
880
|
+
write_root_for_test(
|
|
881
|
+
transaction.as_mut(),
|
|
882
|
+
&tracked_state,
|
|
883
|
+
"base",
|
|
884
|
+
None,
|
|
885
|
+
base_rows,
|
|
886
|
+
)
|
|
887
|
+
.await
|
|
888
|
+
.expect("base root should write");
|
|
889
|
+
write_root_for_test(
|
|
890
|
+
transaction.as_mut(),
|
|
891
|
+
&tracked_state,
|
|
892
|
+
"target",
|
|
893
|
+
None,
|
|
894
|
+
target_rows,
|
|
895
|
+
)
|
|
896
|
+
.await
|
|
897
|
+
.expect("target root should write");
|
|
898
|
+
write_root_for_test(
|
|
899
|
+
transaction.as_mut(),
|
|
900
|
+
&tracked_state,
|
|
901
|
+
"source",
|
|
902
|
+
None,
|
|
903
|
+
source_rows,
|
|
904
|
+
)
|
|
905
|
+
.await
|
|
906
|
+
.expect("source root should write");
|
|
907
|
+
transaction
|
|
908
|
+
.commit()
|
|
909
|
+
.await
|
|
910
|
+
.expect("transaction should commit");
|
|
911
|
+
(storage, tracked_state)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
fn merge_patch_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
915
|
+
plan.patches
|
|
916
|
+
.iter()
|
|
917
|
+
.map(|entry| entry.identity().entity_id.as_string().expect("identity"))
|
|
918
|
+
.collect()
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
|
|
922
|
+
plan.conflicts
|
|
923
|
+
.iter()
|
|
924
|
+
.map(|entry| entry.identity.entity_id.as_string().expect("identity"))
|
|
925
|
+
.collect()
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async fn write_root_for_test(
|
|
929
|
+
transaction: &mut dyn StorageWriteTransaction,
|
|
930
|
+
tracked_state: &TrackedStateContext,
|
|
931
|
+
commit_id: &str,
|
|
932
|
+
parent_commit_id: Option<&str>,
|
|
933
|
+
rows: &[TrackedStateRow],
|
|
934
|
+
) -> Result<TrackedStateWriteReceipt, LixError> {
|
|
935
|
+
let mut writes = StorageWriteSet::new();
|
|
936
|
+
let receipt = {
|
|
937
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
938
|
+
tracked_state
|
|
939
|
+
.writer()
|
|
940
|
+
.stage_root(
|
|
941
|
+
transaction,
|
|
942
|
+
&mut writes,
|
|
943
|
+
&mut json_writer,
|
|
944
|
+
commit_id,
|
|
945
|
+
parent_commit_id,
|
|
946
|
+
rows,
|
|
947
|
+
)
|
|
948
|
+
.await?
|
|
949
|
+
};
|
|
950
|
+
writes.apply(transaction).await?;
|
|
951
|
+
Ok(receipt)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
fn tombstone(entity_id: &str, change_id: &str, commit_id: &str) -> TrackedStateRow {
|
|
955
|
+
let mut row = row(entity_id, change_id, commit_id);
|
|
956
|
+
row.snapshot_content = None;
|
|
957
|
+
row
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
fn row(entity_id: &str, change_id: &str, commit_id: &str) -> TrackedStateRow {
|
|
961
|
+
row_with_value(entity_id, change_id, commit_id, "value")
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
fn row_with_value(
|
|
965
|
+
entity_id: &str,
|
|
966
|
+
change_id: &str,
|
|
967
|
+
commit_id: &str,
|
|
968
|
+
value: &str,
|
|
969
|
+
) -> TrackedStateRow {
|
|
970
|
+
TrackedStateRow {
|
|
971
|
+
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
972
|
+
schema_key: "test_schema".to_string(),
|
|
973
|
+
file_id: None,
|
|
974
|
+
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
975
|
+
metadata: None,
|
|
976
|
+
schema_version: "1".to_string(),
|
|
977
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
978
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
979
|
+
change_id: change_id.to_string(),
|
|
980
|
+
commit_id: commit_id.to_string(),
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|