@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,1484 @@
|
|
|
1
|
+
use crate::binary_cas::BinaryCasContext;
|
|
2
|
+
use crate::commit_store::{ChangeRef, CommitDraftRef, CommitStoreContext, StagedCommitStoreCommit};
|
|
3
|
+
use crate::functions::FunctionContext;
|
|
4
|
+
use crate::json_store::{JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
|
|
5
|
+
use crate::storage::{StorageReader, StorageWriteSet, StorageWriteTransaction};
|
|
6
|
+
use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
|
|
7
|
+
use crate::transaction::prepare_version_ref_row;
|
|
8
|
+
use crate::transaction::staging::PreparedWriteSet;
|
|
9
|
+
use crate::transaction::types::{PreparedAdoptedStateRow, PreparedStateRow, StagedCommitMembers};
|
|
10
|
+
use crate::untracked_state::{
|
|
11
|
+
UntrackedStateContext, UntrackedStateIdentity, UntrackedStateIdentityRef, UntrackedStateRowRef,
|
|
12
|
+
};
|
|
13
|
+
use crate::version::{VersionContext, VersionRefReader};
|
|
14
|
+
use crate::LixError;
|
|
15
|
+
use std::collections::BTreeMap;
|
|
16
|
+
|
|
17
|
+
type RowIndex = usize;
|
|
18
|
+
type AdoptedRowIndex = usize;
|
|
19
|
+
|
|
20
|
+
/// Commits prepared transaction rows into durable tracked and untracked stores.
|
|
21
|
+
///
|
|
22
|
+
/// Providers decode DataFusion DML into hydrated `PreparedStateRow`s. Untracked
|
|
23
|
+
/// rows are durable local overlay state and bypass commit-store rows. Tracked
|
|
24
|
+
/// rows stage canonical commit-store facts, then update the live-state serving
|
|
25
|
+
/// projection. The tracked side of that projection is a prolly root keyed by
|
|
26
|
+
/// the new commit id.
|
|
27
|
+
pub(crate) async fn commit_prepared_writes(
|
|
28
|
+
binary_cas: &BinaryCasContext,
|
|
29
|
+
commit_store: &CommitStoreContext,
|
|
30
|
+
version_ctx: &VersionContext,
|
|
31
|
+
runtime_functions: Option<&FunctionContext>,
|
|
32
|
+
transaction: &mut (impl StorageWriteTransaction + ?Sized),
|
|
33
|
+
prepared_writes: PreparedWriteSet,
|
|
34
|
+
) -> Result<(), LixError> {
|
|
35
|
+
let mut writes = StorageWriteSet::new();
|
|
36
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
37
|
+
|
|
38
|
+
if !prepared_writes.file_data_writes.is_empty() {
|
|
39
|
+
let mut blob_writer = binary_cas.writer(&mut writes);
|
|
40
|
+
for write in &prepared_writes.file_data_writes {
|
|
41
|
+
blob_writer.stage_bytes(&write.data)?;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let state_rows = prepared_writes.state_rows;
|
|
46
|
+
let adopted_rows = prepared_writes.adopted_rows;
|
|
47
|
+
let finalized = finalize_commit_rows(
|
|
48
|
+
prepared_writes.commit_members_by_version,
|
|
49
|
+
prepared_writes.extra_commit_parents_by_version,
|
|
50
|
+
version_ctx,
|
|
51
|
+
transaction,
|
|
52
|
+
)
|
|
53
|
+
.await?;
|
|
54
|
+
let commit_rows = finalized.commit_rows;
|
|
55
|
+
let version_heads = finalized.version_heads;
|
|
56
|
+
let tracked_roots = finalized.tracked_roots;
|
|
57
|
+
let row_index = index_prepared_rows(&state_rows)?;
|
|
58
|
+
let adopted_index = index_adopted_rows(&adopted_rows);
|
|
59
|
+
|
|
60
|
+
if let Some(runtime_functions) = runtime_functions {
|
|
61
|
+
runtime_functions
|
|
62
|
+
.stage_persist_if_needed(&mut writes)
|
|
63
|
+
.await?;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if state_rows.is_empty()
|
|
67
|
+
&& adopted_rows.is_empty()
|
|
68
|
+
&& commit_rows.is_empty()
|
|
69
|
+
&& version_heads.is_empty()
|
|
70
|
+
&& writes.is_empty()
|
|
71
|
+
{
|
|
72
|
+
return Ok(());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let staged_commits = stage_commit_store_commits(
|
|
76
|
+
commit_store,
|
|
77
|
+
transaction,
|
|
78
|
+
&mut writes,
|
|
79
|
+
&state_rows,
|
|
80
|
+
&row_index.tracked_row_indices_by_commit,
|
|
81
|
+
&adopted_rows,
|
|
82
|
+
&adopted_index.tracked_row_indices_by_commit,
|
|
83
|
+
&commit_rows,
|
|
84
|
+
)
|
|
85
|
+
.await?;
|
|
86
|
+
|
|
87
|
+
let json_pack_indexes_by_commit = stage_prepared_json_payloads(
|
|
88
|
+
&mut json_writer,
|
|
89
|
+
&mut writes,
|
|
90
|
+
&state_rows,
|
|
91
|
+
&row_index.tracked_row_indices_by_commit,
|
|
92
|
+
&staged_commits,
|
|
93
|
+
&row_index.untracked_row_indices,
|
|
94
|
+
)?;
|
|
95
|
+
|
|
96
|
+
// The serving projection is updated in the same backend transaction as the
|
|
97
|
+
// commit-store append. Tracked rows become prolly mutations under their owning
|
|
98
|
+
// commit root; untracked rows remain in the separate local overlay store.
|
|
99
|
+
{
|
|
100
|
+
let untracked_overlay_delete_identities = existing_untracked_overlay_delete_identities(
|
|
101
|
+
transaction,
|
|
102
|
+
row_index
|
|
103
|
+
.canonical_row_indices
|
|
104
|
+
.iter()
|
|
105
|
+
.map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index]))
|
|
106
|
+
.chain(
|
|
107
|
+
adopted_rows
|
|
108
|
+
.iter()
|
|
109
|
+
.map(untracked_identity_ref_from_adopted_row),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
.await?;
|
|
113
|
+
UntrackedStateContext::new()
|
|
114
|
+
.writer(&mut writes)
|
|
115
|
+
.stage_rows(
|
|
116
|
+
row_index
|
|
117
|
+
.untracked_row_indices
|
|
118
|
+
.iter()
|
|
119
|
+
.map(|&row_index| untracked_row_ref_from_state_row(&state_rows[row_index])),
|
|
120
|
+
)?;
|
|
121
|
+
UntrackedStateContext::new()
|
|
122
|
+
.writer(&mut writes)
|
|
123
|
+
.stage_delete_rows(
|
|
124
|
+
untracked_overlay_delete_identities
|
|
125
|
+
.iter()
|
|
126
|
+
.map(UntrackedStateIdentity::as_ref),
|
|
127
|
+
);
|
|
128
|
+
stage_tracked_roots(
|
|
129
|
+
transaction,
|
|
130
|
+
&mut writes,
|
|
131
|
+
&state_rows,
|
|
132
|
+
row_index.tracked_row_indices_by_commit,
|
|
133
|
+
&adopted_rows,
|
|
134
|
+
adopted_index.tracked_row_indices_by_commit,
|
|
135
|
+
tracked_roots,
|
|
136
|
+
staged_commits,
|
|
137
|
+
json_pack_indexes_by_commit,
|
|
138
|
+
)
|
|
139
|
+
.await?;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for version_head in version_heads {
|
|
143
|
+
let canonical_row = prepare_version_ref_row(
|
|
144
|
+
&version_head.version_id,
|
|
145
|
+
&version_head.commit_id,
|
|
146
|
+
&version_head.timestamp,
|
|
147
|
+
)?;
|
|
148
|
+
version_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
writes.apply(transaction).await?;
|
|
152
|
+
Ok(())
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn stage_prepared_json_payloads(
|
|
156
|
+
json_writer: &mut crate::json_store::JsonStoreWriter,
|
|
157
|
+
writes: &mut StorageWriteSet,
|
|
158
|
+
state_rows: &[PreparedStateRow],
|
|
159
|
+
tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
|
|
160
|
+
staged_commits: &BTreeMap<String, StagedCommitStoreCommit>,
|
|
161
|
+
untracked_row_indices: &[RowIndex],
|
|
162
|
+
) -> Result<BTreeMap<String, BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>>, LixError> {
|
|
163
|
+
let mut pack_indexes_by_commit = BTreeMap::new();
|
|
164
|
+
for (commit_id, row_indices) in tracked_row_indices_by_commit {
|
|
165
|
+
let staged_commit = staged_commits.get(commit_id).ok_or_else(|| {
|
|
166
|
+
LixError::new(
|
|
167
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
168
|
+
format!("commit '{commit_id}' has tracked JSON rows but no staged commit-store locators"),
|
|
169
|
+
)
|
|
170
|
+
})?;
|
|
171
|
+
if row_indices.len() != staged_commit.authored_locators.len() {
|
|
172
|
+
return Err(LixError::new(
|
|
173
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
174
|
+
format!(
|
|
175
|
+
"commit '{commit_id}' has {} tracked JSON rows but {} authored locators",
|
|
176
|
+
row_indices.len(),
|
|
177
|
+
staged_commit.authored_locators.len()
|
|
178
|
+
),
|
|
179
|
+
));
|
|
180
|
+
}
|
|
181
|
+
let mut row_indices_by_pack = BTreeMap::<u32, Vec<RowIndex>>::new();
|
|
182
|
+
for (&row_index, locator) in row_indices.iter().zip(&staged_commit.authored_locators) {
|
|
183
|
+
row_indices_by_pack
|
|
184
|
+
.entry(locator.source_pack_id)
|
|
185
|
+
.or_default()
|
|
186
|
+
.push(row_index);
|
|
187
|
+
}
|
|
188
|
+
for (pack_id, pack_row_indices) in row_indices_by_pack {
|
|
189
|
+
let report = json_writer.stage_batch_report(
|
|
190
|
+
writes,
|
|
191
|
+
JsonWritePlacementRef::CommitPack { commit_id, pack_id },
|
|
192
|
+
pack_row_indices
|
|
193
|
+
.iter()
|
|
194
|
+
.flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
|
|
195
|
+
)?;
|
|
196
|
+
pack_indexes_by_commit
|
|
197
|
+
.entry(commit_id.clone())
|
|
198
|
+
.or_insert_with(BTreeMap::new)
|
|
199
|
+
.insert(pack_id, report.pack_indexes);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
json_writer.stage_batch(
|
|
203
|
+
writes,
|
|
204
|
+
JsonWritePlacementRef::OutOfBand,
|
|
205
|
+
untracked_row_indices
|
|
206
|
+
.iter()
|
|
207
|
+
.flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
|
|
208
|
+
)?;
|
|
209
|
+
Ok(pack_indexes_by_commit)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn json_payloads_from_state_row(
|
|
213
|
+
row: &PreparedStateRow,
|
|
214
|
+
) -> impl Iterator<Item = NormalizedJsonRef<'_>> {
|
|
215
|
+
row.snapshot
|
|
216
|
+
.iter()
|
|
217
|
+
.chain(row.metadata.iter())
|
|
218
|
+
.map(|json| NormalizedJsonRef::trusted_prehashed(json.normalized.as_ref(), json.json_ref))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async fn existing_untracked_overlay_delete_identities<'a>(
|
|
222
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
223
|
+
identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
|
|
224
|
+
) -> Result<Vec<UntrackedStateIdentity>, LixError> {
|
|
225
|
+
UntrackedStateContext::new()
|
|
226
|
+
.reader(transaction)
|
|
227
|
+
.existing_identities(identities)
|
|
228
|
+
.await
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
struct PreparedRowIndex {
|
|
232
|
+
canonical_row_indices: Vec<RowIndex>,
|
|
233
|
+
untracked_row_indices: Vec<RowIndex>,
|
|
234
|
+
tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
struct PreparedAdoptedRowIndex {
|
|
238
|
+
tracked_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, LixError> {
|
|
242
|
+
let mut canonical_row_indices = Vec::new();
|
|
243
|
+
let mut untracked_row_indices = Vec::new();
|
|
244
|
+
let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<RowIndex>>::new();
|
|
245
|
+
|
|
246
|
+
for (row_index, row) in rows.iter().enumerate() {
|
|
247
|
+
if row.untracked {
|
|
248
|
+
untracked_row_indices.push(row_index);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let Some(commit_id) = row.commit_id.as_ref() else {
|
|
252
|
+
return Err(LixError::new(
|
|
253
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
254
|
+
"tracked prepared row is missing commit_id before commit indexing",
|
|
255
|
+
));
|
|
256
|
+
};
|
|
257
|
+
canonical_row_indices.push(row_index);
|
|
258
|
+
tracked_row_indices_by_commit
|
|
259
|
+
.entry(commit_id.clone())
|
|
260
|
+
.or_default()
|
|
261
|
+
.push(row_index);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Ok(PreparedRowIndex {
|
|
265
|
+
canonical_row_indices,
|
|
266
|
+
untracked_row_indices,
|
|
267
|
+
tracked_row_indices_by_commit,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn index_adopted_rows(rows: &[PreparedAdoptedStateRow]) -> PreparedAdoptedRowIndex {
|
|
272
|
+
let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<AdoptedRowIndex>>::new();
|
|
273
|
+
for (row_index, row) in rows.iter().enumerate() {
|
|
274
|
+
tracked_row_indices_by_commit
|
|
275
|
+
.entry(row.commit_id.clone())
|
|
276
|
+
.or_default()
|
|
277
|
+
.push(row_index);
|
|
278
|
+
}
|
|
279
|
+
PreparedAdoptedRowIndex {
|
|
280
|
+
tracked_row_indices_by_commit,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async fn stage_commit_store_commits(
|
|
285
|
+
commit_store: &CommitStoreContext,
|
|
286
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
287
|
+
writes: &mut StorageWriteSet,
|
|
288
|
+
state_rows: &[PreparedStateRow],
|
|
289
|
+
tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
|
|
290
|
+
adopted_rows: &[PreparedAdoptedStateRow],
|
|
291
|
+
adopted_row_indices_by_commit: &BTreeMap<String, Vec<AdoptedRowIndex>>,
|
|
292
|
+
commit_rows: &[FinalizedCommitRow],
|
|
293
|
+
) -> Result<BTreeMap<String, StagedCommitStoreCommit>, LixError> {
|
|
294
|
+
let mut commits = Vec::with_capacity(commit_rows.len());
|
|
295
|
+
let mut commit_ids = Vec::with_capacity(commit_rows.len());
|
|
296
|
+
for commit_row in commit_rows {
|
|
297
|
+
let state_row_indices = tracked_row_indices_by_commit
|
|
298
|
+
.get(&commit_row.commit_id)
|
|
299
|
+
.map(Vec::as_slice)
|
|
300
|
+
.unwrap_or_default();
|
|
301
|
+
let adopted_row_indices = adopted_row_indices_by_commit
|
|
302
|
+
.get(&commit_row.commit_id)
|
|
303
|
+
.map(Vec::as_slice)
|
|
304
|
+
.unwrap_or_default();
|
|
305
|
+
let mut authored_changes = Vec::with_capacity(state_row_indices.len());
|
|
306
|
+
for &row_index in state_row_indices {
|
|
307
|
+
authored_changes.push(change_ref_from_state_row(&state_rows[row_index])?);
|
|
308
|
+
}
|
|
309
|
+
let mut adopted_changes = Vec::with_capacity(adopted_row_indices.len());
|
|
310
|
+
for &row_index in adopted_row_indices {
|
|
311
|
+
adopted_changes.push(change_ref_from_adopted_row(&adopted_rows[row_index]));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let commit = CommitDraftRef {
|
|
315
|
+
id: &commit_row.commit_id,
|
|
316
|
+
change_id: &commit_row.change_id,
|
|
317
|
+
parent_ids: &commit_row.parent_commit_ids,
|
|
318
|
+
author_account_ids: &[],
|
|
319
|
+
created_at: &commit_row.created_at,
|
|
320
|
+
};
|
|
321
|
+
commit_ids.push(commit_row.commit_id.clone());
|
|
322
|
+
commits.push((commit, authored_changes, adopted_changes));
|
|
323
|
+
}
|
|
324
|
+
let staged = commit_store
|
|
325
|
+
.writer(transaction, writes)
|
|
326
|
+
.stage_tracked_commit_drafts(commits)
|
|
327
|
+
.await?;
|
|
328
|
+
if staged.len() != commit_ids.len() {
|
|
329
|
+
return Err(LixError::new(
|
|
330
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
331
|
+
format!(
|
|
332
|
+
"commit-store staged {} commits for {} finalized commit rows",
|
|
333
|
+
staged.len(),
|
|
334
|
+
commit_ids.len()
|
|
335
|
+
),
|
|
336
|
+
));
|
|
337
|
+
}
|
|
338
|
+
Ok(commit_ids.into_iter().zip(staged).collect())
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn change_ref_from_state_row(row: &PreparedStateRow) -> Result<ChangeRef<'_>, LixError> {
|
|
342
|
+
let Some(change_id) = row.change_id.as_deref() else {
|
|
343
|
+
return Err(LixError::new(
|
|
344
|
+
"LIX_ERROR_UNKNOWN",
|
|
345
|
+
"tracked staged row is missing change_id before commit-store append",
|
|
346
|
+
));
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
Ok(ChangeRef {
|
|
350
|
+
id: change_id,
|
|
351
|
+
entity_id: &row.entity_id,
|
|
352
|
+
schema_key: &row.schema_key,
|
|
353
|
+
file_id: row.file_id.as_deref(),
|
|
354
|
+
snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
|
|
355
|
+
metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
|
|
356
|
+
created_at: &row.updated_at,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fn change_ref_from_adopted_row(row: &PreparedAdoptedStateRow) -> ChangeRef<'_> {
|
|
361
|
+
ChangeRef {
|
|
362
|
+
id: &row.change_id,
|
|
363
|
+
entity_id: &row.entity_id,
|
|
364
|
+
schema_key: &row.schema_key,
|
|
365
|
+
file_id: row.file_id.as_deref(),
|
|
366
|
+
snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
|
|
367
|
+
metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
|
|
368
|
+
created_at: &row.updated_at,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async fn stage_tracked_roots(
|
|
373
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
374
|
+
writes: &mut StorageWriteSet,
|
|
375
|
+
state_rows: &[PreparedStateRow],
|
|
376
|
+
mut tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
|
|
377
|
+
adopted_rows: &[PreparedAdoptedStateRow],
|
|
378
|
+
mut adopted_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
|
|
379
|
+
tracked_roots: Vec<PendingTrackedRoot>,
|
|
380
|
+
mut staged_commits: BTreeMap<String, StagedCommitStoreCommit>,
|
|
381
|
+
json_pack_indexes_by_commit: BTreeMap<
|
|
382
|
+
String,
|
|
383
|
+
BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>,
|
|
384
|
+
>,
|
|
385
|
+
) -> Result<(), LixError> {
|
|
386
|
+
let tracked_state = TrackedStateContext::new();
|
|
387
|
+
let mut writer = tracked_state.writer(transaction, writes);
|
|
388
|
+
for root in tracked_roots {
|
|
389
|
+
let staged = staged_commits.remove(&root.commit_id).ok_or_else(|| {
|
|
390
|
+
LixError::new(
|
|
391
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
392
|
+
format!(
|
|
393
|
+
"tracked-state root for commit '{}' has no staged commit-store locators",
|
|
394
|
+
root.commit_id
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
})?;
|
|
398
|
+
let state_row_indices = tracked_row_indices_by_commit
|
|
399
|
+
.remove(&root.commit_id)
|
|
400
|
+
.unwrap_or_default();
|
|
401
|
+
let adopted_row_indices = adopted_row_indices_by_commit
|
|
402
|
+
.remove(&root.commit_id)
|
|
403
|
+
.unwrap_or_default();
|
|
404
|
+
if state_row_indices.len() != staged.authored_locators.len() {
|
|
405
|
+
return Err(LixError::new(
|
|
406
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
407
|
+
format!(
|
|
408
|
+
"commit '{}' has {} tracked authored rows but {} commit-store authored locators",
|
|
409
|
+
root.commit_id,
|
|
410
|
+
state_row_indices.len(),
|
|
411
|
+
staged.authored_locators.len()
|
|
412
|
+
),
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
if adopted_row_indices.len() != staged.adopted_locators.len() {
|
|
416
|
+
return Err(LixError::new(
|
|
417
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
418
|
+
format!(
|
|
419
|
+
"commit '{}' has {} tracked adopted rows but {} commit-store adopted locators",
|
|
420
|
+
root.commit_id,
|
|
421
|
+
adopted_row_indices.len(),
|
|
422
|
+
staged.adopted_locators.len()
|
|
423
|
+
),
|
|
424
|
+
));
|
|
425
|
+
}
|
|
426
|
+
let authored_changes = state_row_indices
|
|
427
|
+
.iter()
|
|
428
|
+
.map(|&row_index| change_ref_from_state_row(&state_rows[row_index]))
|
|
429
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
430
|
+
let adopted_changes = adopted_row_indices
|
|
431
|
+
.iter()
|
|
432
|
+
.map(|&row_index| change_ref_from_adopted_row(&adopted_rows[row_index]))
|
|
433
|
+
.collect::<Vec<_>>();
|
|
434
|
+
let authored_updated_at = state_row_indices
|
|
435
|
+
.iter()
|
|
436
|
+
.map(|&row_index| state_rows[row_index].updated_at.as_str())
|
|
437
|
+
.collect::<Vec<_>>();
|
|
438
|
+
let authored_created_at = state_row_indices
|
|
439
|
+
.iter()
|
|
440
|
+
.map(|&row_index| state_rows[row_index].created_at.as_str())
|
|
441
|
+
.collect::<Vec<_>>();
|
|
442
|
+
let adopted_updated_at = adopted_row_indices
|
|
443
|
+
.iter()
|
|
444
|
+
.map(|&row_index| adopted_rows[row_index].updated_at.as_str())
|
|
445
|
+
.collect::<Vec<_>>();
|
|
446
|
+
let adopted_created_at = adopted_row_indices
|
|
447
|
+
.iter()
|
|
448
|
+
.map(|&row_index| adopted_rows[row_index].created_at.as_str())
|
|
449
|
+
.collect::<Vec<_>>();
|
|
450
|
+
let mut deltas = Vec::with_capacity(authored_changes.len() + adopted_changes.len());
|
|
451
|
+
deltas.extend(
|
|
452
|
+
authored_changes
|
|
453
|
+
.iter()
|
|
454
|
+
.zip(&staged.authored_locators)
|
|
455
|
+
.zip(authored_created_at)
|
|
456
|
+
.zip(authored_updated_at)
|
|
457
|
+
.map(
|
|
458
|
+
|(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
|
|
459
|
+
change: *change,
|
|
460
|
+
locator: locator.as_ref(),
|
|
461
|
+
created_at,
|
|
462
|
+
updated_at,
|
|
463
|
+
},
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
deltas.extend(
|
|
467
|
+
adopted_changes
|
|
468
|
+
.iter()
|
|
469
|
+
.zip(&staged.adopted_locators)
|
|
470
|
+
.zip(adopted_created_at)
|
|
471
|
+
.zip(adopted_updated_at)
|
|
472
|
+
.map(
|
|
473
|
+
|(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
|
|
474
|
+
change: *change,
|
|
475
|
+
locator: locator.as_ref(),
|
|
476
|
+
created_at,
|
|
477
|
+
updated_at,
|
|
478
|
+
},
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
if let Some(indexes) = json_pack_indexes_by_commit
|
|
482
|
+
.get(&root.commit_id)
|
|
483
|
+
.and_then(|packs| packs.get(&0))
|
|
484
|
+
{
|
|
485
|
+
writer
|
|
486
|
+
.stage_delta_with_json_pack_indexes(
|
|
487
|
+
&root.commit_id,
|
|
488
|
+
root.parent_commit_id.as_deref(),
|
|
489
|
+
&deltas,
|
|
490
|
+
crate::tracked_state::DeltaJsonPackIndexesRef {
|
|
491
|
+
commit_id: &root.commit_id,
|
|
492
|
+
pack_id: 0,
|
|
493
|
+
indexes,
|
|
494
|
+
},
|
|
495
|
+
)
|
|
496
|
+
.await?;
|
|
497
|
+
} else {
|
|
498
|
+
writer
|
|
499
|
+
.stage_delta(&root.commit_id, root.parent_commit_id.as_deref(), &deltas)
|
|
500
|
+
.await?;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if !tracked_row_indices_by_commit.is_empty() || !adopted_row_indices_by_commit.is_empty() {
|
|
504
|
+
let mut commit_ids = tracked_row_indices_by_commit
|
|
505
|
+
.keys()
|
|
506
|
+
.chain(adopted_row_indices_by_commit.keys())
|
|
507
|
+
.cloned()
|
|
508
|
+
.collect::<Vec<_>>();
|
|
509
|
+
commit_ids.sort();
|
|
510
|
+
commit_ids.dedup();
|
|
511
|
+
return Err(LixError::new(
|
|
512
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
513
|
+
format!(
|
|
514
|
+
"tracked live_state rows have no finalized root metadata for commit ids: {}",
|
|
515
|
+
commit_ids.join(", ")
|
|
516
|
+
),
|
|
517
|
+
));
|
|
518
|
+
}
|
|
519
|
+
if !staged_commits.is_empty() {
|
|
520
|
+
let commit_ids = staged_commits.keys().cloned().collect::<Vec<_>>();
|
|
521
|
+
return Err(LixError::new(
|
|
522
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
523
|
+
format!(
|
|
524
|
+
"commit-store staged commits without tracked root metadata: {}",
|
|
525
|
+
commit_ids.join(", ")
|
|
526
|
+
),
|
|
527
|
+
));
|
|
528
|
+
}
|
|
529
|
+
Ok(())
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRowRef<'_> {
|
|
533
|
+
UntrackedStateRowRef {
|
|
534
|
+
entity_id: &row.entity_id,
|
|
535
|
+
schema_key: &row.schema_key,
|
|
536
|
+
file_id: row.file_id.as_deref(),
|
|
537
|
+
snapshot_content: row
|
|
538
|
+
.snapshot
|
|
539
|
+
.as_ref()
|
|
540
|
+
.map(|snapshot| snapshot.normalized.as_ref()),
|
|
541
|
+
metadata: row
|
|
542
|
+
.metadata
|
|
543
|
+
.as_ref()
|
|
544
|
+
.map(|metadata| metadata.normalized.as_ref()),
|
|
545
|
+
created_at: &row.created_at,
|
|
546
|
+
updated_at: &row.updated_at,
|
|
547
|
+
global: row.global,
|
|
548
|
+
version_id: &row.version_id,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
fn untracked_identity_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateIdentityRef<'_> {
|
|
553
|
+
UntrackedStateIdentityRef {
|
|
554
|
+
version_id: &row.version_id,
|
|
555
|
+
schema_key: &row.schema_key,
|
|
556
|
+
entity_id: &row.entity_id,
|
|
557
|
+
file_id: row.file_id.as_deref(),
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fn untracked_identity_ref_from_adopted_row(
|
|
562
|
+
row: &PreparedAdoptedStateRow,
|
|
563
|
+
) -> UntrackedStateIdentityRef<'_> {
|
|
564
|
+
UntrackedStateIdentityRef {
|
|
565
|
+
version_id: &row.version_id,
|
|
566
|
+
schema_key: &row.schema_key,
|
|
567
|
+
entity_id: &row.entity_id,
|
|
568
|
+
file_id: row.file_id.as_deref(),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/// Materializes tracked staged membership into commit-store commits.
|
|
573
|
+
///
|
|
574
|
+
/// Staging only accumulates `version_id -> change_ids` because commit ids,
|
|
575
|
+
/// parent heads, and commit-row timestamps belong to transaction finalization.
|
|
576
|
+
/// The `change_ids` list is the ordered set of canonical changes whose effects
|
|
577
|
+
/// the commit introduces relative to its first parent; merge commits may later
|
|
578
|
+
/// populate this list with existing source-parent changes instead of copied
|
|
579
|
+
/// change payloads.
|
|
580
|
+
/// This function turns those membership sets into finalized commit facts.
|
|
581
|
+
///
|
|
582
|
+
/// Commit finalization output split by durability target.
|
|
583
|
+
///
|
|
584
|
+
/// `commit_rows` are canonical commit-store facts. live_state later projects
|
|
585
|
+
/// commit SQL surfaces from commit_store; tracked_state roots do not store
|
|
586
|
+
/// commit graph facts.
|
|
587
|
+
///
|
|
588
|
+
/// `version_heads` are moving refs. They are written through `VersionContext`,
|
|
589
|
+
/// not the canonical commit store.
|
|
590
|
+
struct FinalizedCommitRows {
|
|
591
|
+
commit_rows: Vec<FinalizedCommitRow>,
|
|
592
|
+
version_heads: Vec<PendingVersionHead>,
|
|
593
|
+
tracked_roots: Vec<PendingTrackedRoot>,
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
struct FinalizedCommitRow {
|
|
597
|
+
commit_id: String,
|
|
598
|
+
parent_commit_ids: Vec<String>,
|
|
599
|
+
created_at: String,
|
|
600
|
+
change_id: String,
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
struct PendingVersionHead {
|
|
604
|
+
version_id: String,
|
|
605
|
+
commit_id: String,
|
|
606
|
+
timestamp: String,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
struct PendingTrackedRoot {
|
|
610
|
+
commit_id: String,
|
|
611
|
+
parent_commit_id: Option<String>,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async fn finalize_commit_rows(
|
|
615
|
+
commit_members_by_version: BTreeMap<String, StagedCommitMembers>,
|
|
616
|
+
extra_commit_parents_by_version: BTreeMap<String, Vec<String>>,
|
|
617
|
+
version_ctx: &VersionContext,
|
|
618
|
+
transaction: &mut (impl StorageReader + ?Sized),
|
|
619
|
+
) -> Result<FinalizedCommitRows, LixError> {
|
|
620
|
+
let mut commit_rows = Vec::new();
|
|
621
|
+
let mut version_heads = Vec::new();
|
|
622
|
+
let mut tracked_roots = Vec::new();
|
|
623
|
+
|
|
624
|
+
for (version_id, members) in commit_members_by_version {
|
|
625
|
+
if members.is_empty() && !members.allow_empty {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let commit_id = members.commit_id;
|
|
630
|
+
let commit_change_id = members.commit_change_id;
|
|
631
|
+
let timestamp = members.created_at;
|
|
632
|
+
let _change_ids = members.change_ids;
|
|
633
|
+
let parent_commit_ids = version_ctx
|
|
634
|
+
.ref_reader(&mut *transaction)
|
|
635
|
+
.load_head_commit_id(&version_id)
|
|
636
|
+
.await?
|
|
637
|
+
.into_iter()
|
|
638
|
+
.collect::<Vec<_>>();
|
|
639
|
+
let parent_commit_ids = merge_parent_commit_ids(
|
|
640
|
+
parent_commit_ids,
|
|
641
|
+
extra_commit_parents_by_version
|
|
642
|
+
.get(&version_id)
|
|
643
|
+
.cloned()
|
|
644
|
+
.unwrap_or_default(),
|
|
645
|
+
);
|
|
646
|
+
let parent_commit_id = parent_commit_ids.first().cloned();
|
|
647
|
+
|
|
648
|
+
commit_rows.push(FinalizedCommitRow {
|
|
649
|
+
commit_id: commit_id.clone(),
|
|
650
|
+
parent_commit_ids: parent_commit_ids.clone(),
|
|
651
|
+
created_at: timestamp.clone(),
|
|
652
|
+
change_id: commit_change_id,
|
|
653
|
+
});
|
|
654
|
+
version_heads.push(PendingVersionHead {
|
|
655
|
+
version_id: version_id.clone(),
|
|
656
|
+
commit_id: commit_id.clone(),
|
|
657
|
+
timestamp,
|
|
658
|
+
});
|
|
659
|
+
tracked_roots.push(PendingTrackedRoot {
|
|
660
|
+
commit_id,
|
|
661
|
+
parent_commit_id,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
Ok(FinalizedCommitRows {
|
|
666
|
+
commit_rows,
|
|
667
|
+
version_heads,
|
|
668
|
+
tracked_roots,
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn merge_parent_commit_ids(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
|
|
673
|
+
for parent in extra {
|
|
674
|
+
if !base.contains(&parent) {
|
|
675
|
+
base.push(parent);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
base
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#[cfg(test)]
|
|
682
|
+
mod tests {
|
|
683
|
+
use std::collections::BTreeMap;
|
|
684
|
+
use std::sync::{
|
|
685
|
+
atomic::{AtomicUsize, Ordering},
|
|
686
|
+
Arc,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
use super::*;
|
|
690
|
+
use crate::backend::{
|
|
691
|
+
testing::UnitTestBackend, Backend, BackendKvEntryPage, BackendKvExistsBatch,
|
|
692
|
+
BackendKvGetRequest, BackendKvKeyPage, BackendKvScanRequest, BackendKvValueBatch,
|
|
693
|
+
BackendKvValuePage, BackendKvWriteBatch, BackendKvWriteStats, BackendReadTransaction,
|
|
694
|
+
BackendWriteTransaction,
|
|
695
|
+
};
|
|
696
|
+
use crate::catalog::SchemaPlanId;
|
|
697
|
+
use crate::commit_store::{ChangeIndexEntry, ChangeLocator};
|
|
698
|
+
use crate::live_state::{LiveStateContext, LiveStateRowRequest};
|
|
699
|
+
use crate::storage::StorageContext;
|
|
700
|
+
use crate::transaction::types::PreparedRowFacts;
|
|
701
|
+
use crate::untracked_state::{
|
|
702
|
+
MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateRowRequest,
|
|
703
|
+
};
|
|
704
|
+
use crate::version::VersionContext;
|
|
705
|
+
use crate::NullableKeyFilter;
|
|
706
|
+
use crate::GLOBAL_VERSION_ID;
|
|
707
|
+
use async_trait::async_trait;
|
|
708
|
+
|
|
709
|
+
const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
|
|
710
|
+
const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
|
|
711
|
+
|
|
712
|
+
fn live_state_context() -> LiveStateContext {
|
|
713
|
+
LiveStateContext::new(
|
|
714
|
+
crate::tracked_state::TrackedStateContext::new(),
|
|
715
|
+
crate::untracked_state::UntrackedStateContext::new(),
|
|
716
|
+
crate::commit_graph::CommitGraphContext::new(),
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#[tokio::test]
|
|
721
|
+
async fn commit_staged_writes_appends_commit_store_and_updates_serving_projection() {
|
|
722
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
723
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
724
|
+
let binary_cas = BinaryCasContext::new();
|
|
725
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
726
|
+
let mut transaction = storage
|
|
727
|
+
.begin_write_transaction()
|
|
728
|
+
.await
|
|
729
|
+
.expect("transaction should open");
|
|
730
|
+
|
|
731
|
+
let state_rows = vec![tracked_global_row("change-1")];
|
|
732
|
+
commit_prepared_writes(
|
|
733
|
+
&binary_cas,
|
|
734
|
+
&crate::commit_store::CommitStoreContext::new(),
|
|
735
|
+
&version_ctx,
|
|
736
|
+
None,
|
|
737
|
+
transaction.as_mut(),
|
|
738
|
+
PreparedWriteSet {
|
|
739
|
+
insert_identities: BTreeMap::new(),
|
|
740
|
+
state_rows,
|
|
741
|
+
adopted_rows: Vec::new(),
|
|
742
|
+
commit_members_by_version: BTreeMap::from([(
|
|
743
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
744
|
+
members(["change-1"]),
|
|
745
|
+
)]),
|
|
746
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
747
|
+
file_data_writes: Vec::new(),
|
|
748
|
+
},
|
|
749
|
+
)
|
|
750
|
+
.await
|
|
751
|
+
.expect("commit should flush staged rows");
|
|
752
|
+
transaction
|
|
753
|
+
.commit()
|
|
754
|
+
.await
|
|
755
|
+
.expect("commit should persist kv");
|
|
756
|
+
|
|
757
|
+
let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
|
|
758
|
+
let commit = commit_reader
|
|
759
|
+
.load_commit("test-uuid-1")
|
|
760
|
+
.await
|
|
761
|
+
.expect("commit-store commit should load")
|
|
762
|
+
.expect("commit-store commit should exist");
|
|
763
|
+
assert_eq!(commit.change_id, "test-uuid-2");
|
|
764
|
+
assert_eq!(commit.change_pack_count, 1);
|
|
765
|
+
assert_eq!(commit.membership_pack_count, 0);
|
|
766
|
+
let index_entries = commit_reader
|
|
767
|
+
.load_change_index_entries(&["change-1".to_string(), "test-uuid-2".to_string()])
|
|
768
|
+
.await
|
|
769
|
+
.expect("commit-store change index should load");
|
|
770
|
+
assert_eq!(
|
|
771
|
+
index_entries,
|
|
772
|
+
vec![
|
|
773
|
+
Some(ChangeIndexEntry::PackedChange {
|
|
774
|
+
locator: ChangeLocator {
|
|
775
|
+
source_commit_id: "test-uuid-1".to_string(),
|
|
776
|
+
source_pack_id: 0,
|
|
777
|
+
source_ordinal: 0,
|
|
778
|
+
change_id: "change-1".to_string(),
|
|
779
|
+
},
|
|
780
|
+
}),
|
|
781
|
+
Some(ChangeIndexEntry::CommitHeader {
|
|
782
|
+
commit_id: "test-uuid-1".to_string(),
|
|
783
|
+
change_id: "test-uuid-2".to_string(),
|
|
784
|
+
}),
|
|
785
|
+
]
|
|
786
|
+
);
|
|
787
|
+
let change_pack = commit_reader
|
|
788
|
+
.load_change_pack("test-uuid-1", 0)
|
|
789
|
+
.await
|
|
790
|
+
.expect("commit-store change pack should load")
|
|
791
|
+
.expect("commit-store change pack should exist");
|
|
792
|
+
assert_eq!(change_pack.len(), 1);
|
|
793
|
+
assert_eq!(change_pack[0].id, "change-1");
|
|
794
|
+
assert_eq!(change_pack[0].schema_key, "test_schema");
|
|
795
|
+
|
|
796
|
+
let loaded_head = version_ctx
|
|
797
|
+
.ref_reader(storage.clone())
|
|
798
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
799
|
+
.await
|
|
800
|
+
.expect("version ref load should succeed");
|
|
801
|
+
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
#[tokio::test]
|
|
805
|
+
async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
|
|
806
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
807
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
808
|
+
let binary_cas = BinaryCasContext::new();
|
|
809
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
810
|
+
let untracked_state = UntrackedStateContext::new();
|
|
811
|
+
let mut transaction = storage
|
|
812
|
+
.begin_write_transaction()
|
|
813
|
+
.await
|
|
814
|
+
.expect("transaction should open");
|
|
815
|
+
|
|
816
|
+
let state_rows = vec![untracked_global_row("change-untracked")];
|
|
817
|
+
commit_prepared_writes(
|
|
818
|
+
&binary_cas,
|
|
819
|
+
&crate::commit_store::CommitStoreContext::new(),
|
|
820
|
+
&version_ctx,
|
|
821
|
+
None,
|
|
822
|
+
transaction.as_mut(),
|
|
823
|
+
PreparedWriteSet {
|
|
824
|
+
insert_identities: BTreeMap::new(),
|
|
825
|
+
state_rows,
|
|
826
|
+
adopted_rows: Vec::new(),
|
|
827
|
+
commit_members_by_version: BTreeMap::new(),
|
|
828
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
829
|
+
file_data_writes: Vec::new(),
|
|
830
|
+
},
|
|
831
|
+
)
|
|
832
|
+
.await
|
|
833
|
+
.expect("commit should flush untracked row");
|
|
834
|
+
transaction
|
|
835
|
+
.commit()
|
|
836
|
+
.await
|
|
837
|
+
.expect("commit should persist kv");
|
|
838
|
+
|
|
839
|
+
let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
|
|
840
|
+
let index_entries = commit_reader
|
|
841
|
+
.load_change_index_entries(&["change-untracked".to_string()])
|
|
842
|
+
.await
|
|
843
|
+
.expect("commit-store change index should load");
|
|
844
|
+
assert_eq!(index_entries, vec![None]);
|
|
845
|
+
|
|
846
|
+
let loaded = {
|
|
847
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
848
|
+
untracked_reader
|
|
849
|
+
.load_row(&UntrackedStateRowRequest {
|
|
850
|
+
schema_key: "test_schema".to_string(),
|
|
851
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
852
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
853
|
+
file_id: NullableKeyFilter::Null,
|
|
854
|
+
})
|
|
855
|
+
.await
|
|
856
|
+
}
|
|
857
|
+
.expect("untracked row load should succeed")
|
|
858
|
+
.expect("untracked row should be persisted");
|
|
859
|
+
assert_eq!(
|
|
860
|
+
loaded.snapshot_content.as_deref(),
|
|
861
|
+
Some("{\"value\":\"untracked\"}")
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
#[tokio::test]
|
|
866
|
+
async fn tracked_write_deletes_matching_untracked_overlay() {
|
|
867
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
868
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
869
|
+
let binary_cas = BinaryCasContext::new();
|
|
870
|
+
let untracked_state = UntrackedStateContext::new();
|
|
871
|
+
let live_state = Arc::new(live_state_context());
|
|
872
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
873
|
+
|
|
874
|
+
let mut seed_transaction = storage
|
|
875
|
+
.begin_write_transaction()
|
|
876
|
+
.await
|
|
877
|
+
.expect("seed transaction should open");
|
|
878
|
+
let mut writes = StorageWriteSet::new();
|
|
879
|
+
let staged_row = untracked_global_row("change-untracked");
|
|
880
|
+
let canonical_row = crate::test_support::untracked_state_row_from_materialized(
|
|
881
|
+
&mut writes,
|
|
882
|
+
&MaterializedUntrackedStateRow::from(staged_row),
|
|
883
|
+
)
|
|
884
|
+
.expect("untracked seed should canonicalize");
|
|
885
|
+
untracked_state
|
|
886
|
+
.writer(&mut writes)
|
|
887
|
+
.stage_rows(std::iter::once(canonical_row.as_ref()))
|
|
888
|
+
.expect("untracked seed should write");
|
|
889
|
+
writes
|
|
890
|
+
.apply(&mut seed_transaction.as_mut())
|
|
891
|
+
.await
|
|
892
|
+
.expect("untracked seed should apply");
|
|
893
|
+
seed_transaction
|
|
894
|
+
.commit()
|
|
895
|
+
.await
|
|
896
|
+
.expect("seed transaction should persist");
|
|
897
|
+
|
|
898
|
+
let mut transaction = storage
|
|
899
|
+
.begin_write_transaction()
|
|
900
|
+
.await
|
|
901
|
+
.expect("transaction should open");
|
|
902
|
+
let state_rows = vec![tracked_global_row("change-tracked")];
|
|
903
|
+
commit_prepared_writes(
|
|
904
|
+
&binary_cas,
|
|
905
|
+
&crate::commit_store::CommitStoreContext::new(),
|
|
906
|
+
&version_ctx,
|
|
907
|
+
None,
|
|
908
|
+
transaction.as_mut(),
|
|
909
|
+
PreparedWriteSet {
|
|
910
|
+
insert_identities: BTreeMap::new(),
|
|
911
|
+
state_rows,
|
|
912
|
+
adopted_rows: Vec::new(),
|
|
913
|
+
commit_members_by_version: BTreeMap::from([(
|
|
914
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
915
|
+
members(["change-tracked"]),
|
|
916
|
+
)]),
|
|
917
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
918
|
+
file_data_writes: Vec::new(),
|
|
919
|
+
},
|
|
920
|
+
)
|
|
921
|
+
.await
|
|
922
|
+
.expect("tracked commit should flush");
|
|
923
|
+
transaction
|
|
924
|
+
.commit()
|
|
925
|
+
.await
|
|
926
|
+
.expect("commit should persist kv");
|
|
927
|
+
|
|
928
|
+
let untracked = {
|
|
929
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
930
|
+
untracked_reader.load_row(&untracked_request()).await
|
|
931
|
+
}
|
|
932
|
+
.expect("untracked load should succeed");
|
|
933
|
+
assert_eq!(untracked, None);
|
|
934
|
+
|
|
935
|
+
let visible = live_state
|
|
936
|
+
.reader(storage.clone())
|
|
937
|
+
.load_row(&live_state_request())
|
|
938
|
+
.await
|
|
939
|
+
.expect("live-state load should succeed")
|
|
940
|
+
.expect("tracked row should be visible");
|
|
941
|
+
assert!(!visible.untracked);
|
|
942
|
+
assert_eq!(visible.change_id.as_deref(), Some("change-tracked"));
|
|
943
|
+
assert_eq!(visible.snapshot_content.as_deref(), Some("{\"value\":1}"));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
#[tokio::test]
|
|
947
|
+
async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
|
|
948
|
+
let counting_backend = Arc::new(CountingBackend::new());
|
|
949
|
+
let write_batches = counting_backend.write_batches();
|
|
950
|
+
let backend: Arc<dyn Backend + Send + Sync> = counting_backend;
|
|
951
|
+
let storage = StorageContext::new(backend);
|
|
952
|
+
let binary_cas = BinaryCasContext::new();
|
|
953
|
+
let live_state = Arc::new(live_state_context());
|
|
954
|
+
let untracked_state = UntrackedStateContext::new();
|
|
955
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
956
|
+
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
957
|
+
{
|
|
958
|
+
let mut seed_transaction = storage
|
|
959
|
+
.begin_write_transaction()
|
|
960
|
+
.await
|
|
961
|
+
.expect("seed transaction should open");
|
|
962
|
+
let mut writes = StorageWriteSet::new();
|
|
963
|
+
let mode_snapshot = serde_json::to_string(&serde_json::json!({
|
|
964
|
+
"key": DETERMINISTIC_MODE_KEY,
|
|
965
|
+
"value": { "enabled": true },
|
|
966
|
+
}))
|
|
967
|
+
.expect("mode snapshot should serialize");
|
|
968
|
+
JsonStoreContext::new()
|
|
969
|
+
.writer()
|
|
970
|
+
.stage_batch(
|
|
971
|
+
&mut writes,
|
|
972
|
+
JsonWritePlacementRef::OutOfBand,
|
|
973
|
+
[NormalizedJsonRef::new(mode_snapshot.as_str())],
|
|
974
|
+
)
|
|
975
|
+
.expect("deterministic mode snapshot should stage");
|
|
976
|
+
let row = crate::untracked_state::UntrackedStateRow {
|
|
977
|
+
entity_id: crate::entity_identity::EntityIdentity::single(DETERMINISTIC_MODE_KEY),
|
|
978
|
+
schema_key: "lix_key_value".to_string(),
|
|
979
|
+
file_id: None,
|
|
980
|
+
snapshot_content: Some(mode_snapshot.to_string()),
|
|
981
|
+
metadata: None,
|
|
982
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
983
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
984
|
+
global: true,
|
|
985
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
986
|
+
};
|
|
987
|
+
UntrackedStateContext::new()
|
|
988
|
+
.writer(&mut writes)
|
|
989
|
+
.stage_rows(std::iter::once(row.as_ref()))
|
|
990
|
+
.expect("deterministic mode should stage");
|
|
991
|
+
writes
|
|
992
|
+
.apply(&mut seed_transaction.as_mut())
|
|
993
|
+
.await
|
|
994
|
+
.expect("deterministic mode should apply");
|
|
995
|
+
seed_transaction
|
|
996
|
+
.commit()
|
|
997
|
+
.await
|
|
998
|
+
.expect("seed transaction should persist");
|
|
999
|
+
}
|
|
1000
|
+
write_batches.store(0, Ordering::SeqCst);
|
|
1001
|
+
let runtime_functions = {
|
|
1002
|
+
let reader = live_state.reader(storage.clone());
|
|
1003
|
+
FunctionContext::prepare(&reader)
|
|
1004
|
+
.await
|
|
1005
|
+
.expect("runtime context should prepare")
|
|
1006
|
+
};
|
|
1007
|
+
runtime_functions.provider().call_uuid_v7();
|
|
1008
|
+
let mut transaction = storage
|
|
1009
|
+
.begin_write_transaction()
|
|
1010
|
+
.await
|
|
1011
|
+
.expect("transaction should open");
|
|
1012
|
+
|
|
1013
|
+
let tracked_row = tracked_global_row("change-tracked");
|
|
1014
|
+
let mut untracked_row = untracked_global_row("change-untracked");
|
|
1015
|
+
untracked_row.entity_id = crate::entity_identity::EntityIdentity::single("entity-2");
|
|
1016
|
+
|
|
1017
|
+
commit_prepared_writes(
|
|
1018
|
+
&binary_cas,
|
|
1019
|
+
&crate::commit_store::CommitStoreContext::new(),
|
|
1020
|
+
&version_ctx,
|
|
1021
|
+
Some(&runtime_functions),
|
|
1022
|
+
transaction.as_mut(),
|
|
1023
|
+
PreparedWriteSet {
|
|
1024
|
+
insert_identities: BTreeMap::new(),
|
|
1025
|
+
state_rows: vec![tracked_row, untracked_row],
|
|
1026
|
+
adopted_rows: Vec::new(),
|
|
1027
|
+
commit_members_by_version: BTreeMap::from([(
|
|
1028
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
1029
|
+
members(["change-tracked"]),
|
|
1030
|
+
)]),
|
|
1031
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
1032
|
+
file_data_writes: Vec::new(),
|
|
1033
|
+
},
|
|
1034
|
+
)
|
|
1035
|
+
.await
|
|
1036
|
+
.expect("cross-subsystem commit should stage and apply");
|
|
1037
|
+
|
|
1038
|
+
assert_eq!(
|
|
1039
|
+
write_batches.load(Ordering::SeqCst),
|
|
1040
|
+
1,
|
|
1041
|
+
"tracked, json, untracked, commit-store, and version refs must apply as one backend write batch"
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
transaction
|
|
1045
|
+
.commit()
|
|
1046
|
+
.await
|
|
1047
|
+
.expect("commit should persist kv");
|
|
1048
|
+
assert_eq!(write_batches.load(Ordering::SeqCst), 1);
|
|
1049
|
+
|
|
1050
|
+
let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
|
|
1051
|
+
let commit = commit_reader
|
|
1052
|
+
.load_commit("test-uuid-1")
|
|
1053
|
+
.await
|
|
1054
|
+
.expect("commit-store commit should load")
|
|
1055
|
+
.expect("commit-store commit should exist");
|
|
1056
|
+
assert_eq!(commit.change_id, "test-uuid-2");
|
|
1057
|
+
let index_entries = commit_reader
|
|
1058
|
+
.load_change_index_entries(&["change-tracked".to_string()])
|
|
1059
|
+
.await
|
|
1060
|
+
.expect("commit-store change index should load");
|
|
1061
|
+
assert!(matches!(
|
|
1062
|
+
index_entries.as_slice(),
|
|
1063
|
+
[Some(ChangeIndexEntry::PackedChange { .. })]
|
|
1064
|
+
));
|
|
1065
|
+
|
|
1066
|
+
let loaded_head = version_ctx
|
|
1067
|
+
.ref_reader(storage.clone())
|
|
1068
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
1069
|
+
.await
|
|
1070
|
+
.expect("version ref load should succeed");
|
|
1071
|
+
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
1072
|
+
|
|
1073
|
+
let untracked = {
|
|
1074
|
+
let mut untracked_reader = untracked_state.reader(storage.clone());
|
|
1075
|
+
untracked_reader
|
|
1076
|
+
.load_row(&UntrackedStateRowRequest {
|
|
1077
|
+
schema_key: "test_schema".to_string(),
|
|
1078
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1079
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-2"),
|
|
1080
|
+
file_id: NullableKeyFilter::Null,
|
|
1081
|
+
})
|
|
1082
|
+
.await
|
|
1083
|
+
}
|
|
1084
|
+
.expect("untracked row load should succeed")
|
|
1085
|
+
.expect("untracked row should persist");
|
|
1086
|
+
assert_eq!(
|
|
1087
|
+
untracked.snapshot_content.as_deref(),
|
|
1088
|
+
Some("{\"value\":\"untracked\"}")
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
let sequence_row = live_state
|
|
1092
|
+
.reader(storage.clone())
|
|
1093
|
+
.load_row(&LiveStateRowRequest {
|
|
1094
|
+
schema_key: "lix_key_value".to_string(),
|
|
1095
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1096
|
+
entity_id: crate::entity_identity::EntityIdentity::single(
|
|
1097
|
+
DETERMINISTIC_SEQUENCE_KEY,
|
|
1098
|
+
),
|
|
1099
|
+
file_id: NullableKeyFilter::Null,
|
|
1100
|
+
})
|
|
1101
|
+
.await
|
|
1102
|
+
.expect("deterministic sequence should load")
|
|
1103
|
+
.expect("deterministic sequence should persist");
|
|
1104
|
+
assert_eq!(
|
|
1105
|
+
sequence_row.snapshot_content.as_deref(),
|
|
1106
|
+
Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":0}")
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
#[tokio::test]
|
|
1111
|
+
async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_version() {
|
|
1112
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1113
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1114
|
+
let binary_cas = BinaryCasContext::new();
|
|
1115
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1116
|
+
crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
|
|
1117
|
+
.await;
|
|
1118
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "version-a-before")
|
|
1119
|
+
.await;
|
|
1120
|
+
|
|
1121
|
+
let mut transaction = storage
|
|
1122
|
+
.begin_write_transaction()
|
|
1123
|
+
.await
|
|
1124
|
+
.expect("transaction should open");
|
|
1125
|
+
let state_rows = vec![tracked_version_row("version-a", "change-version-a")];
|
|
1126
|
+
commit_prepared_writes(
|
|
1127
|
+
&binary_cas,
|
|
1128
|
+
&crate::commit_store::CommitStoreContext::new(),
|
|
1129
|
+
&version_ctx,
|
|
1130
|
+
None,
|
|
1131
|
+
transaction.as_mut(),
|
|
1132
|
+
PreparedWriteSet {
|
|
1133
|
+
insert_identities: BTreeMap::new(),
|
|
1134
|
+
state_rows,
|
|
1135
|
+
adopted_rows: Vec::new(),
|
|
1136
|
+
commit_members_by_version: BTreeMap::from([(
|
|
1137
|
+
"version-a".to_string(),
|
|
1138
|
+
members(["change-version-a"]),
|
|
1139
|
+
)]),
|
|
1140
|
+
extra_commit_parents_by_version: BTreeMap::new(),
|
|
1141
|
+
file_data_writes: Vec::new(),
|
|
1142
|
+
},
|
|
1143
|
+
)
|
|
1144
|
+
.await
|
|
1145
|
+
.expect("version commit should flush");
|
|
1146
|
+
transaction
|
|
1147
|
+
.commit()
|
|
1148
|
+
.await
|
|
1149
|
+
.expect("commit should persist kv");
|
|
1150
|
+
|
|
1151
|
+
let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
|
|
1152
|
+
let commit = commit_reader
|
|
1153
|
+
.load_commit("test-uuid-1")
|
|
1154
|
+
.await
|
|
1155
|
+
.expect("commit-store commit should load")
|
|
1156
|
+
.expect("commit-store commit should exist");
|
|
1157
|
+
assert_eq!(commit.change_id, "test-uuid-2");
|
|
1158
|
+
assert_eq!(commit.parent_ids, vec!["version-a-before"]);
|
|
1159
|
+
let index_entries = commit_reader
|
|
1160
|
+
.load_change_index_entries(&["change-version-a".to_string()])
|
|
1161
|
+
.await
|
|
1162
|
+
.expect("commit-store change index should load");
|
|
1163
|
+
assert!(matches!(
|
|
1164
|
+
index_entries.as_slice(),
|
|
1165
|
+
[Some(ChangeIndexEntry::PackedChange { .. })]
|
|
1166
|
+
));
|
|
1167
|
+
|
|
1168
|
+
let global_head = version_ctx
|
|
1169
|
+
.ref_reader(storage.clone())
|
|
1170
|
+
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
1171
|
+
.await
|
|
1172
|
+
.expect("global head should load");
|
|
1173
|
+
let version_head = version_ctx
|
|
1174
|
+
.ref_reader(storage.clone())
|
|
1175
|
+
.load_head_commit_id("version-a")
|
|
1176
|
+
.await
|
|
1177
|
+
.expect("version head should load");
|
|
1178
|
+
assert_eq!(global_head.as_deref(), Some("global-before"));
|
|
1179
|
+
assert_eq!(version_head.as_deref(), Some("test-uuid-1"));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
#[tokio::test]
|
|
1183
|
+
async fn finalize_commit_rows_parents_global_commit_to_existing_version_ref() {
|
|
1184
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1185
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1186
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1187
|
+
crate::test_support::seed_version_head(
|
|
1188
|
+
storage.clone(),
|
|
1189
|
+
GLOBAL_VERSION_ID,
|
|
1190
|
+
"initial-commit",
|
|
1191
|
+
)
|
|
1192
|
+
.await;
|
|
1193
|
+
|
|
1194
|
+
let mut transaction = storage
|
|
1195
|
+
.begin_write_transaction()
|
|
1196
|
+
.await
|
|
1197
|
+
.expect("transaction should open");
|
|
1198
|
+
let rows = finalize_commit_rows(
|
|
1199
|
+
BTreeMap::from([(
|
|
1200
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
1201
|
+
members(["change-a", "change-b"]),
|
|
1202
|
+
)]),
|
|
1203
|
+
BTreeMap::new(),
|
|
1204
|
+
&version_ctx,
|
|
1205
|
+
transaction.as_mut(),
|
|
1206
|
+
)
|
|
1207
|
+
.await
|
|
1208
|
+
.expect("global commit row should finalize");
|
|
1209
|
+
|
|
1210
|
+
assert_eq!(rows.commit_rows.len(), 1);
|
|
1211
|
+
assert_eq!(rows.version_heads.len(), 1);
|
|
1212
|
+
let row = &rows.commit_rows[0];
|
|
1213
|
+
assert_eq!(row.commit_id, "test-uuid-1");
|
|
1214
|
+
assert_eq!(row.change_id, "test-uuid-2");
|
|
1215
|
+
assert_eq!(row.created_at, "test-timestamp-1");
|
|
1216
|
+
assert_eq!(row.parent_commit_ids, vec!["initial-commit"]);
|
|
1217
|
+
|
|
1218
|
+
let version_head = &rows.version_heads[0];
|
|
1219
|
+
assert_eq!(version_head.version_id, GLOBAL_VERSION_ID);
|
|
1220
|
+
assert_eq!(version_head.commit_id, "test-uuid-1");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
#[tokio::test]
|
|
1224
|
+
async fn finalize_commit_rows_skips_empty_members() {
|
|
1225
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1226
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1227
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1228
|
+
let mut transaction = storage
|
|
1229
|
+
.begin_write_transaction()
|
|
1230
|
+
.await
|
|
1231
|
+
.expect("transaction should open");
|
|
1232
|
+
let rows = finalize_commit_rows(
|
|
1233
|
+
BTreeMap::from([(
|
|
1234
|
+
GLOBAL_VERSION_ID.to_string(),
|
|
1235
|
+
StagedCommitMembers::default(),
|
|
1236
|
+
)]),
|
|
1237
|
+
BTreeMap::new(),
|
|
1238
|
+
&version_ctx,
|
|
1239
|
+
transaction.as_mut(),
|
|
1240
|
+
)
|
|
1241
|
+
.await
|
|
1242
|
+
.expect("empty members should be ignored");
|
|
1243
|
+
|
|
1244
|
+
assert!(rows.commit_rows.is_empty());
|
|
1245
|
+
assert!(rows.version_heads.is_empty());
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
#[tokio::test]
|
|
1249
|
+
async fn finalize_commit_rows_uses_existing_version_ref_as_parent() {
|
|
1250
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1251
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1252
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1253
|
+
crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
|
|
1254
|
+
.await;
|
|
1255
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "previous-commit")
|
|
1256
|
+
.await;
|
|
1257
|
+
|
|
1258
|
+
let mut transaction = storage
|
|
1259
|
+
.begin_write_transaction()
|
|
1260
|
+
.await
|
|
1261
|
+
.expect("transaction should open");
|
|
1262
|
+
let rows = finalize_commit_rows(
|
|
1263
|
+
BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
|
|
1264
|
+
BTreeMap::new(),
|
|
1265
|
+
&version_ctx,
|
|
1266
|
+
transaction.as_mut(),
|
|
1267
|
+
)
|
|
1268
|
+
.await
|
|
1269
|
+
.expect("active-version commit finalization should resolve parent");
|
|
1270
|
+
|
|
1271
|
+
assert_eq!(
|
|
1272
|
+
rows.commit_rows[0].parent_commit_ids,
|
|
1273
|
+
vec!["previous-commit"]
|
|
1274
|
+
);
|
|
1275
|
+
assert_eq!(rows.version_heads[0].version_id, "version-a");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
#[tokio::test]
|
|
1279
|
+
async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
|
|
1280
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1281
|
+
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1282
|
+
let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1283
|
+
crate::test_support::seed_version_head(storage.clone(), "version-a", "target-head").await;
|
|
1284
|
+
|
|
1285
|
+
let mut transaction = storage
|
|
1286
|
+
.begin_write_transaction()
|
|
1287
|
+
.await
|
|
1288
|
+
.expect("transaction should open");
|
|
1289
|
+
let rows = finalize_commit_rows(
|
|
1290
|
+
BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
|
|
1291
|
+
BTreeMap::from([("version-a".to_string(), vec!["source-head".to_string()])]),
|
|
1292
|
+
&version_ctx,
|
|
1293
|
+
transaction.as_mut(),
|
|
1294
|
+
)
|
|
1295
|
+
.await
|
|
1296
|
+
.expect("merge commit finalization should resolve parents");
|
|
1297
|
+
|
|
1298
|
+
assert_eq!(
|
|
1299
|
+
rows.commit_rows[0].parent_commit_ids,
|
|
1300
|
+
vec!["target-head", "source-head"]
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
fn members<const N: usize>(change_ids: [&str; N]) -> StagedCommitMembers {
|
|
1305
|
+
let mut members = StagedCommitMembers::new(
|
|
1306
|
+
"test-uuid-1".to_string(),
|
|
1307
|
+
"test-uuid-2".to_string(),
|
|
1308
|
+
"test-timestamp-1".to_string(),
|
|
1309
|
+
);
|
|
1310
|
+
for change_id in change_ids {
|
|
1311
|
+
members.add_change_id(change_id.to_string());
|
|
1312
|
+
}
|
|
1313
|
+
members
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
fn tracked_global_row(change_id: &str) -> PreparedStateRow {
|
|
1317
|
+
tracked_version_row(GLOBAL_VERSION_ID, change_id)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
fn tracked_version_row(version_id: &str, change_id: &str) -> PreparedStateRow {
|
|
1321
|
+
PreparedStateRow {
|
|
1322
|
+
schema_plan_id: SchemaPlanId::for_test(0),
|
|
1323
|
+
facts: PreparedRowFacts::default(),
|
|
1324
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1325
|
+
schema_key: "test_schema".to_string(),
|
|
1326
|
+
file_id: None,
|
|
1327
|
+
snapshot: Some(
|
|
1328
|
+
crate::transaction::types::stage_json_from_value(
|
|
1329
|
+
crate::transaction::types::TransactionJson::from_value_for_test(
|
|
1330
|
+
serde_json::json!({ "value": 1 }),
|
|
1331
|
+
),
|
|
1332
|
+
"test tracked row snapshot",
|
|
1333
|
+
)
|
|
1334
|
+
.expect("test snapshot should stage"),
|
|
1335
|
+
),
|
|
1336
|
+
metadata: None,
|
|
1337
|
+
origin: None,
|
|
1338
|
+
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1339
|
+
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1340
|
+
global: version_id == GLOBAL_VERSION_ID,
|
|
1341
|
+
change_id: Some(change_id.to_string()),
|
|
1342
|
+
commit_id: Some("test-uuid-1".to_string()),
|
|
1343
|
+
untracked: false,
|
|
1344
|
+
version_id: version_id.to_string(),
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
fn untracked_global_row(change_id: &str) -> PreparedStateRow {
|
|
1349
|
+
let mut row = tracked_global_row(change_id);
|
|
1350
|
+
row.snapshot = Some(
|
|
1351
|
+
crate::transaction::types::stage_json_from_value(
|
|
1352
|
+
crate::transaction::types::TransactionJson::from_value_for_test(
|
|
1353
|
+
serde_json::json!({ "value": "untracked" }),
|
|
1354
|
+
),
|
|
1355
|
+
"test untracked row snapshot",
|
|
1356
|
+
)
|
|
1357
|
+
.expect("test snapshot should stage"),
|
|
1358
|
+
);
|
|
1359
|
+
PreparedStateRow {
|
|
1360
|
+
change_id: None,
|
|
1361
|
+
commit_id: None,
|
|
1362
|
+
untracked: true,
|
|
1363
|
+
..row
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
fn untracked_request() -> UntrackedStateRowRequest {
|
|
1368
|
+
UntrackedStateRowRequest {
|
|
1369
|
+
schema_key: "test_schema".to_string(),
|
|
1370
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1371
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1372
|
+
file_id: NullableKeyFilter::Null,
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
fn live_state_request() -> LiveStateRowRequest {
|
|
1377
|
+
LiveStateRowRequest {
|
|
1378
|
+
schema_key: "test_schema".to_string(),
|
|
1379
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1380
|
+
entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
|
|
1381
|
+
file_id: NullableKeyFilter::Null,
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
struct CountingBackend {
|
|
1386
|
+
inner: UnitTestBackend,
|
|
1387
|
+
write_batches: Arc<AtomicUsize>,
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
impl CountingBackend {
|
|
1391
|
+
fn new() -> Self {
|
|
1392
|
+
Self {
|
|
1393
|
+
inner: UnitTestBackend::new(),
|
|
1394
|
+
write_batches: Arc::new(AtomicUsize::new(0)),
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
fn write_batches(&self) -> Arc<AtomicUsize> {
|
|
1399
|
+
Arc::clone(&self.write_batches)
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
#[async_trait]
|
|
1404
|
+
impl Backend for CountingBackend {
|
|
1405
|
+
async fn begin_read_transaction(
|
|
1406
|
+
&self,
|
|
1407
|
+
) -> Result<Box<dyn BackendReadTransaction + Send + Sync + 'static>, LixError> {
|
|
1408
|
+
self.inner.begin_read_transaction().await
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async fn begin_write_transaction(
|
|
1412
|
+
&self,
|
|
1413
|
+
) -> Result<Box<dyn BackendWriteTransaction + Send + Sync + 'static>, LixError> {
|
|
1414
|
+
Ok(Box::new(CountingWriteTransaction {
|
|
1415
|
+
inner: self.inner.begin_write_transaction().await?,
|
|
1416
|
+
write_batches: Arc::clone(&self.write_batches),
|
|
1417
|
+
}))
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
struct CountingWriteTransaction {
|
|
1422
|
+
inner: Box<dyn BackendWriteTransaction + Send + Sync + 'static>,
|
|
1423
|
+
write_batches: Arc<AtomicUsize>,
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
#[async_trait]
|
|
1427
|
+
impl BackendReadTransaction for CountingWriteTransaction {
|
|
1428
|
+
async fn get_values(
|
|
1429
|
+
&mut self,
|
|
1430
|
+
request: BackendKvGetRequest,
|
|
1431
|
+
) -> Result<BackendKvValueBatch, LixError> {
|
|
1432
|
+
self.inner.get_values(request).await
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
async fn exists_many(
|
|
1436
|
+
&mut self,
|
|
1437
|
+
request: BackendKvGetRequest,
|
|
1438
|
+
) -> Result<BackendKvExistsBatch, LixError> {
|
|
1439
|
+
self.inner.exists_many(request).await
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
async fn scan_keys(
|
|
1443
|
+
&mut self,
|
|
1444
|
+
request: BackendKvScanRequest,
|
|
1445
|
+
) -> Result<BackendKvKeyPage, LixError> {
|
|
1446
|
+
self.inner.scan_keys(request).await
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async fn scan_values(
|
|
1450
|
+
&mut self,
|
|
1451
|
+
request: BackendKvScanRequest,
|
|
1452
|
+
) -> Result<BackendKvValuePage, LixError> {
|
|
1453
|
+
self.inner.scan_values(request).await
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async fn scan_entries(
|
|
1457
|
+
&mut self,
|
|
1458
|
+
request: BackendKvScanRequest,
|
|
1459
|
+
) -> Result<BackendKvEntryPage, LixError> {
|
|
1460
|
+
self.inner.scan_entries(request).await
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async fn rollback(self: Box<Self>) -> Result<(), LixError> {
|
|
1464
|
+
let Self { inner, .. } = *self;
|
|
1465
|
+
inner.rollback().await
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
#[async_trait]
|
|
1470
|
+
impl BackendWriteTransaction for CountingWriteTransaction {
|
|
1471
|
+
async fn write_kv_batch(
|
|
1472
|
+
&mut self,
|
|
1473
|
+
batch: BackendKvWriteBatch,
|
|
1474
|
+
) -> Result<BackendKvWriteStats, LixError> {
|
|
1475
|
+
self.write_batches.fetch_add(1, Ordering::SeqCst);
|
|
1476
|
+
self.inner.write_kv_batch(batch).await
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
async fn commit(self: Box<Self>) -> Result<(), LixError> {
|
|
1480
|
+
let Self { inner, .. } = *self;
|
|
1481
|
+
inner.commit().await
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|