@lix-js/sdk 0.6.0-preview.0 → 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/README.md +9 -0
- package/SKILL.md +468 -0
- package/dist/engine-wasm/index.d.ts +15 -11
- package/dist/engine-wasm/index.js +105 -38
- package/dist/engine-wasm/wasm/lix_engine.d.ts +14 -2
- package/dist/engine-wasm/wasm/lix_engine.js +18 -17
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +2 -1
- package/dist/generated/builtin-schemas.d.ts +31 -41
- package/dist/generated/builtin-schemas.js +52 -56
- package/dist/open-lix.d.ts +141 -24
- package/dist/open-lix.js +199 -35
- 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 -63
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
use crate::binary_cas::BinaryCasContext;
|
|
4
|
+
use crate::changelog::{CanonicalChange, ChangelogContext};
|
|
5
|
+
use crate::functions::FunctionContext;
|
|
6
|
+
use crate::json_store::{JsonRef, JsonStoreContext, JsonStoreWriter};
|
|
7
|
+
use crate::live_state::{LiveStateContext, LiveStateRow};
|
|
8
|
+
use crate::storage::{StorageReader, StorageWriteSet, StorageWriteTransaction};
|
|
9
|
+
use crate::transaction::staging::StagedWriteSet;
|
|
10
|
+
use crate::transaction::types::{StagedAdoptedStateRow, StagedCommitMembers, StagedStateRow};
|
|
11
|
+
use crate::version::{VersionContext, VersionRefReader};
|
|
12
|
+
use crate::GLOBAL_VERSION_ID;
|
|
13
|
+
use crate::{serialize_row_metadata, LixError, RowMetadata};
|
|
14
|
+
|
|
15
|
+
/// Commits transaction-staged rows into durable tracked and untracked stores.
|
|
16
|
+
///
|
|
17
|
+
/// Providers decode DataFusion DML into hydrated `StagedStateRow`s. Untracked
|
|
18
|
+
/// rows are durable local overlay state and bypass changelog/commit rows.
|
|
19
|
+
/// Tracked rows receive normal `lix_commit` rows, append canonical changelog
|
|
20
|
+
/// facts, then update the live-state serving projection. The tracked side of
|
|
21
|
+
/// that projection is a prolly root keyed by the new commit id.
|
|
22
|
+
pub(crate) async fn commit_staged_writes(
|
|
23
|
+
binary_cas: &BinaryCasContext,
|
|
24
|
+
changelog: &ChangelogContext,
|
|
25
|
+
live_state: &LiveStateContext,
|
|
26
|
+
version_ctx: &VersionContext,
|
|
27
|
+
runtime_functions: Option<&FunctionContext>,
|
|
28
|
+
transaction: &mut (impl StorageWriteTransaction + ?Sized),
|
|
29
|
+
staged_writes: StagedWriteSet,
|
|
30
|
+
) -> Result<(), LixError> {
|
|
31
|
+
let mut writes = StorageWriteSet::new();
|
|
32
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
33
|
+
|
|
34
|
+
if !staged_writes.file_data_writes.is_empty() {
|
|
35
|
+
let mut blob_writer = binary_cas.writer(&mut writes);
|
|
36
|
+
for write in &staged_writes.file_data_writes {
|
|
37
|
+
blob_writer.stage_bytes(&write.data)?;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let (mut changelog_rows, untracked_rows): (Vec<_>, Vec<_>) = staged_writes
|
|
42
|
+
.state_rows
|
|
43
|
+
.into_iter()
|
|
44
|
+
.partition(|row| !row.untracked);
|
|
45
|
+
let adopted_rows = staged_writes.adopted_rows;
|
|
46
|
+
let finalized = finalize_commit_rows(
|
|
47
|
+
staged_writes.commit_members_by_version,
|
|
48
|
+
staged_writes.extra_commit_parents_by_version,
|
|
49
|
+
version_ctx,
|
|
50
|
+
transaction,
|
|
51
|
+
)
|
|
52
|
+
.await?;
|
|
53
|
+
changelog_rows.extend(finalized.commit_rows);
|
|
54
|
+
let version_heads = finalized.version_heads;
|
|
55
|
+
|
|
56
|
+
if let Some(runtime_functions) = runtime_functions {
|
|
57
|
+
let mut writer = live_state.writer(&mut *transaction);
|
|
58
|
+
runtime_functions
|
|
59
|
+
.stage_persist_if_needed(&mut writer, &mut writes, &mut json_writer)
|
|
60
|
+
.await?;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if changelog_rows.is_empty()
|
|
64
|
+
&& adopted_rows.is_empty()
|
|
65
|
+
&& untracked_rows.is_empty()
|
|
66
|
+
&& version_heads.is_empty()
|
|
67
|
+
&& writes.is_empty()
|
|
68
|
+
{
|
|
69
|
+
return Ok(());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if !changelog_rows.is_empty() {
|
|
73
|
+
let canonical_changes = new_canonical_changes(
|
|
74
|
+
changelog,
|
|
75
|
+
transaction,
|
|
76
|
+
&mut writes,
|
|
77
|
+
&mut json_writer,
|
|
78
|
+
&changelog_rows,
|
|
79
|
+
)
|
|
80
|
+
.await?;
|
|
81
|
+
{
|
|
82
|
+
let mut writer = changelog.writer(&mut writes);
|
|
83
|
+
writer.stage_changes(&canonical_changes)?;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if !adopted_rows.is_empty() {
|
|
87
|
+
validate_adopted_canonical_changes(changelog, transaction, &adopted_rows).await?;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The serving projection is updated in the same backend transaction as the
|
|
91
|
+
// changelog append. Tracked rows become prolly mutations under their owning
|
|
92
|
+
// commit root; untracked rows remain in the separate local overlay store.
|
|
93
|
+
let live_state_rows = changelog_rows
|
|
94
|
+
.into_iter()
|
|
95
|
+
.map(LiveStateRow::from)
|
|
96
|
+
.chain(adopted_rows.into_iter().map(LiveStateRow::from))
|
|
97
|
+
.chain(untracked_rows.into_iter().map(LiveStateRow::from))
|
|
98
|
+
.collect::<Vec<_>>();
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
let mut writer = live_state.writer(&mut *transaction);
|
|
102
|
+
writer
|
|
103
|
+
.stage_rows(&mut writes, &mut json_writer, &live_state_rows)
|
|
104
|
+
.await?;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for version_head in version_heads {
|
|
108
|
+
let canonical_row = version_ctx.canonical_ref_row(
|
|
109
|
+
&mut writes,
|
|
110
|
+
&mut json_writer,
|
|
111
|
+
&version_head.version_id,
|
|
112
|
+
&version_head.commit_id,
|
|
113
|
+
&version_head.timestamp,
|
|
114
|
+
)?;
|
|
115
|
+
version_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row])?;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writes.apply(transaction).await?;
|
|
119
|
+
Ok(())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async fn new_canonical_changes(
|
|
123
|
+
changelog: &ChangelogContext,
|
|
124
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
125
|
+
writes: &mut StorageWriteSet,
|
|
126
|
+
json_writer: &mut JsonStoreWriter,
|
|
127
|
+
rows: &[StagedStateRow],
|
|
128
|
+
) -> Result<Vec<CanonicalChange>, LixError> {
|
|
129
|
+
let reader = changelog.reader(&mut *transaction);
|
|
130
|
+
let mut changes = Vec::new();
|
|
131
|
+
for row in rows {
|
|
132
|
+
let change = canonical_change_from_staged_row(writes, json_writer, row)?;
|
|
133
|
+
match reader.load_change(&change.id).await? {
|
|
134
|
+
Some(existing) => {
|
|
135
|
+
let entity_id = existing
|
|
136
|
+
.entity_id
|
|
137
|
+
.as_string()
|
|
138
|
+
.unwrap_or_else(|_| "<invalid entity_id>".to_string());
|
|
139
|
+
return Err(LixError::new(
|
|
140
|
+
LixError::CODE_UNIQUE,
|
|
141
|
+
format!(
|
|
142
|
+
"canonical change id '{}' already exists with different content for schema '{}' entity '{}'",
|
|
143
|
+
change.id,
|
|
144
|
+
existing.schema_key,
|
|
145
|
+
entity_id
|
|
146
|
+
),
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
None => changes.push(change),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
Ok(changes)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async fn validate_adopted_canonical_changes(
|
|
156
|
+
changelog: &ChangelogContext,
|
|
157
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
158
|
+
rows: &[StagedAdoptedStateRow],
|
|
159
|
+
) -> Result<(), LixError> {
|
|
160
|
+
let mut writes = StorageWriteSet::new();
|
|
161
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
162
|
+
let reader = changelog.reader(&mut *transaction);
|
|
163
|
+
for row in rows {
|
|
164
|
+
let expected = canonical_change_from_adopted_row(&mut writes, &mut json_writer, row)?;
|
|
165
|
+
match reader.load_change(&expected.id).await? {
|
|
166
|
+
Some(existing) if existing == expected => {}
|
|
167
|
+
Some(existing) => {
|
|
168
|
+
let entity_id = existing
|
|
169
|
+
.entity_id
|
|
170
|
+
.as_string()
|
|
171
|
+
.unwrap_or_else(|_| "<invalid entity_id>".to_string());
|
|
172
|
+
return Err(LixError::new(
|
|
173
|
+
LixError::CODE_UNIQUE,
|
|
174
|
+
format!(
|
|
175
|
+
"adopted canonical change id '{}' exists with different content for schema '{}' entity '{}'",
|
|
176
|
+
expected.id, existing.schema_key, entity_id
|
|
177
|
+
),
|
|
178
|
+
));
|
|
179
|
+
}
|
|
180
|
+
None => {
|
|
181
|
+
return Err(LixError::new(
|
|
182
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
183
|
+
format!(
|
|
184
|
+
"adopted canonical change id '{}' does not exist in the changelog",
|
|
185
|
+
expected.id
|
|
186
|
+
),
|
|
187
|
+
));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
Ok(())
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn canonical_change_from_staged_row(
|
|
195
|
+
writes: &mut StorageWriteSet,
|
|
196
|
+
json_writer: &mut JsonStoreWriter,
|
|
197
|
+
row: &StagedStateRow,
|
|
198
|
+
) -> Result<CanonicalChange, LixError> {
|
|
199
|
+
let Some(change_id) = row.change_id.as_ref() else {
|
|
200
|
+
return Err(LixError::new(
|
|
201
|
+
"LIX_ERROR_UNKNOWN",
|
|
202
|
+
"tracked staged row is missing change_id before changelog append",
|
|
203
|
+
));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
Ok(CanonicalChange {
|
|
207
|
+
id: change_id.clone(),
|
|
208
|
+
entity_id: row.entity_id.clone(),
|
|
209
|
+
schema_key: row.schema_key.clone(),
|
|
210
|
+
schema_version: row.schema_version.clone(),
|
|
211
|
+
file_id: row.file_id.clone(),
|
|
212
|
+
snapshot_ref: stage_optional_json(writes, json_writer, row.snapshot_content.as_deref())?,
|
|
213
|
+
metadata_ref: stage_optional_metadata(writes, json_writer, row.metadata.as_ref())?,
|
|
214
|
+
created_at: row.created_at.clone(),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn stage_optional_json(
|
|
219
|
+
writes: &mut StorageWriteSet,
|
|
220
|
+
json_writer: &mut JsonStoreWriter,
|
|
221
|
+
value: Option<&str>,
|
|
222
|
+
) -> Result<Option<JsonRef>, LixError> {
|
|
223
|
+
let Some(value) = value else {
|
|
224
|
+
return Ok(None);
|
|
225
|
+
};
|
|
226
|
+
json_writer.stage_bytes(writes, value.as_bytes()).map(Some)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn stage_optional_metadata(
|
|
230
|
+
writes: &mut StorageWriteSet,
|
|
231
|
+
json_writer: &mut JsonStoreWriter,
|
|
232
|
+
value: Option<&RowMetadata>,
|
|
233
|
+
) -> Result<Option<JsonRef>, LixError> {
|
|
234
|
+
let Some(value) = value else {
|
|
235
|
+
return Ok(None);
|
|
236
|
+
};
|
|
237
|
+
let serialized = serialize_row_metadata(value);
|
|
238
|
+
json_writer
|
|
239
|
+
.stage_bytes(writes, serialized.as_bytes())
|
|
240
|
+
.map(Some)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn canonical_change_from_adopted_row(
|
|
244
|
+
writes: &mut StorageWriteSet,
|
|
245
|
+
json_writer: &mut JsonStoreWriter,
|
|
246
|
+
row: &StagedAdoptedStateRow,
|
|
247
|
+
) -> Result<CanonicalChange, LixError> {
|
|
248
|
+
Ok(CanonicalChange {
|
|
249
|
+
id: row.change_id.clone(),
|
|
250
|
+
entity_id: row.entity_id.clone(),
|
|
251
|
+
schema_key: row.schema_key.clone(),
|
|
252
|
+
schema_version: row.schema_version.clone(),
|
|
253
|
+
file_id: row.file_id.clone(),
|
|
254
|
+
snapshot_ref: stage_optional_json(writes, json_writer, row.snapshot_content.as_deref())?,
|
|
255
|
+
metadata_ref: stage_optional_metadata(writes, json_writer, row.metadata.as_ref())?,
|
|
256
|
+
created_at: row.created_at.clone(),
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Materializes tracked staged membership into `lix_commit` rows.
|
|
261
|
+
///
|
|
262
|
+
/// Staging only accumulates `version_id -> change_ids` because commit ids,
|
|
263
|
+
/// parent heads, and commit-row timestamps belong to transaction finalization.
|
|
264
|
+
/// The `change_ids` list is the ordered set of canonical changes whose effects
|
|
265
|
+
/// the commit introduces relative to its first parent; merge commits may later
|
|
266
|
+
/// populate this list with existing source-parent changes instead of copied
|
|
267
|
+
/// changelog facts.
|
|
268
|
+
/// This function turns those membership sets into normal `StagedStateRow`s with
|
|
269
|
+
/// `schema_key = "lix_commit"`, so the changelog/live_state flush can treat
|
|
270
|
+
/// commit rows exactly like any other staged state row.
|
|
271
|
+
///
|
|
272
|
+
/// Commit finalization output split by durability target.
|
|
273
|
+
///
|
|
274
|
+
/// `commit_rows` are ordinary changelog facts. live_state later projects them
|
|
275
|
+
/// from commit_graph; tracked_state roots do not store commit graph facts.
|
|
276
|
+
///
|
|
277
|
+
/// `version_heads` are moving refs. They are written through `VersionContext`
|
|
278
|
+
/// and must never be appended to changelog.
|
|
279
|
+
struct FinalizedCommitRows {
|
|
280
|
+
commit_rows: Vec<StagedStateRow>,
|
|
281
|
+
version_heads: Vec<PendingVersionHead>,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
struct PendingVersionHead {
|
|
285
|
+
version_id: String,
|
|
286
|
+
commit_id: String,
|
|
287
|
+
timestamp: String,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async fn finalize_commit_rows(
|
|
291
|
+
commit_members_by_version: BTreeMap<String, StagedCommitMembers>,
|
|
292
|
+
extra_commit_parents_by_version: BTreeMap<String, Vec<String>>,
|
|
293
|
+
version_ctx: &VersionContext,
|
|
294
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
295
|
+
) -> Result<FinalizedCommitRows, LixError> {
|
|
296
|
+
let mut commit_rows = Vec::new();
|
|
297
|
+
let mut version_heads = Vec::new();
|
|
298
|
+
|
|
299
|
+
for (version_id, members) in commit_members_by_version {
|
|
300
|
+
if members.is_empty() && !members.allow_empty {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let commit_id = members.commit_id;
|
|
305
|
+
let commit_change_id = members.commit_change_id;
|
|
306
|
+
let change_set_id = members.change_set_id;
|
|
307
|
+
let timestamp = members.created_at;
|
|
308
|
+
let change_ids = members.change_ids.into_iter().collect::<Vec<_>>();
|
|
309
|
+
let parent_commit_ids = version_ctx
|
|
310
|
+
.ref_reader(&mut *transaction)
|
|
311
|
+
.load_head_commit_id(&version_id)
|
|
312
|
+
.await?
|
|
313
|
+
.into_iter()
|
|
314
|
+
.collect::<Vec<_>>();
|
|
315
|
+
let parent_commit_ids = merge_parent_commit_ids(
|
|
316
|
+
parent_commit_ids,
|
|
317
|
+
extra_commit_parents_by_version
|
|
318
|
+
.get(&version_id)
|
|
319
|
+
.cloned()
|
|
320
|
+
.unwrap_or_default(),
|
|
321
|
+
);
|
|
322
|
+
let snapshot_content = serde_json::to_string(&serde_json::json!({
|
|
323
|
+
"id": commit_id,
|
|
324
|
+
"change_set_id": change_set_id,
|
|
325
|
+
"change_ids": change_ids,
|
|
326
|
+
"author_account_ids": [],
|
|
327
|
+
"parent_commit_ids": parent_commit_ids,
|
|
328
|
+
}))
|
|
329
|
+
.map_err(|error| {
|
|
330
|
+
LixError::new(
|
|
331
|
+
"LIX_ERROR_UNKNOWN",
|
|
332
|
+
format!("engine2 commit row snapshot serialization failed: {error}"),
|
|
333
|
+
)
|
|
334
|
+
})?;
|
|
335
|
+
|
|
336
|
+
commit_rows.push(StagedStateRow {
|
|
337
|
+
entity_id: crate::entity_identity::EntityIdentity::single(&commit_id),
|
|
338
|
+
schema_key: "lix_commit".to_string(),
|
|
339
|
+
file_id: None,
|
|
340
|
+
snapshot_content: Some(snapshot_content),
|
|
341
|
+
metadata: None,
|
|
342
|
+
origin: None,
|
|
343
|
+
schema_version: "1".to_string(),
|
|
344
|
+
created_at: timestamp.clone(),
|
|
345
|
+
updated_at: timestamp.clone(),
|
|
346
|
+
global: true,
|
|
347
|
+
change_id: Some(commit_change_id),
|
|
348
|
+
commit_id: Some(commit_id.clone()),
|
|
349
|
+
untracked: false,
|
|
350
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
351
|
+
});
|
|
352
|
+
version_heads.push(PendingVersionHead {
|
|
353
|
+
version_id,
|
|
354
|
+
commit_id,
|
|
355
|
+
timestamp,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Ok(FinalizedCommitRows {
|
|
360
|
+
commit_rows,
|
|
361
|
+
version_heads,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn merge_parent_commit_ids(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
|
|
366
|
+
for parent in extra {
|
|
367
|
+
if !base.contains(&parent) {
|
|
368
|
+
base.push(parent);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
base
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[cfg(test)]
|
|
375
|
+
mod tests {
|
|
376
|
+
use std::collections::BTreeMap;
|
|
377
|
+
use std::sync::{
|
|
378
|
+
atomic::{AtomicUsize, Ordering},
|
|
379
|
+
Arc,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
use async_trait::async_trait;
|
|
383
|
+
use serde_json::Value as JsonValue;
|
|
384
|
+
|
|
385
|
+
use super::*;
|
|
386
|
+
use crate::backend::{
|
|
387
|
+
testing::UnitTestBackend, Backend, BackendKvEntryPage, BackendKvExistsBatch,
|
|
388
|
+
BackendKvGetRequest, BackendKvKeyPage, BackendKvScanRequest, BackendKvValueBatch,
|
|
389
|
+
BackendKvValuePage, BackendKvWriteBatch, BackendKvWriteStats, BackendReadTransaction,
|
|
390
|
+
BackendWriteTransaction,
|
|
391
|
+
};
|
|
392
|
+
use crate::changelog::ChangelogContext;
|
|
393
|
+
use crate::live_state::{LiveStateContext, LiveStateRowRequest};
|
|
394
|
+
use crate::storage::StorageContext;
|
|
395
|
+
use crate::untracked_state::{
|
|
396
|
+
canonicalize_materialized_row, MaterializedUntrackedStateRow, UntrackedStateContext,
|
|
397
|
+
UntrackedStateRowRequest,
|
|
398
|
+
};
|
|
399
|
+
use crate::version::VersionContext;
|
|
400
|
+
use crate::NullableKeyFilter;
|
|
401
|
+
|
|
402
|
+
const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
|
|
403
|
+
const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
|
|
404
|
+
|
|
405
|
+
fn live_state_context() -> LiveStateContext {
|
|
406
|
+
LiveStateContext::new(
|
|
407
|
+
crate::tracked_state::TrackedStateContext::new(),
|
|
408
|
+
crate::untracked_state::UntrackedStateContext::new(),
|
|
409
|
+
crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[tokio::test]
|
|
414
|
+
async fn commit_staged_writes_appends_changelog_and_updates_serving_projection() {
|
|
415
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
416
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
417
|
+
let binary_cas = BinaryCasContext::new();
|
|
418
|
+
let changelog = ChangelogContext::new();
|
|
419
|
+
let live_state = Arc::new(live_state_context());
|
|
420
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
421
|
+
let mut transaction = storage
|
|
422
|
+
.begin_write_transaction()
|
|
423
|
+
.await
|
|
424
|
+
.expect("transaction should open");
|
|
425
|
+
|
|
426
|
+
commit_staged_writes(
|
|
427
|
+
&binary_cas,
|
|
428
|
+
&changelog,
|
|
429
|
+
live_state.as_ref(),
|
|
430
|
+
&version_ctx,
|
|
431
|
+
None,
|
|
432
|
+
transaction.as_mut(),
|
|
433
|
+
StagedWriteSet {
|
|
434
|
+
insert_identities: BTreeMap::new(),
|
|
435
|
+
state_rows: vec![tracked_global_row("change-1")],
|
|
436
|
+
adopted_rows: Vec::new(),
|
|
437
|
+
commit_members_by_version: BTreeMap::from([(
|
|
438
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
439
|
+
members(["change-1"]),
|
|
440
|
+
)]),
|
|
441
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
442
|
+
file_data_writes: Vec::new(),
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
.await
|
|
446
|
+
.expect("commit should flush staged rows");
|
|
447
|
+
transaction
|
|
448
|
+
.commit()
|
|
449
|
+
.await
|
|
450
|
+
.expect("commit should persist kv");
|
|
451
|
+
|
|
452
|
+
let changes = {
|
|
453
|
+
let reader = changelog.reader(storage.clone());
|
|
454
|
+
reader
|
|
455
|
+
.scan_changes(&crate::changelog::ChangelogScanRequest::default())
|
|
456
|
+
.await
|
|
457
|
+
}
|
|
458
|
+
.expect("changelog scan should succeed");
|
|
459
|
+
let change_ids = changes
|
|
460
|
+
.iter()
|
|
461
|
+
.map(|change| change.id.as_str())
|
|
462
|
+
.collect::<Vec<_>>();
|
|
463
|
+
assert_eq!(change_ids, vec!["change-1", "test-uuid-2"]);
|
|
464
|
+
assert!(changes
|
|
465
|
+
.iter()
|
|
466
|
+
.any(|change| change.schema_key == "lix_commit"));
|
|
467
|
+
assert!(!changes
|
|
468
|
+
.iter()
|
|
469
|
+
.any(|change| change.schema_key == "lix_version_ref"));
|
|
470
|
+
|
|
471
|
+
let loaded_head = version_ctx
|
|
472
|
+
.ref_reader(storage.clone())
|
|
473
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
474
|
+
.await
|
|
475
|
+
.expect("version ref load should succeed");
|
|
476
|
+
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#[tokio::test]
|
|
480
|
+
async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
|
|
481
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
482
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
483
|
+
let binary_cas = BinaryCasContext::new();
|
|
484
|
+
let changelog = ChangelogContext::new();
|
|
485
|
+
let live_state = Arc::new(live_state_context());
|
|
486
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
487
|
+
let untracked_state = UntrackedStateContext::new();
|
|
488
|
+
let mut transaction = storage
|
|
489
|
+
.begin_write_transaction()
|
|
490
|
+
.await
|
|
491
|
+
.expect("transaction should open");
|
|
492
|
+
|
|
493
|
+
commit_staged_writes(
|
|
494
|
+
&binary_cas,
|
|
495
|
+
&changelog,
|
|
496
|
+
live_state.as_ref(),
|
|
497
|
+
&version_ctx,
|
|
498
|
+
None,
|
|
499
|
+
transaction.as_mut(),
|
|
500
|
+
StagedWriteSet {
|
|
501
|
+
insert_identities: BTreeMap::new(),
|
|
502
|
+
state_rows: vec![untracked_global_row("change-untracked")],
|
|
503
|
+
adopted_rows: Vec::new(),
|
|
504
|
+
commit_members_by_version: BTreeMap::new(),
|
|
505
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
506
|
+
file_data_writes: Vec::new(),
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
.await
|
|
510
|
+
.expect("commit should flush untracked row");
|
|
511
|
+
transaction
|
|
512
|
+
.commit()
|
|
513
|
+
.await
|
|
514
|
+
.expect("commit should persist kv");
|
|
515
|
+
|
|
516
|
+
let changes = {
|
|
517
|
+
let reader = changelog.reader(storage.clone());
|
|
518
|
+
reader
|
|
519
|
+
.scan_changes(&crate::changelog::ChangelogScanRequest::default())
|
|
520
|
+
.await
|
|
521
|
+
}
|
|
522
|
+
.expect("changelog scan should succeed");
|
|
523
|
+
assert!(changes.is_empty());
|
|
524
|
+
|
|
525
|
+
let loaded = {
|
|
526
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
527
|
+
untracked_reader
|
|
528
|
+
.load_row(&UntrackedStateRowRequest {
|
|
529
|
+
schema_key: "test_schema".to_string(),
|
|
530
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
531
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
532
|
+
file_id: NullableKeyFilter::Null,
|
|
533
|
+
})
|
|
534
|
+
.await
|
|
535
|
+
}
|
|
536
|
+
.expect("untracked row load should succeed")
|
|
537
|
+
.expect("untracked row should be persisted");
|
|
538
|
+
assert_eq!(
|
|
539
|
+
loaded.snapshot_content.as_deref(),
|
|
540
|
+
Some("{\"value\":\"untracked\"}")
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#[tokio::test]
|
|
545
|
+
async fn tracked_write_deletes_matching_untracked_overlay() {
|
|
546
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
547
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
548
|
+
let binary_cas = BinaryCasContext::new();
|
|
549
|
+
let changelog = ChangelogContext::new();
|
|
550
|
+
let untracked_state = UntrackedStateContext::new();
|
|
551
|
+
let live_state = Arc::new(live_state_context());
|
|
552
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
553
|
+
|
|
554
|
+
let mut seed_transaction = storage
|
|
555
|
+
.begin_write_transaction()
|
|
556
|
+
.await
|
|
557
|
+
.expect("seed transaction should open");
|
|
558
|
+
let mut writes = StorageWriteSet::new();
|
|
559
|
+
let canonical_row = {
|
|
560
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
561
|
+
canonicalize_materialized_row(
|
|
562
|
+
&mut writes,
|
|
563
|
+
&mut json_writer,
|
|
564
|
+
&MaterializedUntrackedStateRow::from(untracked_global_row("change-untracked")),
|
|
565
|
+
)
|
|
566
|
+
.expect("untracked seed should canonicalize")
|
|
567
|
+
};
|
|
568
|
+
untracked_state
|
|
569
|
+
.writer(&mut writes)
|
|
570
|
+
.stage_rows(&[canonical_row])
|
|
571
|
+
.expect("untracked seed should write");
|
|
572
|
+
writes
|
|
573
|
+
.apply(&mut seed_transaction.as_mut())
|
|
574
|
+
.await
|
|
575
|
+
.expect("untracked seed should apply");
|
|
576
|
+
seed_transaction
|
|
577
|
+
.commit()
|
|
578
|
+
.await
|
|
579
|
+
.expect("seed transaction should persist");
|
|
580
|
+
|
|
581
|
+
let mut transaction = storage
|
|
582
|
+
.begin_write_transaction()
|
|
583
|
+
.await
|
|
584
|
+
.expect("transaction should open");
|
|
585
|
+
commit_staged_writes(
|
|
586
|
+
&binary_cas,
|
|
587
|
+
&changelog,
|
|
588
|
+
live_state.as_ref(),
|
|
589
|
+
&version_ctx,
|
|
590
|
+
None,
|
|
591
|
+
transaction.as_mut(),
|
|
592
|
+
StagedWriteSet {
|
|
593
|
+
insert_identities: BTreeMap::new(),
|
|
594
|
+
state_rows: vec![tracked_global_row("change-tracked")],
|
|
595
|
+
adopted_rows: Vec::new(),
|
|
596
|
+
commit_members_by_version: BTreeMap::from([(
|
|
597
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
598
|
+
members(["change-tracked"]),
|
|
599
|
+
)]),
|
|
600
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
601
|
+
file_data_writes: Vec::new(),
|
|
602
|
+
},
|
|
603
|
+
)
|
|
604
|
+
.await
|
|
605
|
+
.expect("tracked commit should flush");
|
|
606
|
+
transaction
|
|
607
|
+
.commit()
|
|
608
|
+
.await
|
|
609
|
+
.expect("commit should persist kv");
|
|
610
|
+
|
|
611
|
+
let untracked = {
|
|
612
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
613
|
+
untracked_reader.load_row(&untracked_request()).await
|
|
614
|
+
}
|
|
615
|
+
.expect("untracked load should succeed");
|
|
616
|
+
assert_eq!(untracked, None);
|
|
617
|
+
|
|
618
|
+
let visible = live_state
|
|
619
|
+
.reader(storage.clone())
|
|
620
|
+
.load_row(&live_state_request())
|
|
621
|
+
.await
|
|
622
|
+
.expect("live-state load should succeed")
|
|
623
|
+
.expect("tracked row should be visible");
|
|
624
|
+
assert!(!visible.untracked);
|
|
625
|
+
assert_eq!(visible.change_id.as_deref(), Some("change-tracked"));
|
|
626
|
+
assert_eq!(visible.snapshot_content.as_deref(), Some("{\"value\":1}"));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#[tokio::test]
|
|
630
|
+
async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
|
|
631
|
+
let counting_backend = Arc::new(CountingBackend::new());
|
|
632
|
+
let write_batches = counting_backend.write_batches();
|
|
633
|
+
let backend: Arc<dyn Backend + Send + Sync> = counting_backend;
|
|
634
|
+
let storage = StorageContext::new(backend);
|
|
635
|
+
let binary_cas = BinaryCasContext::new();
|
|
636
|
+
let changelog = ChangelogContext::new();
|
|
637
|
+
let live_state = Arc::new(live_state_context());
|
|
638
|
+
let untracked_state = UntrackedStateContext::new();
|
|
639
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
640
|
+
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
641
|
+
{
|
|
642
|
+
let mut seed_transaction = storage
|
|
643
|
+
.begin_write_transaction()
|
|
644
|
+
.await
|
|
645
|
+
.expect("seed transaction should open");
|
|
646
|
+
let mut writes = StorageWriteSet::new();
|
|
647
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
648
|
+
let mode_snapshot = serde_json::to_string(&serde_json::json!({
|
|
649
|
+
"key": DETERMINISTIC_MODE_KEY,
|
|
650
|
+
"value": { "enabled": true },
|
|
651
|
+
}))
|
|
652
|
+
.expect("mode snapshot should serialize");
|
|
653
|
+
{
|
|
654
|
+
let mut writer = live_state.writer(seed_transaction.as_mut());
|
|
655
|
+
writer
|
|
656
|
+
.stage_rows(
|
|
657
|
+
&mut writes,
|
|
658
|
+
&mut json_writer,
|
|
659
|
+
&[LiveStateRow {
|
|
660
|
+
entity_id: crate::entity_identity::EntityIdentity::single(
|
|
661
|
+
DETERMINISTIC_MODE_KEY,
|
|
662
|
+
),
|
|
663
|
+
schema_key: "lix_key_value".to_string(),
|
|
664
|
+
file_id: None,
|
|
665
|
+
snapshot_content: Some(mode_snapshot),
|
|
666
|
+
metadata: None,
|
|
667
|
+
schema_version: "1".to_string(),
|
|
668
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
669
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
670
|
+
global: true,
|
|
671
|
+
change_id: None,
|
|
672
|
+
commit_id: None,
|
|
673
|
+
untracked: true,
|
|
674
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
675
|
+
}],
|
|
676
|
+
)
|
|
677
|
+
.await
|
|
678
|
+
.expect("deterministic mode should stage");
|
|
679
|
+
}
|
|
680
|
+
writes
|
|
681
|
+
.apply(&mut seed_transaction.as_mut())
|
|
682
|
+
.await
|
|
683
|
+
.expect("deterministic mode should apply");
|
|
684
|
+
seed_transaction
|
|
685
|
+
.commit()
|
|
686
|
+
.await
|
|
687
|
+
.expect("seed transaction should persist");
|
|
688
|
+
}
|
|
689
|
+
write_batches.store(0, Ordering::SeqCst);
|
|
690
|
+
let runtime_functions = {
|
|
691
|
+
let reader = live_state.reader(storage.clone());
|
|
692
|
+
FunctionContext::prepare(&reader)
|
|
693
|
+
.await
|
|
694
|
+
.expect("runtime context should prepare")
|
|
695
|
+
};
|
|
696
|
+
runtime_functions.provider().call_uuid_v7();
|
|
697
|
+
let mut transaction = storage
|
|
698
|
+
.begin_write_transaction()
|
|
699
|
+
.await
|
|
700
|
+
.expect("transaction should open");
|
|
701
|
+
|
|
702
|
+
let mut untracked_row = untracked_global_row("change-untracked");
|
|
703
|
+
untracked_row.entity_id = crate::entity_identity::EntityIdentity::single("entity-2");
|
|
704
|
+
|
|
705
|
+
commit_staged_writes(
|
|
706
|
+
&binary_cas,
|
|
707
|
+
&changelog,
|
|
708
|
+
live_state.as_ref(),
|
|
709
|
+
&version_ctx,
|
|
710
|
+
Some(&runtime_functions),
|
|
711
|
+
transaction.as_mut(),
|
|
712
|
+
StagedWriteSet {
|
|
713
|
+
insert_identities: BTreeMap::new(),
|
|
714
|
+
state_rows: vec![tracked_global_row("change-tracked"), untracked_row],
|
|
715
|
+
adopted_rows: Vec::new(),
|
|
716
|
+
commit_members_by_version: BTreeMap::from([(
|
|
717
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
718
|
+
members(["change-tracked"]),
|
|
719
|
+
)]),
|
|
720
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
721
|
+
file_data_writes: Vec::new(),
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
.await
|
|
725
|
+
.expect("cross-subsystem commit should stage and apply");
|
|
726
|
+
|
|
727
|
+
assert_eq!(
|
|
728
|
+
write_batches.load(Ordering::SeqCst),
|
|
729
|
+
1,
|
|
730
|
+
"tracked, json, untracked, changelog, and version refs must apply as one backend write batch"
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
transaction
|
|
734
|
+
.commit()
|
|
735
|
+
.await
|
|
736
|
+
.expect("commit should persist kv");
|
|
737
|
+
assert_eq!(write_batches.load(Ordering::SeqCst), 1);
|
|
738
|
+
|
|
739
|
+
let changes = changelog
|
|
740
|
+
.reader(storage.clone())
|
|
741
|
+
.scan_changes(&crate::changelog::ChangelogScanRequest::default())
|
|
742
|
+
.await
|
|
743
|
+
.expect("changelog scan should succeed");
|
|
744
|
+
assert!(changes.iter().any(|change| change.id == "change-tracked"));
|
|
745
|
+
assert!(changes
|
|
746
|
+
.iter()
|
|
747
|
+
.any(|change| change.schema_key == "lix_commit"));
|
|
748
|
+
|
|
749
|
+
let loaded_head = version_ctx
|
|
750
|
+
.ref_reader(storage.clone())
|
|
751
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
752
|
+
.await
|
|
753
|
+
.expect("version ref load should succeed");
|
|
754
|
+
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
755
|
+
|
|
756
|
+
let untracked = {
|
|
757
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
758
|
+
untracked_reader
|
|
759
|
+
.load_row(&UntrackedStateRowRequest {
|
|
760
|
+
schema_key: "test_schema".to_string(),
|
|
761
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
762
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-2"),
|
|
763
|
+
file_id: NullableKeyFilter::Null,
|
|
764
|
+
})
|
|
765
|
+
.await
|
|
766
|
+
}
|
|
767
|
+
.expect("untracked row load should succeed")
|
|
768
|
+
.expect("untracked row should persist");
|
|
769
|
+
assert_eq!(
|
|
770
|
+
untracked.snapshot_content.as_deref(),
|
|
771
|
+
Some("{\"value\":\"untracked\"}")
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
let sequence_row = live_state
|
|
775
|
+
.reader(storage.clone())
|
|
776
|
+
.load_row(&LiveStateRowRequest {
|
|
777
|
+
schema_key: "lix_key_value".to_string(),
|
|
778
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
779
|
+
entity_id: crate::entity_identity::EntityIdentity::single(
|
|
780
|
+
DETERMINISTIC_SEQUENCE_KEY,
|
|
781
|
+
),
|
|
782
|
+
file_id: NullableKeyFilter::Null,
|
|
783
|
+
})
|
|
784
|
+
.await
|
|
785
|
+
.expect("deterministic sequence should load")
|
|
786
|
+
.expect("deterministic sequence should persist");
|
|
787
|
+
assert_eq!(
|
|
788
|
+
sequence_row.snapshot_content.as_deref(),
|
|
789
|
+
Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":0}")
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
#[tokio::test]
|
|
794
|
+
async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_version() {
|
|
795
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
796
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
797
|
+
let binary_cas = BinaryCasContext::new();
|
|
798
|
+
let changelog = ChangelogContext::new();
|
|
799
|
+
let live_state = Arc::new(live_state_context());
|
|
800
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
801
|
+
crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
|
|
802
|
+
.await;
|
|
803
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "version-a-before")
|
|
804
|
+
.await;
|
|
805
|
+
|
|
806
|
+
let mut transaction = storage
|
|
807
|
+
.begin_write_transaction()
|
|
808
|
+
.await
|
|
809
|
+
.expect("transaction should open");
|
|
810
|
+
commit_staged_writes(
|
|
811
|
+
&binary_cas,
|
|
812
|
+
&changelog,
|
|
813
|
+
live_state.as_ref(),
|
|
814
|
+
&version_ctx,
|
|
815
|
+
None,
|
|
816
|
+
transaction.as_mut(),
|
|
817
|
+
StagedWriteSet {
|
|
818
|
+
insert_identities: BTreeMap::new(),
|
|
819
|
+
state_rows: vec![tracked_version_row("version-a", "change-version-a")],
|
|
820
|
+
adopted_rows: Vec::new(),
|
|
821
|
+
commit_members_by_version: BTreeMap::from([(
|
|
822
|
+
"version-a".to_string(),
|
|
823
|
+
members(["change-version-a"]),
|
|
824
|
+
)]),
|
|
825
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
826
|
+
file_data_writes: Vec::new(),
|
|
827
|
+
},
|
|
828
|
+
)
|
|
829
|
+
.await
|
|
830
|
+
.expect("version commit should flush");
|
|
831
|
+
transaction
|
|
832
|
+
.commit()
|
|
833
|
+
.await
|
|
834
|
+
.expect("commit should persist kv");
|
|
835
|
+
|
|
836
|
+
let changes = changelog
|
|
837
|
+
.reader(storage.clone())
|
|
838
|
+
.scan_changes(&crate::changelog::ChangelogScanRequest::default())
|
|
839
|
+
.await
|
|
840
|
+
.expect("changelog scan should succeed");
|
|
841
|
+
let commit_changes = changes
|
|
842
|
+
.iter()
|
|
843
|
+
.filter(|change| change.schema_key == "lix_commit")
|
|
844
|
+
.collect::<Vec<_>>();
|
|
845
|
+
assert_eq!(
|
|
846
|
+
commit_changes.len(),
|
|
847
|
+
1,
|
|
848
|
+
"a write to one non-global version must create exactly one commit"
|
|
849
|
+
);
|
|
850
|
+
assert_eq!(
|
|
851
|
+
commit_changes[0]
|
|
852
|
+
.entity_id
|
|
853
|
+
.as_string()
|
|
854
|
+
.expect("commit entity id should project"),
|
|
855
|
+
"test-uuid-1"
|
|
856
|
+
);
|
|
857
|
+
assert!(changes.iter().any(|change| change.id == "change-version-a"));
|
|
858
|
+
assert!(!changes
|
|
859
|
+
.iter()
|
|
860
|
+
.any(|change| change.schema_key == "lix_version_ref"));
|
|
861
|
+
|
|
862
|
+
let global_head = version_ctx
|
|
863
|
+
.ref_reader(storage.clone())
|
|
864
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
865
|
+
.await
|
|
866
|
+
.expect("global head should load");
|
|
867
|
+
let version_head = version_ctx
|
|
868
|
+
.ref_reader(storage.clone())
|
|
869
|
+
.load_head_commit_id("version-a")
|
|
870
|
+
.await
|
|
871
|
+
.expect("version head should load");
|
|
872
|
+
assert_eq!(global_head.as_deref(), Some("global-before"));
|
|
873
|
+
assert_eq!(version_head.as_deref(), Some("test-uuid-1"));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
#[tokio::test]
|
|
877
|
+
async fn finalize_commit_rows_parents_global_commit_to_existing_version_ref() {
|
|
878
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
879
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
880
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
881
|
+
crate::test_support::seed_version_head(
|
|
882
|
+
storage.clone(),
|
|
883
|
+
GLOBAL_VERSION_ID,
|
|
884
|
+
"initial-commit",
|
|
885
|
+
)
|
|
886
|
+
.await;
|
|
887
|
+
|
|
888
|
+
let mut transaction = storage
|
|
889
|
+
.begin_write_transaction()
|
|
890
|
+
.await
|
|
891
|
+
.expect("transaction should open");
|
|
892
|
+
let rows = finalize_commit_rows(
|
|
893
|
+
BTreeMap::from([(
|
|
894
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
895
|
+
members(["change-a", "change-b"]),
|
|
896
|
+
)]),
|
|
897
|
+
BTreeMap::new(),
|
|
898
|
+
&version_ctx,
|
|
899
|
+
transaction.as_mut(),
|
|
900
|
+
)
|
|
901
|
+
.await
|
|
902
|
+
.expect("global commit row should finalize");
|
|
903
|
+
|
|
904
|
+
assert_eq!(rows.commit_rows.len(), 1);
|
|
905
|
+
assert_eq!(rows.version_heads.len(), 1);
|
|
906
|
+
let row = &rows.commit_rows[0];
|
|
907
|
+
assert_eq!(row.entity_id.as_string().as_deref(), Ok("test-uuid-1"));
|
|
908
|
+
assert_eq!(row.schema_key, "lix_commit");
|
|
909
|
+
assert_eq!(row.schema_version, "1");
|
|
910
|
+
assert_eq!(row.change_id.as_deref(), Some("test-uuid-2"));
|
|
911
|
+
assert_eq!(row.commit_id.as_deref(), Some("test-uuid-1"));
|
|
912
|
+
assert!(row.global);
|
|
913
|
+
assert!(!row.untracked);
|
|
914
|
+
assert_eq!(row.version_id, GLOBAL_VERSION_ID);
|
|
915
|
+
assert_eq!(row.created_at, "test-timestamp-1");
|
|
916
|
+
assert_eq!(row.updated_at, "test-timestamp-1");
|
|
917
|
+
|
|
918
|
+
let snapshot = serde_json::from_str::<JsonValue>(
|
|
919
|
+
row.snapshot_content
|
|
920
|
+
.as_deref()
|
|
921
|
+
.expect("commit row should have snapshot"),
|
|
922
|
+
)
|
|
923
|
+
.expect("commit snapshot should be JSON");
|
|
924
|
+
assert_eq!(
|
|
925
|
+
snapshot.get("id").and_then(JsonValue::as_str),
|
|
926
|
+
Some("test-uuid-1")
|
|
927
|
+
);
|
|
928
|
+
assert_eq!(
|
|
929
|
+
snapshot
|
|
930
|
+
.get("change_ids")
|
|
931
|
+
.and_then(JsonValue::as_array)
|
|
932
|
+
.expect("change_ids should be array")
|
|
933
|
+
.iter()
|
|
934
|
+
.map(|value| value.as_str().expect("change id should be string"))
|
|
935
|
+
.collect::<Vec<_>>(),
|
|
936
|
+
vec!["change-a", "change-b"]
|
|
937
|
+
);
|
|
938
|
+
assert_eq!(
|
|
939
|
+
snapshot
|
|
940
|
+
.get("parent_commit_ids")
|
|
941
|
+
.and_then(JsonValue::as_array)
|
|
942
|
+
.expect("parent_commit_ids should be array")
|
|
943
|
+
.iter()
|
|
944
|
+
.map(|value| value.as_str().expect("parent id should be string"))
|
|
945
|
+
.collect::<Vec<_>>(),
|
|
946
|
+
vec!["initial-commit"]
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
let version_head = &rows.version_heads[0];
|
|
950
|
+
assert_eq!(version_head.version_id, GLOBAL_VERSION_ID);
|
|
951
|
+
assert_eq!(version_head.commit_id, "test-uuid-1");
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
#[tokio::test]
|
|
955
|
+
async fn finalize_commit_rows_skips_empty_members() {
|
|
956
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
957
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
958
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
959
|
+
let mut transaction = storage
|
|
960
|
+
.begin_write_transaction()
|
|
961
|
+
.await
|
|
962
|
+
.expect("transaction should open");
|
|
963
|
+
let rows = finalize_commit_rows(
|
|
964
|
+
BTreeMap::from([(
|
|
965
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
966
|
+
StagedCommitMembers::default(),
|
|
967
|
+
)]),
|
|
968
|
+
BTreeMap::new(),
|
|
969
|
+
&version_ctx,
|
|
970
|
+
transaction.as_mut(),
|
|
971
|
+
)
|
|
972
|
+
.await
|
|
973
|
+
.expect("empty members should be ignored");
|
|
974
|
+
|
|
975
|
+
assert!(rows.commit_rows.is_empty());
|
|
976
|
+
assert!(rows.version_heads.is_empty());
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
#[tokio::test]
|
|
980
|
+
async fn finalize_commit_rows_uses_existing_version_ref_as_parent() {
|
|
981
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
982
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
983
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
984
|
+
crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
|
|
985
|
+
.await;
|
|
986
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "previous-commit")
|
|
987
|
+
.await;
|
|
988
|
+
|
|
989
|
+
let mut transaction = storage
|
|
990
|
+
.begin_write_transaction()
|
|
991
|
+
.await
|
|
992
|
+
.expect("transaction should open");
|
|
993
|
+
let rows = finalize_commit_rows(
|
|
994
|
+
BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
|
|
995
|
+
BTreeMap::new(),
|
|
996
|
+
&version_ctx,
|
|
997
|
+
transaction.as_mut(),
|
|
998
|
+
)
|
|
999
|
+
.await
|
|
1000
|
+
.expect("active-version commit finalization should resolve parent");
|
|
1001
|
+
|
|
1002
|
+
let snapshot = serde_json::from_str::<JsonValue>(
|
|
1003
|
+
rows.commit_rows[0]
|
|
1004
|
+
.snapshot_content
|
|
1005
|
+
.as_deref()
|
|
1006
|
+
.expect("commit row should have snapshot"),
|
|
1007
|
+
)
|
|
1008
|
+
.expect("commit snapshot should be JSON");
|
|
1009
|
+
assert_eq!(
|
|
1010
|
+
snapshot
|
|
1011
|
+
.get("parent_commit_ids")
|
|
1012
|
+
.and_then(JsonValue::as_array)
|
|
1013
|
+
.expect("parent_commit_ids should be array")
|
|
1014
|
+
.iter()
|
|
1015
|
+
.map(|value| value.as_str().expect("parent id should be text"))
|
|
1016
|
+
.collect::<Vec<_>>(),
|
|
1017
|
+
vec!["previous-commit"]
|
|
1018
|
+
);
|
|
1019
|
+
assert_eq!(rows.version_heads[0].version_id, "version-a");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
#[tokio::test]
|
|
1023
|
+
async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
|
|
1024
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1025
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1026
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1027
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "target-head").await;
|
|
1028
|
+
|
|
1029
|
+
let mut transaction = storage
|
|
1030
|
+
.begin_write_transaction()
|
|
1031
|
+
.await
|
|
1032
|
+
.expect("transaction should open");
|
|
1033
|
+
let rows = finalize_commit_rows(
|
|
1034
|
+
BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
|
|
1035
|
+
BTreeMap::from([("version-a".to_string(), vec!["source-head".to_string()])]),
|
|
1036
|
+
&version_ctx,
|
|
1037
|
+
transaction.as_mut(),
|
|
1038
|
+
)
|
|
1039
|
+
.await
|
|
1040
|
+
.expect("merge commit finalization should resolve parents");
|
|
1041
|
+
|
|
1042
|
+
let snapshot = serde_json::from_str::<JsonValue>(
|
|
1043
|
+
rows.commit_rows[0]
|
|
1044
|
+
.snapshot_content
|
|
1045
|
+
.as_deref()
|
|
1046
|
+
.expect("commit row should have snapshot"),
|
|
1047
|
+
)
|
|
1048
|
+
.expect("commit snapshot should be JSON");
|
|
1049
|
+
assert_eq!(
|
|
1050
|
+
snapshot
|
|
1051
|
+
.get("parent_commit_ids")
|
|
1052
|
+
.and_then(JsonValue::as_array)
|
|
1053
|
+
.expect("parent_commit_ids should be array")
|
|
1054
|
+
.iter()
|
|
1055
|
+
.map(|value| value.as_str().expect("parent id should be text"))
|
|
1056
|
+
.collect::<Vec<_>>(),
|
|
1057
|
+
vec!["target-head", "source-head"]
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
fn members<const N: usize>(change_ids: [&str; N]) -> StagedCommitMembers {
|
|
1062
|
+
let mut members = StagedCommitMembers::new(
|
|
1063
|
+
"test-uuid-1".to_string(),
|
|
1064
|
+
"test-uuid-2".to_string(),
|
|
1065
|
+
"test-uuid-3".to_string(),
|
|
1066
|
+
"test-timestamp-1".to_string(),
|
|
1067
|
+
);
|
|
1068
|
+
for change_id in change_ids {
|
|
1069
|
+
members.add_change_id(change_id.to_string());
|
|
1070
|
+
}
|
|
1071
|
+
members
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
fn tracked_global_row(change_id: &str) -> StagedStateRow {
|
|
1075
|
+
tracked_version_row(GLOBAL_VERSION_ID, change_id)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
fn tracked_version_row(version_id: &str, change_id: &str) -> StagedStateRow {
|
|
1079
|
+
StagedStateRow {
|
|
1080
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1081
|
+
schema_key: "test_schema".to_string(),
|
|
1082
|
+
file_id: None,
|
|
1083
|
+
snapshot_content: Some("{\"value\":1}".to_string()),
|
|
1084
|
+
metadata: None,
|
|
1085
|
+
origin: None,
|
|
1086
|
+
schema_version: "1".to_string(),
|
|
1087
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1088
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1089
|
+
global: version_id == GLOBAL_VERSION_ID,
|
|
1090
|
+
change_id: Some(change_id.to_string()),
|
|
1091
|
+
commit_id: Some("test-uuid-1".to_string()),
|
|
1092
|
+
untracked: false,
|
|
1093
|
+
version_id: version_id.to_string(),
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
fn untracked_global_row(change_id: &str) -> StagedStateRow {
|
|
1098
|
+
StagedStateRow {
|
|
1099
|
+
snapshot_content: Some("{\"value\":\"untracked\"}".to_string()),
|
|
1100
|
+
change_id: None,
|
|
1101
|
+
commit_id: None,
|
|
1102
|
+
untracked: true,
|
|
1103
|
+
..tracked_global_row(change_id)
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
fn untracked_request() -> UntrackedStateRowRequest {
|
|
1108
|
+
UntrackedStateRowRequest {
|
|
1109
|
+
schema_key: "test_schema".to_string(),
|
|
1110
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1111
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1112
|
+
file_id: NullableKeyFilter::Null,
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
fn live_state_request() -> LiveStateRowRequest {
|
|
1117
|
+
LiveStateRowRequest {
|
|
1118
|
+
schema_key: "test_schema".to_string(),
|
|
1119
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1120
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1121
|
+
file_id: NullableKeyFilter::Null,
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
struct CountingBackend {
|
|
1126
|
+
inner: UnitTestBackend,
|
|
1127
|
+
write_batches: Arc<AtomicUsize>,
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
impl CountingBackend {
|
|
1131
|
+
fn new() -> Self {
|
|
1132
|
+
Self {
|
|
1133
|
+
inner: UnitTestBackend::new(),
|
|
1134
|
+
write_batches: Arc::new(AtomicUsize::new(0)),
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
fn write_batches(&self) -> Arc<AtomicUsize> {
|
|
1139
|
+
Arc::clone(&self.write_batches)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
#[async_trait]
|
|
1144
|
+
impl Backend for CountingBackend {
|
|
1145
|
+
async fn begin_read_transaction(
|
|
1146
|
+
&self,
|
|
1147
|
+
) -> Result<Box<dyn BackendReadTransaction + Send + Sync + 'static>, LixError> {
|
|
1148
|
+
self.inner.begin_read_transaction().await
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async fn begin_write_transaction(
|
|
1152
|
+
&self,
|
|
1153
|
+
) -> Result<Box<dyn BackendWriteTransaction + Send + Sync + 'static>, LixError> {
|
|
1154
|
+
Ok(Box::new(CountingWriteTransaction {
|
|
1155
|
+
inner: self.inner.begin_write_transaction().await?,
|
|
1156
|
+
write_batches: Arc::clone(&self.write_batches),
|
|
1157
|
+
}))
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
struct CountingWriteTransaction {
|
|
1162
|
+
inner: Box<dyn BackendWriteTransaction + Send + Sync + 'static>,
|
|
1163
|
+
write_batches: Arc<AtomicUsize>,
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[async_trait]
|
|
1167
|
+
impl BackendReadTransaction for CountingWriteTransaction {
|
|
1168
|
+
async fn get_values(
|
|
1169
|
+
&mut self,
|
|
1170
|
+
request: BackendKvGetRequest,
|
|
1171
|
+
) -> Result<BackendKvValueBatch, LixError> {
|
|
1172
|
+
self.inner.get_values(request).await
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async fn exists_many(
|
|
1176
|
+
&mut self,
|
|
1177
|
+
request: BackendKvGetRequest,
|
|
1178
|
+
) -> Result<BackendKvExistsBatch, LixError> {
|
|
1179
|
+
self.inner.exists_many(request).await
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async fn scan_keys(
|
|
1183
|
+
&mut self,
|
|
1184
|
+
request: BackendKvScanRequest,
|
|
1185
|
+
) -> Result<BackendKvKeyPage, LixError> {
|
|
1186
|
+
self.inner.scan_keys(request).await
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async fn scan_values(
|
|
1190
|
+
&mut self,
|
|
1191
|
+
request: BackendKvScanRequest,
|
|
1192
|
+
) -> Result<BackendKvValuePage, LixError> {
|
|
1193
|
+
self.inner.scan_values(request).await
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async fn scan_entries(
|
|
1197
|
+
&mut self,
|
|
1198
|
+
request: BackendKvScanRequest,
|
|
1199
|
+
) -> Result<BackendKvEntryPage, LixError> {
|
|
1200
|
+
self.inner.scan_entries(request).await
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
async fn rollback(self: Box<Self>) -> Result<(), LixError> {
|
|
1204
|
+
let Self { inner, .. } = *self;
|
|
1205
|
+
inner.rollback().await
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
#[async_trait]
|
|
1210
|
+
impl BackendWriteTransaction for CountingWriteTransaction {
|
|
1211
|
+
async fn write_kv_batch(
|
|
1212
|
+
&mut self,
|
|
1213
|
+
batch: BackendKvWriteBatch,
|
|
1214
|
+
) -> Result<BackendKvWriteStats, LixError> {
|
|
1215
|
+
self.write_batches.fetch_add(1, Ordering::SeqCst);
|
|
1216
|
+
self.inner.write_kv_batch(batch).await
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async fn commit(self: Box<Self>) -> Result<(), LixError> {
|
|
1220
|
+
let Self { inner, .. } = *self;
|
|
1221
|
+
inner.commit().await
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|