@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,901 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use crate::commit_graph::walker::{best_common_ancestors, walk_reachable_commits};
|
|
4
|
+
use crate::commit_graph::{
|
|
5
|
+
CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
|
|
6
|
+
CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
|
|
7
|
+
};
|
|
8
|
+
use crate::commit_store::{Change, Commit, CommitStoreContext, CommitStoreReader, LocatedChange};
|
|
9
|
+
use crate::entity_identity::EntityIdentity;
|
|
10
|
+
use crate::storage::StorageReader;
|
|
11
|
+
use crate::storage::{ScopedStorageReader, StorageReadScope};
|
|
12
|
+
use crate::LixError;
|
|
13
|
+
|
|
14
|
+
const COMMIT_SCHEMA_KEY: &str = "lix_commit";
|
|
15
|
+
|
|
16
|
+
/// Read model for resolving commit-store commits into entity state at a head.
|
|
17
|
+
///
|
|
18
|
+
/// This module does not own durable storage. It reads immutable commit-store
|
|
19
|
+
/// facts through a caller-provided KV store and applies commit graph rules on
|
|
20
|
+
/// top.
|
|
21
|
+
#[derive(Clone)]
|
|
22
|
+
pub(crate) struct CommitGraphContext {
|
|
23
|
+
commit_store: CommitStoreContext,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl CommitGraphContext {
|
|
27
|
+
pub(crate) fn new() -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
commit_store: CommitStoreContext::new(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Creates a graph reader over a caller-provided KV store.
|
|
34
|
+
pub(crate) fn reader<S>(&self, store: S) -> CommitGraphStoreReader<S>
|
|
35
|
+
where
|
|
36
|
+
S: StorageReader,
|
|
37
|
+
{
|
|
38
|
+
let read_scope = StorageReadScope::new(store);
|
|
39
|
+
CommitGraphStoreReader {
|
|
40
|
+
commit_store_reader: self.commit_store.reader(read_scope.store()),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Commit-graph reader that resolves commit-store entities at a commit head.
|
|
46
|
+
pub(crate) struct CommitGraphStoreReader<S>
|
|
47
|
+
where
|
|
48
|
+
S: StorageReader,
|
|
49
|
+
{
|
|
50
|
+
commit_store_reader: CommitStoreReader<ScopedStorageReader<S>>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl<S> CommitGraphStoreReader<S>
|
|
54
|
+
where
|
|
55
|
+
S: StorageReader,
|
|
56
|
+
{
|
|
57
|
+
/// Loads and parses a `lix_commit` canonical change by commit id.
|
|
58
|
+
pub(crate) async fn load_commit(
|
|
59
|
+
&mut self,
|
|
60
|
+
commit_id: &str,
|
|
61
|
+
) -> Result<Option<CommitGraphCommit>, LixError> {
|
|
62
|
+
let Some(commit) = self.commit_store_reader.load_commit(commit_id).await? else {
|
|
63
|
+
return Ok(None);
|
|
64
|
+
};
|
|
65
|
+
self.graph_commit_from_store_commit(commit).await.map(Some)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Loads every commit fact from the commit store.
|
|
69
|
+
///
|
|
70
|
+
/// This is used by global commit surfaces where the caller wants the durable
|
|
71
|
+
/// graph facts themselves, not reachability from a particular version head.
|
|
72
|
+
pub(crate) async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
73
|
+
let stored_commits = self.commit_store_reader.scan_commits().await?;
|
|
74
|
+
let mut commits = Vec::new();
|
|
75
|
+
for commit in stored_commits {
|
|
76
|
+
commits.push(self.graph_commit_from_store_commit(commit).await?);
|
|
77
|
+
}
|
|
78
|
+
commits.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
|
|
79
|
+
Ok(commits)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Walks from `head_commit_id` through parent commits and records nearest depth.
|
|
83
|
+
pub(crate) async fn reachable_commits(
|
|
84
|
+
&mut self,
|
|
85
|
+
head_commit_id: &str,
|
|
86
|
+
) -> Result<Vec<ReachableCommitGraphCommit>, LixError> {
|
|
87
|
+
walk_reachable_commits(self, head_commit_id).await
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Returns the best common ancestors shared by two commit heads.
|
|
91
|
+
///
|
|
92
|
+
/// This is the commit-DAG primitive. It can return more than one commit in
|
|
93
|
+
/// criss-cross histories. Merge code should layer an explicit merge-base
|
|
94
|
+
/// policy on top when it needs exactly one base for a three-way merge.
|
|
95
|
+
pub(crate) async fn best_common_ancestors(
|
|
96
|
+
&mut self,
|
|
97
|
+
left_commit_id: &str,
|
|
98
|
+
right_commit_id: &str,
|
|
99
|
+
) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
100
|
+
best_common_ancestors(self, left_commit_id, right_commit_id).await
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Resolves the single commit base to use for a three-way merge.
|
|
104
|
+
///
|
|
105
|
+
/// This is merge policy layered over `best_common_ancestors(...)`. Histories
|
|
106
|
+
/// with no shared base or multiple equally good bases are rejected for now
|
|
107
|
+
/// so merge code cannot accidentally hide unsupported graph semantics.
|
|
108
|
+
pub(crate) async fn merge_base(
|
|
109
|
+
&mut self,
|
|
110
|
+
left_commit_id: &str,
|
|
111
|
+
right_commit_id: &str,
|
|
112
|
+
) -> Result<CommitGraphCommit, LixError> {
|
|
113
|
+
let ancestors = self
|
|
114
|
+
.best_common_ancestors(left_commit_id, right_commit_id)
|
|
115
|
+
.await?;
|
|
116
|
+
match ancestors.as_slice() {
|
|
117
|
+
[] => Err(LixError::new(
|
|
118
|
+
"LIX_ERROR_UNKNOWN",
|
|
119
|
+
format!(
|
|
120
|
+
"commit_graph found no common history between '{left_commit_id}' and '{right_commit_id}'"
|
|
121
|
+
),
|
|
122
|
+
)),
|
|
123
|
+
[base] => Ok(base.clone()),
|
|
124
|
+
_ => Err(LixError::ambiguous_merge_base(
|
|
125
|
+
left_commit_id,
|
|
126
|
+
right_commit_id,
|
|
127
|
+
ancestors
|
|
128
|
+
.iter()
|
|
129
|
+
.map(|ancestor| ancestor.commit_id.clone())
|
|
130
|
+
.collect(),
|
|
131
|
+
)),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Derives parent/child edges from parsed commits.
|
|
136
|
+
pub(crate) fn commit_edges(&self, commits: &[CommitGraphCommit]) -> Vec<CommitGraphEdge> {
|
|
137
|
+
commits
|
|
138
|
+
.iter()
|
|
139
|
+
.flat_map(|commit| {
|
|
140
|
+
commit.parent_commit_ids.iter().enumerate().map(
|
|
141
|
+
|(parent_order, parent_commit_id)| CommitGraphEdge {
|
|
142
|
+
parent_commit_id: parent_commit_id.clone(),
|
|
143
|
+
child_commit_id: commit.commit_id.clone(),
|
|
144
|
+
parent_order: parent_order as u32,
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
.collect()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Returns canonical changes reachable from `start_commit_id`.
|
|
152
|
+
///
|
|
153
|
+
/// This is the primitive history API. It reports the commit/depth where
|
|
154
|
+
/// each matching canonical change was introduced or adopted during graph
|
|
155
|
+
/// traversal and leaves row shaping to callers such as SQL providers.
|
|
156
|
+
pub(crate) async fn change_history_from_commit(
|
|
157
|
+
&mut self,
|
|
158
|
+
start_commit_id: &str,
|
|
159
|
+
request: &CommitGraphChangeHistoryRequest,
|
|
160
|
+
) -> Result<Vec<CommitGraphChangeHistoryEntry>, LixError> {
|
|
161
|
+
let commits = self.reachable_commits(start_commit_id).await?;
|
|
162
|
+
let mut entries = Vec::new();
|
|
163
|
+
let mut seen_change_ids = BTreeSet::new();
|
|
164
|
+
|
|
165
|
+
for reachable in commits {
|
|
166
|
+
if !depth_matches(reachable.depth, request) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let commit_id = reachable.commit.commit_id;
|
|
171
|
+
for change_id in reachable.commit.change_ids {
|
|
172
|
+
if !seen_change_ids.insert(change_id.clone()) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let change = self
|
|
176
|
+
.load_member_canonical_change(&change_id, &commit_id)
|
|
177
|
+
.await?;
|
|
178
|
+
if change_matches_history_request(&change.record, request) {
|
|
179
|
+
entries.push(CommitGraphChangeHistoryEntry {
|
|
180
|
+
located_change: change,
|
|
181
|
+
observed_commit_id: commit_id.clone(),
|
|
182
|
+
start_commit_id: start_commit_id.to_string(),
|
|
183
|
+
depth: reachable.depth,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Ok(entries)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async fn load_member_canonical_change(
|
|
193
|
+
&mut self,
|
|
194
|
+
change_id: &str,
|
|
195
|
+
source_commit_id: &str,
|
|
196
|
+
) -> Result<LocatedChange, LixError> {
|
|
197
|
+
let change_ids = vec![change_id.to_string()];
|
|
198
|
+
self.load_canonical_changes(&change_ids)
|
|
199
|
+
.await?
|
|
200
|
+
.into_iter()
|
|
201
|
+
.next()
|
|
202
|
+
.flatten()
|
|
203
|
+
.ok_or_else(|| {
|
|
204
|
+
LixError::new(
|
|
205
|
+
"LIX_ERROR_UNKNOWN",
|
|
206
|
+
format!(
|
|
207
|
+
"commit_graph commit '{source_commit_id}' references missing change '{change_id}'"
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async fn graph_commit_from_store_commit(
|
|
214
|
+
&mut self,
|
|
215
|
+
commit: Commit,
|
|
216
|
+
) -> Result<CommitGraphCommit, LixError> {
|
|
217
|
+
let change_ids = self.load_commit_change_ids(&commit).await?;
|
|
218
|
+
Ok(commit_graph_commit_from_store_commit(commit, change_ids)?)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async fn load_commit_change_ids(&self, commit: &Commit) -> Result<Vec<String>, LixError> {
|
|
222
|
+
let mut change_ids = Vec::new();
|
|
223
|
+
for pack_id in 0..commit.change_pack_count {
|
|
224
|
+
let Some(changes) = self
|
|
225
|
+
.commit_store_reader
|
|
226
|
+
.load_change_pack(&commit.id, pack_id)
|
|
227
|
+
.await?
|
|
228
|
+
else {
|
|
229
|
+
return Err(missing_pack_error("change", &commit.id, pack_id));
|
|
230
|
+
};
|
|
231
|
+
change_ids.extend(changes.into_iter().map(|change| change.id));
|
|
232
|
+
}
|
|
233
|
+
for pack_id in 0..commit.membership_pack_count {
|
|
234
|
+
let Some(members) = self
|
|
235
|
+
.commit_store_reader
|
|
236
|
+
.load_membership_pack(&commit.id, pack_id)
|
|
237
|
+
.await?
|
|
238
|
+
else {
|
|
239
|
+
return Err(missing_pack_error("membership", &commit.id, pack_id));
|
|
240
|
+
};
|
|
241
|
+
change_ids.extend(members.into_iter().map(|locator| locator.change_id));
|
|
242
|
+
}
|
|
243
|
+
Ok(change_ids)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async fn load_canonical_changes(
|
|
247
|
+
&self,
|
|
248
|
+
change_ids: &[String],
|
|
249
|
+
) -> Result<Vec<Option<LocatedChange>>, LixError> {
|
|
250
|
+
self.commit_store_reader
|
|
251
|
+
.load_located_changes(change_ids)
|
|
252
|
+
.await
|
|
253
|
+
.map(|changes| {
|
|
254
|
+
changes
|
|
255
|
+
.into_iter()
|
|
256
|
+
.map(|located| {
|
|
257
|
+
located.map(|located| LocatedChange {
|
|
258
|
+
record: canonical_change_from_store_change(located.record),
|
|
259
|
+
source_commit_id: located.source_commit_id,
|
|
260
|
+
source_pack_id: located.source_pack_id,
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
.collect()
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[async_trait::async_trait]
|
|
269
|
+
impl<S> CommitGraphReader for CommitGraphStoreReader<S>
|
|
270
|
+
where
|
|
271
|
+
S: StorageReader,
|
|
272
|
+
{
|
|
273
|
+
async fn load_commit(
|
|
274
|
+
&mut self,
|
|
275
|
+
commit_id: &str,
|
|
276
|
+
) -> Result<Option<CommitGraphCommit>, LixError> {
|
|
277
|
+
CommitGraphStoreReader::load_commit(self, commit_id).await
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
281
|
+
CommitGraphStoreReader::all_commits(self).await
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async fn reachable_commits(
|
|
285
|
+
&mut self,
|
|
286
|
+
head_commit_id: &str,
|
|
287
|
+
) -> Result<Vec<ReachableCommitGraphCommit>, LixError> {
|
|
288
|
+
CommitGraphStoreReader::reachable_commits(self, head_commit_id).await
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async fn best_common_ancestors(
|
|
292
|
+
&mut self,
|
|
293
|
+
left_commit_id: &str,
|
|
294
|
+
right_commit_id: &str,
|
|
295
|
+
) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
296
|
+
CommitGraphStoreReader::best_common_ancestors(self, left_commit_id, right_commit_id).await
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async fn merge_base(
|
|
300
|
+
&mut self,
|
|
301
|
+
left_commit_id: &str,
|
|
302
|
+
right_commit_id: &str,
|
|
303
|
+
) -> Result<CommitGraphCommit, LixError> {
|
|
304
|
+
CommitGraphStoreReader::merge_base(self, left_commit_id, right_commit_id).await
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fn commit_edges(&self, commits: &[CommitGraphCommit]) -> Vec<CommitGraphEdge> {
|
|
308
|
+
CommitGraphStoreReader::commit_edges(self, commits)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async fn change_history_from_commit(
|
|
312
|
+
&mut self,
|
|
313
|
+
start_commit_id: &str,
|
|
314
|
+
request: &CommitGraphChangeHistoryRequest,
|
|
315
|
+
) -> Result<Vec<CommitGraphChangeHistoryEntry>, LixError> {
|
|
316
|
+
CommitGraphStoreReader::change_history_from_commit(self, start_commit_id, request).await
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fn depth_matches(depth: u32, request: &CommitGraphChangeHistoryRequest) -> bool {
|
|
321
|
+
request.min_depth.map_or(true, |min| depth >= min)
|
|
322
|
+
&& request.max_depth.map_or(true, |max| depth <= max)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fn change_matches_history_request(
|
|
326
|
+
change: &Change,
|
|
327
|
+
request: &CommitGraphChangeHistoryRequest,
|
|
328
|
+
) -> bool {
|
|
329
|
+
(request.include_tombstones || change.snapshot_ref.is_some())
|
|
330
|
+
&& (request.entity_ids.is_empty() || request.entity_ids.contains(&change.entity_id))
|
|
331
|
+
&& (request.schema_keys.is_empty() || request.schema_keys.contains(&change.schema_key))
|
|
332
|
+
&& (request.file_ids.is_empty()
|
|
333
|
+
|| change
|
|
334
|
+
.file_id
|
|
335
|
+
.as_ref()
|
|
336
|
+
.is_some_and(|file_id| request.file_ids.contains(file_id)))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn commit_graph_commit_from_store_commit(
|
|
340
|
+
commit: Commit,
|
|
341
|
+
change_ids: Vec<String>,
|
|
342
|
+
) -> Result<CommitGraphCommit, LixError> {
|
|
343
|
+
let change = commit_header_canonical_change(commit.clone());
|
|
344
|
+
Ok(CommitGraphCommit {
|
|
345
|
+
canonical_change: change.clone(),
|
|
346
|
+
change,
|
|
347
|
+
commit_id: commit.id,
|
|
348
|
+
change_ids,
|
|
349
|
+
author_account_ids: commit.author_account_ids,
|
|
350
|
+
parent_commit_ids: commit.parent_ids,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fn commit_header_canonical_change(commit: Commit) -> Change {
|
|
355
|
+
Change {
|
|
356
|
+
id: commit.change_id,
|
|
357
|
+
entity_id: EntityIdentity::single(&commit.id),
|
|
358
|
+
schema_key: COMMIT_SCHEMA_KEY.to_string(),
|
|
359
|
+
file_id: None,
|
|
360
|
+
snapshot_ref: None,
|
|
361
|
+
metadata_ref: None,
|
|
362
|
+
created_at: commit.created_at,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn canonical_change_from_store_change(change: Change) -> Change {
|
|
367
|
+
Change {
|
|
368
|
+
id: change.id,
|
|
369
|
+
entity_id: change.entity_id,
|
|
370
|
+
schema_key: change.schema_key,
|
|
371
|
+
file_id: change.file_id,
|
|
372
|
+
snapshot_ref: change.snapshot_ref,
|
|
373
|
+
metadata_ref: change.metadata_ref,
|
|
374
|
+
created_at: change.created_at,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fn missing_pack_error(label: &str, commit_id: &str, pack_id: u32) -> LixError {
|
|
379
|
+
LixError::new(
|
|
380
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
381
|
+
format!("commit_graph missing {label} pack ({commit_id}, {pack_id})"),
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[cfg(test)]
|
|
386
|
+
mod tests {
|
|
387
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
388
|
+
use std::sync::Arc;
|
|
389
|
+
|
|
390
|
+
use crate::backend::testing::UnitTestBackend;
|
|
391
|
+
use crate::commit_graph::{CommitGraphChangeHistoryRequest, CommitGraphContext};
|
|
392
|
+
use crate::commit_store::{
|
|
393
|
+
Change, ChangeLocator, ChangeRef, CommitDraftRef, CommitStoreContext,
|
|
394
|
+
};
|
|
395
|
+
use crate::storage::{StorageContext, StorageWriteSet};
|
|
396
|
+
|
|
397
|
+
#[tokio::test]
|
|
398
|
+
async fn load_commit_parses_commit_snapshot() {
|
|
399
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
400
|
+
let storage = StorageContext::new(backend.clone());
|
|
401
|
+
append_changes(
|
|
402
|
+
storage.clone(),
|
|
403
|
+
&[commit_change(
|
|
404
|
+
"commit-1-change",
|
|
405
|
+
"commit-1",
|
|
406
|
+
&["change-1", "change-2"],
|
|
407
|
+
&["parent-1"],
|
|
408
|
+
)],
|
|
409
|
+
)
|
|
410
|
+
.await;
|
|
411
|
+
|
|
412
|
+
let graph = CommitGraphContext::new();
|
|
413
|
+
let mut reader = graph.reader(storage);
|
|
414
|
+
let commit = reader
|
|
415
|
+
.load_commit("commit-1")
|
|
416
|
+
.await
|
|
417
|
+
.expect("commit load should succeed")
|
|
418
|
+
.expect("commit should exist");
|
|
419
|
+
|
|
420
|
+
assert_eq!(commit.commit_id, "commit-1");
|
|
421
|
+
assert_eq!(commit.change_ids, vec!["change-1", "change-2"]);
|
|
422
|
+
assert_eq!(commit.parent_commit_ids, vec!["parent-1"]);
|
|
423
|
+
assert_eq!(commit.change.id, "commit-1-change");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[tokio::test]
|
|
427
|
+
async fn load_commit_returns_none_for_missing_commit() {
|
|
428
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
429
|
+
let storage = StorageContext::new(backend.clone());
|
|
430
|
+
let graph = CommitGraphContext::new();
|
|
431
|
+
let mut reader = graph.reader(storage);
|
|
432
|
+
|
|
433
|
+
let commit = reader
|
|
434
|
+
.load_commit("missing")
|
|
435
|
+
.await
|
|
436
|
+
.expect("commit load should succeed");
|
|
437
|
+
|
|
438
|
+
assert_eq!(commit, None);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[tokio::test]
|
|
442
|
+
async fn all_commits_returns_parsed_commits_sorted_by_id() {
|
|
443
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
444
|
+
let storage = StorageContext::new(backend.clone());
|
|
445
|
+
append_changes(
|
|
446
|
+
storage.clone(),
|
|
447
|
+
&[
|
|
448
|
+
commit_change("commit-b-change", "commit-b", &[], &[]),
|
|
449
|
+
entity_change("change-1", "entity-1", "example", "{}"),
|
|
450
|
+
commit_change("commit-a-change", "commit-a", &[], &[]),
|
|
451
|
+
],
|
|
452
|
+
)
|
|
453
|
+
.await;
|
|
454
|
+
|
|
455
|
+
let graph = CommitGraphContext::new();
|
|
456
|
+
let mut reader = graph.reader(storage);
|
|
457
|
+
let commits = reader
|
|
458
|
+
.all_commits()
|
|
459
|
+
.await
|
|
460
|
+
.expect("commit scan should succeed");
|
|
461
|
+
|
|
462
|
+
assert_eq!(
|
|
463
|
+
commits
|
|
464
|
+
.iter()
|
|
465
|
+
.map(|commit| commit.commit_id.as_str())
|
|
466
|
+
.collect::<Vec<_>>(),
|
|
467
|
+
vec!["commit-a", "commit-b"]
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[tokio::test]
|
|
472
|
+
async fn commit_edges_are_derived_from_parent_commit_ids() {
|
|
473
|
+
let graph = CommitGraphContext::new();
|
|
474
|
+
let reader = graph.reader(StorageContext::new(Arc::new(UnitTestBackend::new())));
|
|
475
|
+
let commits = vec![parsed_commit(
|
|
476
|
+
"commit-head",
|
|
477
|
+
&[],
|
|
478
|
+
&["commit-left", "commit-right"],
|
|
479
|
+
)];
|
|
480
|
+
|
|
481
|
+
let edges = reader.commit_edges(&commits);
|
|
482
|
+
|
|
483
|
+
assert_eq!(
|
|
484
|
+
edges
|
|
485
|
+
.iter()
|
|
486
|
+
.map(|edge| (
|
|
487
|
+
edge.parent_commit_id.as_str(),
|
|
488
|
+
edge.child_commit_id.as_str(),
|
|
489
|
+
edge.parent_order,
|
|
490
|
+
))
|
|
491
|
+
.collect::<Vec<_>>(),
|
|
492
|
+
vec![
|
|
493
|
+
("commit-left", "commit-head", 0),
|
|
494
|
+
("commit-right", "commit-head", 1)
|
|
495
|
+
]
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#[tokio::test]
|
|
500
|
+
async fn change_history_from_commit_reports_matching_canonical_changes_with_depth() {
|
|
501
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
502
|
+
let storage = StorageContext::new(backend.clone());
|
|
503
|
+
append_changes(
|
|
504
|
+
storage.clone(),
|
|
505
|
+
&[
|
|
506
|
+
entity_change("change-root", "entity-root", "test_schema", "{}"),
|
|
507
|
+
entity_change("change-head", "entity-head", "test_schema", "{}"),
|
|
508
|
+
commit_change("commit-root-change", "commit-root", &["change-root"], &[]),
|
|
509
|
+
commit_change(
|
|
510
|
+
"commit-head-change",
|
|
511
|
+
"commit-head",
|
|
512
|
+
&["change-head"],
|
|
513
|
+
&["commit-root"],
|
|
514
|
+
),
|
|
515
|
+
],
|
|
516
|
+
)
|
|
517
|
+
.await;
|
|
518
|
+
|
|
519
|
+
let graph = CommitGraphContext::new();
|
|
520
|
+
let mut reader = graph.reader(storage);
|
|
521
|
+
let history = reader
|
|
522
|
+
.change_history_from_commit(
|
|
523
|
+
"commit-head",
|
|
524
|
+
&CommitGraphChangeHistoryRequest {
|
|
525
|
+
schema_keys: vec!["test_schema".to_string()],
|
|
526
|
+
include_tombstones: true,
|
|
527
|
+
..CommitGraphChangeHistoryRequest::default()
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
.await
|
|
531
|
+
.expect("history should resolve");
|
|
532
|
+
|
|
533
|
+
assert_eq!(
|
|
534
|
+
history
|
|
535
|
+
.iter()
|
|
536
|
+
.map(|entry| (
|
|
537
|
+
entry.located_change.record.id.as_str(),
|
|
538
|
+
entry.observed_commit_id.as_str(),
|
|
539
|
+
entry.start_commit_id.as_str(),
|
|
540
|
+
entry.depth
|
|
541
|
+
))
|
|
542
|
+
.collect::<Vec<_>>(),
|
|
543
|
+
vec![
|
|
544
|
+
("change-head", "commit-head", "commit-head", 0),
|
|
545
|
+
("change-root", "commit-root", "commit-head", 1),
|
|
546
|
+
]
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[tokio::test]
|
|
551
|
+
async fn change_history_from_commit_filters_depth_entity_file_and_tombstones() {
|
|
552
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
553
|
+
let storage = StorageContext::new(backend.clone());
|
|
554
|
+
append_changes(
|
|
555
|
+
storage.clone(),
|
|
556
|
+
&[
|
|
557
|
+
entity_change_with_file(
|
|
558
|
+
"change-file-a",
|
|
559
|
+
"entity-1",
|
|
560
|
+
"test_schema",
|
|
561
|
+
Some("file-a"),
|
|
562
|
+
"{}",
|
|
563
|
+
),
|
|
564
|
+
entity_tombstone("change-tombstone", "entity-1", "test_schema"),
|
|
565
|
+
entity_change_with_file(
|
|
566
|
+
"change-file-b",
|
|
567
|
+
"entity-2",
|
|
568
|
+
"test_schema",
|
|
569
|
+
Some("file-b"),
|
|
570
|
+
"{}",
|
|
571
|
+
),
|
|
572
|
+
commit_change("commit-root-change", "commit-root", &["change-file-a"], &[]),
|
|
573
|
+
commit_change(
|
|
574
|
+
"commit-head-change",
|
|
575
|
+
"commit-head",
|
|
576
|
+
&["change-tombstone", "change-file-b"],
|
|
577
|
+
&["commit-root"],
|
|
578
|
+
),
|
|
579
|
+
],
|
|
580
|
+
)
|
|
581
|
+
.await;
|
|
582
|
+
|
|
583
|
+
let graph = CommitGraphContext::new();
|
|
584
|
+
let mut reader = graph.reader(storage);
|
|
585
|
+
let history = reader
|
|
586
|
+
.change_history_from_commit(
|
|
587
|
+
"commit-head",
|
|
588
|
+
&CommitGraphChangeHistoryRequest {
|
|
589
|
+
entity_ids: vec![crate::entity_identity::EntityIdentity::single("entity-1")],
|
|
590
|
+
file_ids: vec!["file-a".to_string()],
|
|
591
|
+
min_depth: Some(1),
|
|
592
|
+
max_depth: Some(1),
|
|
593
|
+
include_tombstones: false,
|
|
594
|
+
..CommitGraphChangeHistoryRequest::default()
|
|
595
|
+
},
|
|
596
|
+
)
|
|
597
|
+
.await
|
|
598
|
+
.expect("history should resolve");
|
|
599
|
+
|
|
600
|
+
assert_eq!(history.len(), 1);
|
|
601
|
+
assert_eq!(history[0].located_change.record.id, "change-file-a");
|
|
602
|
+
assert_eq!(history[0].depth, 1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
#[tokio::test]
|
|
606
|
+
async fn change_history_from_commit_includes_tombstones_when_requested() {
|
|
607
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
608
|
+
let storage = StorageContext::new(backend.clone());
|
|
609
|
+
append_changes(
|
|
610
|
+
storage.clone(),
|
|
611
|
+
&[
|
|
612
|
+
entity_tombstone("change-deleted", "entity-1", "test_schema"),
|
|
613
|
+
commit_change(
|
|
614
|
+
"commit-head-change",
|
|
615
|
+
"commit-head",
|
|
616
|
+
&["change-deleted"],
|
|
617
|
+
&[],
|
|
618
|
+
),
|
|
619
|
+
],
|
|
620
|
+
)
|
|
621
|
+
.await;
|
|
622
|
+
|
|
623
|
+
let graph = CommitGraphContext::new();
|
|
624
|
+
let mut reader = graph.reader(storage);
|
|
625
|
+
let hidden = reader
|
|
626
|
+
.change_history_from_commit("commit-head", &CommitGraphChangeHistoryRequest::default())
|
|
627
|
+
.await
|
|
628
|
+
.expect("history should resolve");
|
|
629
|
+
let visible = reader
|
|
630
|
+
.change_history_from_commit(
|
|
631
|
+
"commit-head",
|
|
632
|
+
&CommitGraphChangeHistoryRequest {
|
|
633
|
+
include_tombstones: true,
|
|
634
|
+
..CommitGraphChangeHistoryRequest::default()
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
.await
|
|
638
|
+
.expect("history should resolve");
|
|
639
|
+
|
|
640
|
+
assert!(hidden.is_empty());
|
|
641
|
+
assert_eq!(visible.len(), 1);
|
|
642
|
+
assert_eq!(visible[0].located_change.record.id, "change-deleted");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
#[derive(Clone)]
|
|
646
|
+
struct TestChange {
|
|
647
|
+
change: Change,
|
|
648
|
+
commit_change_ids: Vec<String>,
|
|
649
|
+
parent_commit_ids: Vec<String>,
|
|
650
|
+
author_account_ids: Vec<String>,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
impl TestChange {
|
|
654
|
+
fn commit(
|
|
655
|
+
change_id: &str,
|
|
656
|
+
commit_id: &str,
|
|
657
|
+
change_ids: &[&str],
|
|
658
|
+
parent_commit_ids: &[&str],
|
|
659
|
+
) -> Self {
|
|
660
|
+
Self {
|
|
661
|
+
change: Change {
|
|
662
|
+
id: change_id.to_string(),
|
|
663
|
+
entity_id: crate::entity_identity::EntityIdentity::single(commit_id),
|
|
664
|
+
schema_key: super::COMMIT_SCHEMA_KEY.to_string(),
|
|
665
|
+
file_id: None,
|
|
666
|
+
snapshot_ref: None,
|
|
667
|
+
metadata_ref: None,
|
|
668
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
669
|
+
},
|
|
670
|
+
commit_change_ids: change_ids.iter().map(|id| id.to_string()).collect(),
|
|
671
|
+
parent_commit_ids: parent_commit_ids.iter().map(|id| id.to_string()).collect(),
|
|
672
|
+
author_account_ids: Vec::new(),
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn entity(
|
|
677
|
+
change_id: &str,
|
|
678
|
+
entity_id: &str,
|
|
679
|
+
schema_key: &str,
|
|
680
|
+
file_id: Option<&str>,
|
|
681
|
+
snapshot_content: Option<&str>,
|
|
682
|
+
created_at: &str,
|
|
683
|
+
) -> Self {
|
|
684
|
+
Self {
|
|
685
|
+
change: Change {
|
|
686
|
+
id: change_id.to_string(),
|
|
687
|
+
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
688
|
+
schema_key: schema_key.to_string(),
|
|
689
|
+
file_id: file_id.map(str::to_string),
|
|
690
|
+
snapshot_ref: snapshot_content.map(|content| {
|
|
691
|
+
crate::json_store::JsonRef::from_hash(blake3::hash(content.as_bytes()))
|
|
692
|
+
}),
|
|
693
|
+
metadata_ref: None,
|
|
694
|
+
created_at: created_at.to_string(),
|
|
695
|
+
},
|
|
696
|
+
commit_change_ids: Vec::new(),
|
|
697
|
+
parent_commit_ids: Vec::new(),
|
|
698
|
+
author_account_ids: Vec::new(),
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
fn is_commit(&self) -> bool {
|
|
703
|
+
self.change.schema_key == super::COMMIT_SCHEMA_KEY
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async fn append_changes(storage: StorageContext, changes: &[TestChange]) {
|
|
708
|
+
let mut tx = storage
|
|
709
|
+
.begin_write_transaction()
|
|
710
|
+
.await
|
|
711
|
+
.expect("transaction should open");
|
|
712
|
+
let mut writes = StorageWriteSet::new();
|
|
713
|
+
let canonical_changes = changes
|
|
714
|
+
.iter()
|
|
715
|
+
.filter(|change| !change.is_commit())
|
|
716
|
+
.map(|change| change.change.clone())
|
|
717
|
+
.collect::<Vec<_>>();
|
|
718
|
+
let changes_by_id: BTreeMap<&str, &Change> = canonical_changes
|
|
719
|
+
.iter()
|
|
720
|
+
.map(|change| (change.id.as_str(), change))
|
|
721
|
+
.collect::<BTreeMap<_, _>>();
|
|
722
|
+
let mut authored_change_ids = BTreeSet::new();
|
|
723
|
+
let commit_store = CommitStoreContext::new();
|
|
724
|
+
for change in changes.iter().filter(|change| change.is_commit()) {
|
|
725
|
+
let commit = crate::commit_graph::CommitGraphCommit {
|
|
726
|
+
canonical_change: change.change.clone(),
|
|
727
|
+
change: change.change.clone(),
|
|
728
|
+
commit_id: change
|
|
729
|
+
.change
|
|
730
|
+
.entity_id
|
|
731
|
+
.as_single_string()
|
|
732
|
+
.expect("commit fixture should use single entity id")
|
|
733
|
+
.to_string(),
|
|
734
|
+
change_ids: change.commit_change_ids.clone(),
|
|
735
|
+
author_account_ids: change.author_account_ids.clone(),
|
|
736
|
+
parent_commit_ids: change.parent_commit_ids.clone(),
|
|
737
|
+
};
|
|
738
|
+
let parent_commit_ids = commit.parent_commit_ids.clone();
|
|
739
|
+
let author_account_ids = commit.author_account_ids.clone();
|
|
740
|
+
let commit_draft = CommitDraftRef {
|
|
741
|
+
id: &commit.commit_id,
|
|
742
|
+
change_id: &commit.canonical_change.id,
|
|
743
|
+
parent_ids: &parent_commit_ids,
|
|
744
|
+
author_account_ids: &author_account_ids,
|
|
745
|
+
created_at: &commit.canonical_change.created_at,
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
let mut authored_changes = Vec::new();
|
|
749
|
+
let mut adopted_changes = Vec::new();
|
|
750
|
+
let mut corrupt_missing_members = Vec::new();
|
|
751
|
+
for change_id in &commit.change_ids {
|
|
752
|
+
if let Some(change) = changes_by_id.get(change_id.as_str()) {
|
|
753
|
+
if authored_change_ids.insert(change_id.clone()) {
|
|
754
|
+
authored_changes.push(change_ref_from_canonical(change.as_ref()));
|
|
755
|
+
} else {
|
|
756
|
+
adopted_changes.push(change_ref_from_canonical(change.as_ref()));
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
corrupt_missing_members.push(change_id.clone());
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if corrupt_missing_members.is_empty() {
|
|
764
|
+
commit_store
|
|
765
|
+
.writer(tx.as_mut(), &mut writes)
|
|
766
|
+
.stage_commit_draft(commit_draft, authored_changes, adopted_changes)
|
|
767
|
+
.await
|
|
768
|
+
.expect("commit-store append should succeed");
|
|
769
|
+
} else {
|
|
770
|
+
crate::commit_store::storage::stage_commit(
|
|
771
|
+
&mut writes,
|
|
772
|
+
commit_draft,
|
|
773
|
+
authored_changes,
|
|
774
|
+
corrupt_missing_members
|
|
775
|
+
.into_iter()
|
|
776
|
+
.map(|change_id| ChangeLocator {
|
|
777
|
+
source_commit_id: "missing-source-commit".to_string(),
|
|
778
|
+
source_pack_id: 0,
|
|
779
|
+
source_ordinal: 0,
|
|
780
|
+
change_id,
|
|
781
|
+
})
|
|
782
|
+
.collect(),
|
|
783
|
+
)
|
|
784
|
+
.expect("corrupt commit-store fixture should stage");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
writes
|
|
788
|
+
.apply(&mut tx.as_mut())
|
|
789
|
+
.await
|
|
790
|
+
.expect("writes should apply");
|
|
791
|
+
tx.commit().await.expect("commit should succeed");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
fn change_ref_from_canonical<'a>(change: crate::commit_store::ChangeRef<'a>) -> ChangeRef<'a> {
|
|
795
|
+
ChangeRef {
|
|
796
|
+
id: change.id,
|
|
797
|
+
entity_id: change.entity_id,
|
|
798
|
+
schema_key: change.schema_key,
|
|
799
|
+
file_id: change.file_id,
|
|
800
|
+
snapshot_ref: change.snapshot_ref,
|
|
801
|
+
metadata_ref: change.metadata_ref,
|
|
802
|
+
created_at: change.created_at,
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fn commit_change(
|
|
807
|
+
change_id: &str,
|
|
808
|
+
commit_id: &str,
|
|
809
|
+
change_ids: &[&str],
|
|
810
|
+
parent_commit_ids: &[&str],
|
|
811
|
+
) -> TestChange {
|
|
812
|
+
TestChange::commit(change_id, commit_id, change_ids, parent_commit_ids)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
fn parsed_commit(
|
|
816
|
+
commit_id: &str,
|
|
817
|
+
change_ids: &[&str],
|
|
818
|
+
parent_commit_ids: &[&str],
|
|
819
|
+
) -> crate::commit_graph::CommitGraphCommit {
|
|
820
|
+
let fixture = commit_change(
|
|
821
|
+
&format!("{commit_id}-change"),
|
|
822
|
+
commit_id,
|
|
823
|
+
change_ids,
|
|
824
|
+
parent_commit_ids,
|
|
825
|
+
);
|
|
826
|
+
crate::commit_graph::CommitGraphCommit {
|
|
827
|
+
canonical_change: fixture.change.clone(),
|
|
828
|
+
change: fixture.change,
|
|
829
|
+
commit_id: commit_id.to_string(),
|
|
830
|
+
change_ids: change_ids
|
|
831
|
+
.iter()
|
|
832
|
+
.map(|change_id| change_id.to_string())
|
|
833
|
+
.collect(),
|
|
834
|
+
author_account_ids: Vec::new(),
|
|
835
|
+
parent_commit_ids: parent_commit_ids
|
|
836
|
+
.iter()
|
|
837
|
+
.map(|parent_id| parent_id.to_string())
|
|
838
|
+
.collect(),
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
fn entity_change(
|
|
843
|
+
change_id: &str,
|
|
844
|
+
entity_id: &str,
|
|
845
|
+
schema_key: &str,
|
|
846
|
+
snapshot_content: &str,
|
|
847
|
+
) -> TestChange {
|
|
848
|
+
entity_change_at(
|
|
849
|
+
change_id,
|
|
850
|
+
entity_id,
|
|
851
|
+
schema_key,
|
|
852
|
+
snapshot_content,
|
|
853
|
+
"2026-01-01T00:00:00Z",
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
fn entity_change_at(
|
|
858
|
+
change_id: &str,
|
|
859
|
+
entity_id: &str,
|
|
860
|
+
schema_key: &str,
|
|
861
|
+
snapshot_content: &str,
|
|
862
|
+
created_at: &str,
|
|
863
|
+
) -> TestChange {
|
|
864
|
+
TestChange::entity(
|
|
865
|
+
change_id,
|
|
866
|
+
entity_id,
|
|
867
|
+
schema_key,
|
|
868
|
+
None,
|
|
869
|
+
Some(snapshot_content),
|
|
870
|
+
created_at,
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
fn entity_change_with_file(
|
|
875
|
+
change_id: &str,
|
|
876
|
+
entity_id: &str,
|
|
877
|
+
schema_key: &str,
|
|
878
|
+
file_id: Option<&str>,
|
|
879
|
+
snapshot_content: &str,
|
|
880
|
+
) -> TestChange {
|
|
881
|
+
TestChange::entity(
|
|
882
|
+
change_id,
|
|
883
|
+
entity_id,
|
|
884
|
+
schema_key,
|
|
885
|
+
file_id,
|
|
886
|
+
Some(snapshot_content),
|
|
887
|
+
"2026-01-01T00:00:00Z",
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
fn entity_tombstone(change_id: &str, entity_id: &str, schema_key: &str) -> TestChange {
|
|
892
|
+
TestChange::entity(
|
|
893
|
+
change_id,
|
|
894
|
+
entity_id,
|
|
895
|
+
schema_key,
|
|
896
|
+
None,
|
|
897
|
+
None,
|
|
898
|
+
"2026-01-02T00:00:00Z",
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
}
|