@lix-js/sdk 0.6.0-preview.5 → 0.6.1
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 +76 -4
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -3
- package/dist/native.d.ts +1 -0
- package/dist/native.js +47 -0
- package/dist/open-lix.d.ts +38 -207
- package/dist/open-lix.js +59 -284
- package/dist/result.d.ts +18 -0
- package/dist/result.js +48 -0
- package/dist/types.d.ts +114 -1
- package/dist/value.d.ts +28 -0
- package/dist/value.js +245 -0
- package/package.json +38 -71
- package/SKILL.md +0 -507
- package/dist/builtin-schemas.d.ts +0 -1
- package/dist/builtin-schemas.js +0 -1
- package/dist/engine-wasm/index.d.ts +0 -87
- package/dist/engine-wasm/index.js +0 -339
- package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
- package/dist/engine-wasm/wasm/lix_engine.js +0 -833
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
- package/dist/generated/builtin-schemas.d.ts +0 -427
- package/dist/generated/builtin-schemas.js +0 -643
- package/dist/sqlite/index.d.ts +0 -12
- package/dist/sqlite/index.js +0 -359
- package/dist-engine-src/README.md +0 -18
- package/dist-engine-src/src/backend/capabilities.rs +0 -67
- package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
- package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
- package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
- package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
- package/dist-engine-src/src/backend/conformance/model.rs +0 -28
- package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
- package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
- package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
- package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
- package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
- package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
- package/dist-engine-src/src/backend/conformance/write.rs +0 -16
- package/dist-engine-src/src/backend/error.rs +0 -94
- package/dist-engine-src/src/backend/in_memory.rs +0 -670
- package/dist-engine-src/src/backend/mod.rs +0 -39
- package/dist-engine-src/src/backend/predicate.rs +0 -80
- package/dist-engine-src/src/backend/traits.rs +0 -260
- package/dist-engine-src/src/backend/types.rs +0 -239
- package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
- package/dist-engine-src/src/binary_cas/codec.rs +0 -346
- package/dist-engine-src/src/binary_cas/context.rs +0 -139
- package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
- package/dist-engine-src/src/binary_cas/mod.rs +0 -11
- package/dist-engine-src/src/binary_cas/types.rs +0 -121
- package/dist-engine-src/src/branch/context.rs +0 -40
- package/dist-engine-src/src/branch/lifecycle.rs +0 -221
- package/dist-engine-src/src/branch/mod.rs +0 -13
- package/dist-engine-src/src/branch/refs.rs +0 -321
- package/dist-engine-src/src/branch/stage_rows.rs +0 -67
- package/dist-engine-src/src/branch/types.rs +0 -21
- package/dist-engine-src/src/catalog/context.rs +0 -412
- package/dist-engine-src/src/catalog/mod.rs +0 -10
- package/dist-engine-src/src/catalog/schema.rs +0 -4
- package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
- package/dist-engine-src/src/cel/context.rs +0 -86
- package/dist-engine-src/src/cel/error.rs +0 -19
- package/dist-engine-src/src/cel/mod.rs +0 -8
- package/dist-engine-src/src/cel/provider.rs +0 -9
- package/dist-engine-src/src/cel/runtime.rs +0 -167
- package/dist-engine-src/src/cel/value.rs +0 -50
- package/dist-engine-src/src/changelog/bench_support.rs +0 -785
- package/dist-engine-src/src/changelog/change.rs +0 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -497
- package/dist-engine-src/src/changelog/commit.rs +0 -1
- package/dist-engine-src/src/changelog/context.rs +0 -1614
- package/dist-engine-src/src/changelog/mod.rs +0 -29
- package/dist-engine-src/src/changelog/store.rs +0 -163
- package/dist-engine-src/src/changelog/test_support.rs +0 -54
- package/dist-engine-src/src/changelog/types.rs +0 -213
- package/dist-engine-src/src/commit_graph/context.rs +0 -944
- package/dist-engine-src/src/commit_graph/mod.rs +0 -9
- package/dist-engine-src/src/commit_graph/types.rs +0 -89
- package/dist-engine-src/src/commit_graph/walker.rs +0 -786
- package/dist-engine-src/src/common/error.rs +0 -347
- package/dist-engine-src/src/common/fingerprint.rs +0 -3
- package/dist-engine-src/src/common/fs_path.rs +0 -1336
- package/dist-engine-src/src/common/identity.rs +0 -145
- package/dist-engine-src/src/common/json_pointer.rs +0 -67
- package/dist-engine-src/src/common/metadata.rs +0 -40
- package/dist-engine-src/src/common/mod.rs +0 -23
- package/dist-engine-src/src/common/types.rs +0 -105
- package/dist-engine-src/src/common/wire.rs +0 -222
- package/dist-engine-src/src/domain.rs +0 -320
- package/dist-engine-src/src/engine.rs +0 -203
- package/dist-engine-src/src/entity_pk.rs +0 -402
- package/dist-engine-src/src/functions/context.rs +0 -296
- package/dist-engine-src/src/functions/deterministic.rs +0 -113
- package/dist-engine-src/src/functions/mod.rs +0 -18
- package/dist-engine-src/src/functions/provider.rs +0 -130
- package/dist-engine-src/src/functions/state.rs +0 -335
- package/dist-engine-src/src/functions/types.rs +0 -37
- package/dist-engine-src/src/init.rs +0 -692
- package/dist-engine-src/src/json_store/compression.rs +0 -77
- package/dist-engine-src/src/json_store/context.rs +0 -172
- package/dist-engine-src/src/json_store/encoded.rs +0 -15
- package/dist-engine-src/src/json_store/mod.rs +0 -38
- package/dist-engine-src/src/json_store/store.rs +0 -494
- package/dist-engine-src/src/json_store/types.rs +0 -212
- package/dist-engine-src/src/lib.rs +0 -92
- package/dist-engine-src/src/live_state/context.rs +0 -1883
- package/dist-engine-src/src/live_state/mod.rs +0 -21
- package/dist-engine-src/src/live_state/overlay.rs +0 -75
- package/dist-engine-src/src/live_state/reader.rs +0 -23
- package/dist-engine-src/src/live_state/types.rs +0 -231
- package/dist-engine-src/src/live_state/visibility.rs +0 -666
- package/dist-engine-src/src/plugin/archive.rs +0 -438
- package/dist-engine-src/src/plugin/component.rs +0 -183
- package/dist-engine-src/src/plugin/install.rs +0 -619
- package/dist-engine-src/src/plugin/manifest.rs +0 -516
- package/dist-engine-src/src/plugin/materializer.rs +0 -202
- package/dist-engine-src/src/plugin/mod.rs +0 -33
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
- package/dist-engine-src/src/plugin/storage.rs +0 -74
- package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
- package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
- package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
- package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
- package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
- package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
- package/dist-engine-src/src/schema/compatibility.rs +0 -787
- package/dist-engine-src/src/schema/definition.json +0 -187
- package/dist-engine-src/src/schema/definition.rs +0 -742
- package/dist-engine-src/src/schema/key.rs +0 -138
- package/dist-engine-src/src/schema/mod.rs +0 -20
- package/dist-engine-src/src/schema/seed.rs +0 -14
- package/dist-engine-src/src/schema/tests.rs +0 -780
- package/dist-engine-src/src/session/context.rs +0 -1059
- package/dist-engine-src/src/session/create_branch.rs +0 -94
- package/dist-engine-src/src/session/execute.rs +0 -681
- package/dist-engine-src/src/session/merge/analysis.rs +0 -108
- package/dist-engine-src/src/session/merge/branch.rs +0 -417
- package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
- package/dist-engine-src/src/session/merge/mod.rs +0 -10
- package/dist-engine-src/src/session/merge/stats.rs +0 -61
- package/dist-engine-src/src/session/mod.rs +0 -30
- package/dist-engine-src/src/session/switch_branch.rs +0 -113
- package/dist-engine-src/src/session/transaction.rs +0 -557
- package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
- package/dist-engine-src/src/sql2/bind/error.rs +0 -5
- package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
- package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
- package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
- package/dist-engine-src/src/sql2/bind/read.rs +0 -65
- package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
- package/dist-engine-src/src/sql2/bind/table.rs +0 -273
- package/dist-engine-src/src/sql2/bind/write.rs +0 -86
- package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
- package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
- package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
- package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
- package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
- package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
- package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
- package/dist-engine-src/src/sql2/context.rs +0 -317
- package/dist-engine-src/src/sql2/dml.rs +0 -148
- package/dist-engine-src/src/sql2/error.rs +0 -215
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
- package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
- package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
- package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
- package/dist-engine-src/src/sql2/exec/write.rs +0 -661
- package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
- package/dist-engine-src/src/sql2/history_projection.rs +0 -56
- package/dist-engine-src/src/sql2/history_route.rs +0 -661
- package/dist-engine-src/src/sql2/mod.rs +0 -52
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
- package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
- package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
- package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
- package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
- package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
- package/dist-engine-src/src/sql2/plan/write.rs +0 -147
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
- package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
- package/dist-engine-src/src/sql2/providers/change.rs +0 -445
- package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
- package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
- package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
- package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
- package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
- package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
- package/dist-engine-src/src/sql2/providers/history.rs +0 -426
- package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
- package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
- package/dist-engine-src/src/sql2/read_only.rs +0 -63
- package/dist-engine-src/src/sql2/record_batch.rs +0 -17
- package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
- package/dist-engine-src/src/sql2/runtime.rs +0 -60
- package/dist-engine-src/src/sql2/session.rs +0 -83
- package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
- package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
- package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
- package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
- package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
- package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
- package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
- package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
- package/dist-engine-src/src/storage/conformance.rs +0 -399
- package/dist-engine-src/src/storage/context.rs +0 -620
- package/dist-engine-src/src/storage/mod.rs +0 -52
- package/dist-engine-src/src/storage/point.rs +0 -440
- package/dist-engine-src/src/storage/read_scope.rs +0 -67
- package/dist-engine-src/src/storage/reader.rs +0 -867
- package/dist-engine-src/src/storage/scan.rs +0 -784
- package/dist-engine-src/src/storage/spaces.rs +0 -236
- package/dist-engine-src/src/storage/stats.rs +0 -80
- package/dist-engine-src/src/storage/write_set.rs +0 -962
- package/dist-engine-src/src/storage_bench.rs +0 -171
- package/dist-engine-src/src/test_support.rs +0 -450
- package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
- package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
- package/dist-engine-src/src/tracked_state/context.rs +0 -2801
- package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
- package/dist-engine-src/src/tracked_state/merge.rs +0 -478
- package/dist-engine-src/src/tracked_state/mod.rs +0 -35
- package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
- package/dist-engine-src/src/tracked_state/storage.rs +0 -427
- package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
- package/dist-engine-src/src/tracked_state/types.rs +0 -238
- package/dist-engine-src/src/transaction/bench_support.rs +0 -407
- package/dist-engine-src/src/transaction/commit.rs +0 -1592
- package/dist-engine-src/src/transaction/context.rs +0 -1653
- package/dist-engine-src/src/transaction/mod.rs +0 -24
- package/dist-engine-src/src/transaction/normalization.rs +0 -877
- package/dist-engine-src/src/transaction/prep.rs +0 -37
- package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
- package/dist-engine-src/src/transaction/staging.rs +0 -1525
- package/dist-engine-src/src/transaction/types.rs +0 -403
- package/dist-engine-src/src/transaction/validation.rs +0 -5766
- package/dist-engine-src/src/untracked_state/codec.rs +0 -615
- package/dist-engine-src/src/untracked_state/context.rs +0 -98
- package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
- package/dist-engine-src/src/untracked_state/mod.rs +0 -15
- package/dist-engine-src/src/untracked_state/storage.rs +0 -898
- package/dist-engine-src/src/untracked_state/types.rs +0 -146
- package/dist-engine-src/src/wasm/mod.rs +0 -60
|
@@ -1,1592 +0,0 @@
|
|
|
1
|
-
use crate::binary_cas::BinaryCasContext;
|
|
2
|
-
use crate::branch::{BranchContext, BranchRefReader};
|
|
3
|
-
use crate::changelog::{
|
|
4
|
-
ChangeRecord, ChangelogAppend, ChangelogContext, ChangelogWriter, CommitChangeRef,
|
|
5
|
-
CommitChangeRefSet, CommitRecord,
|
|
6
|
-
};
|
|
7
|
-
use crate::entity_pk::EntityPk;
|
|
8
|
-
use crate::functions::FunctionContext;
|
|
9
|
-
use crate::json_store::{JsonRef, JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
|
|
10
|
-
use crate::storage::{StorageRead, StorageWriteSet};
|
|
11
|
-
use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
|
|
12
|
-
use crate::transaction::prepare_branch_ref_row;
|
|
13
|
-
use crate::transaction::staging::PreparedWriteSet;
|
|
14
|
-
use crate::transaction::types::{PreparedStateRow, StagedCommitChangeRef, StagedCommitChangeRefs};
|
|
15
|
-
use crate::untracked_state::{
|
|
16
|
-
UntrackedStateContext, UntrackedStateIdentity, UntrackedStateIdentityRef, UntrackedStateRowRef,
|
|
17
|
-
};
|
|
18
|
-
use crate::LixError;
|
|
19
|
-
use std::collections::{BTreeMap, BTreeSet};
|
|
20
|
-
|
|
21
|
-
type RowIndex = usize;
|
|
22
|
-
|
|
23
|
-
/// Commits prepared transaction rows into durable tracked and untracked stores.
|
|
24
|
-
///
|
|
25
|
-
/// Providers decode DataFusion DML into hydrated `PreparedStateRow`s. Untracked
|
|
26
|
-
/// rows are durable local overlay state and bypass changelog change refs. Tracked
|
|
27
|
-
/// rows stage canonical changelog facts, then update the live-state serving
|
|
28
|
-
/// commit root. The tracked side of that commit root is a prolly root keyed by
|
|
29
|
-
/// the new commit id.
|
|
30
|
-
pub(crate) async fn commit_prepared_writes(
|
|
31
|
-
binary_cas: &BinaryCasContext,
|
|
32
|
-
branch_ctx: &BranchContext,
|
|
33
|
-
runtime_functions: Option<&FunctionContext>,
|
|
34
|
-
read: &mut (impl StorageRead + Send + Sync),
|
|
35
|
-
prepared_writes: PreparedWriteSet,
|
|
36
|
-
) -> Result<StorageWriteSet, LixError> {
|
|
37
|
-
let mut writes = StorageWriteSet::new();
|
|
38
|
-
let mut json_writer = JsonStoreContext::new().writer();
|
|
39
|
-
|
|
40
|
-
if !prepared_writes.file_data_writes.is_empty() {
|
|
41
|
-
let mut blob_writer = binary_cas.writer(&mut writes);
|
|
42
|
-
for write in &prepared_writes.file_data_writes {
|
|
43
|
-
blob_writer.stage_bytes(&write.data)?;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let state_rows = prepared_writes.state_rows;
|
|
48
|
-
let finalized = finalize_commit_rows(
|
|
49
|
-
prepared_writes.commit_change_refs_by_branch,
|
|
50
|
-
prepared_writes.extra_commit_parents_by_branch,
|
|
51
|
-
branch_ctx,
|
|
52
|
-
&*read,
|
|
53
|
-
)
|
|
54
|
-
.await?;
|
|
55
|
-
let commit_rows = finalized.commit_rows;
|
|
56
|
-
let branch_heads = finalized.branch_heads;
|
|
57
|
-
let tracked_roots = finalized.tracked_roots;
|
|
58
|
-
let row_index = index_prepared_rows(&state_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
|
-
&& commit_rows.is_empty()
|
|
68
|
-
&& branch_heads.is_empty()
|
|
69
|
-
&& writes.is_empty()
|
|
70
|
-
{
|
|
71
|
-
return Ok(writes);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let staged_commits = stage_changelog_commits(
|
|
75
|
-
read,
|
|
76
|
-
&mut writes,
|
|
77
|
-
&state_rows,
|
|
78
|
-
&row_index.tracked_row_indices_by_commit,
|
|
79
|
-
&commit_rows,
|
|
80
|
-
)
|
|
81
|
-
.await?;
|
|
82
|
-
|
|
83
|
-
stage_state_json_payloads(
|
|
84
|
-
&mut json_writer,
|
|
85
|
-
&mut writes,
|
|
86
|
-
&state_rows,
|
|
87
|
-
&row_index.canonical_row_indices,
|
|
88
|
-
)?;
|
|
89
|
-
|
|
90
|
-
// The serving commit root 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
|
-
{
|
|
94
|
-
let untracked_overlay_delete_identities = existing_untracked_overlay_delete_identities(
|
|
95
|
-
&*read,
|
|
96
|
-
row_index
|
|
97
|
-
.canonical_row_indices
|
|
98
|
-
.iter()
|
|
99
|
-
.map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index])),
|
|
100
|
-
)
|
|
101
|
-
.await?;
|
|
102
|
-
UntrackedStateContext::new()
|
|
103
|
-
.writer(&mut writes)
|
|
104
|
-
.stage_rows(
|
|
105
|
-
row_index
|
|
106
|
-
.untracked_row_indices
|
|
107
|
-
.iter()
|
|
108
|
-
.map(|&row_index| untracked_row_ref_from_state_row(&state_rows[row_index])),
|
|
109
|
-
)?;
|
|
110
|
-
UntrackedStateContext::new()
|
|
111
|
-
.writer(&mut writes)
|
|
112
|
-
.stage_delete_rows(
|
|
113
|
-
untracked_overlay_delete_identities
|
|
114
|
-
.iter()
|
|
115
|
-
.map(UntrackedStateIdentity::as_ref),
|
|
116
|
-
)?;
|
|
117
|
-
stage_tracked_roots(
|
|
118
|
-
read,
|
|
119
|
-
&mut writes,
|
|
120
|
-
&state_rows,
|
|
121
|
-
row_index.tracked_row_indices_by_commit,
|
|
122
|
-
tracked_roots,
|
|
123
|
-
staged_commits,
|
|
124
|
-
)
|
|
125
|
-
.await?;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
for branch_head in branch_heads {
|
|
129
|
-
let canonical_row = prepare_branch_ref_row(
|
|
130
|
-
&branch_head.branch_id,
|
|
131
|
-
&branch_head.commit_id,
|
|
132
|
-
&branch_head.timestamp,
|
|
133
|
-
)?;
|
|
134
|
-
branch_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
Ok(writes)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fn stage_state_json_payloads(
|
|
141
|
-
json_writer: &mut crate::json_store::JsonStoreWriter,
|
|
142
|
-
writes: &mut StorageWriteSet,
|
|
143
|
-
state_rows: &[PreparedStateRow],
|
|
144
|
-
row_indices: &[RowIndex],
|
|
145
|
-
) -> Result<(), LixError> {
|
|
146
|
-
json_writer.stage_batch(
|
|
147
|
-
writes,
|
|
148
|
-
JsonWritePlacementRef::OutOfBand,
|
|
149
|
-
row_indices
|
|
150
|
-
.iter()
|
|
151
|
-
.flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
|
|
152
|
-
)?;
|
|
153
|
-
Ok(())
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
fn json_payloads_from_state_row(
|
|
157
|
-
row: &PreparedStateRow,
|
|
158
|
-
) -> impl Iterator<Item = NormalizedJsonRef<'_>> {
|
|
159
|
-
row.snapshot
|
|
160
|
-
.iter()
|
|
161
|
-
.chain(row.metadata.iter())
|
|
162
|
-
.map(|json| NormalizedJsonRef::trusted_prehashed(json.normalized.as_ref(), json.json_ref))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async fn existing_untracked_overlay_delete_identities<'a>(
|
|
166
|
-
read: &(impl StorageRead + Send + Sync + ?Sized),
|
|
167
|
-
identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
|
|
168
|
-
) -> Result<Vec<UntrackedStateIdentity>, LixError> {
|
|
169
|
-
UntrackedStateContext::new()
|
|
170
|
-
.reader(read)
|
|
171
|
-
.existing_identities(identities)
|
|
172
|
-
.await
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
struct PreparedRowIndex {
|
|
176
|
-
canonical_row_indices: Vec<RowIndex>,
|
|
177
|
-
untracked_row_indices: Vec<RowIndex>,
|
|
178
|
-
tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, LixError> {
|
|
182
|
-
let mut canonical_row_indices = Vec::new();
|
|
183
|
-
let mut untracked_row_indices = Vec::new();
|
|
184
|
-
let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<RowIndex>>::new();
|
|
185
|
-
|
|
186
|
-
for (row_index, row) in rows.iter().enumerate() {
|
|
187
|
-
if row.untracked {
|
|
188
|
-
untracked_row_indices.push(row_index);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
let Some(commit_id) = row.commit_id.as_ref() else {
|
|
192
|
-
return Err(LixError::new(
|
|
193
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
194
|
-
"tracked prepared row is missing commit_id before commit indexing",
|
|
195
|
-
));
|
|
196
|
-
};
|
|
197
|
-
canonical_row_indices.push(row_index);
|
|
198
|
-
tracked_row_indices_by_commit
|
|
199
|
-
.entry(commit_id.clone())
|
|
200
|
-
.or_default()
|
|
201
|
-
.push(row_index);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
Ok(PreparedRowIndex {
|
|
205
|
-
canonical_row_indices,
|
|
206
|
-
untracked_row_indices,
|
|
207
|
-
tracked_row_indices_by_commit,
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
#[derive(Clone, Debug)]
|
|
212
|
-
struct StagedChangelogCommit {
|
|
213
|
-
change_ids: Vec<String>,
|
|
214
|
-
selected_change_refs: Vec<StagedCommitChangeRef>,
|
|
215
|
-
commit_change_id: String,
|
|
216
|
-
commit_created_at: String,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async fn stage_changelog_commits(
|
|
220
|
-
read: &mut (impl StorageRead + Send + Sync),
|
|
221
|
-
writes: &mut StorageWriteSet,
|
|
222
|
-
state_rows: &[PreparedStateRow],
|
|
223
|
-
tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
|
|
224
|
-
commit_rows: &[FinalizedCommitRow],
|
|
225
|
-
) -> Result<BTreeMap<String, StagedChangelogCommit>, LixError> {
|
|
226
|
-
if commit_rows.is_empty() {
|
|
227
|
-
return Ok(BTreeMap::new());
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let mut commits = Vec::with_capacity(commit_rows.len());
|
|
231
|
-
let mut changes = Vec::new();
|
|
232
|
-
let mut commit_change_refs = Vec::with_capacity(commit_rows.len());
|
|
233
|
-
let mut staged = BTreeMap::<String, StagedChangelogCommit>::new();
|
|
234
|
-
for commit_row in commit_rows {
|
|
235
|
-
let state_row_indices = tracked_row_indices_by_commit
|
|
236
|
-
.get(&commit_row.commit_id)
|
|
237
|
-
.map(Vec::as_slice)
|
|
238
|
-
.unwrap_or_default();
|
|
239
|
-
let mut refs = Vec::with_capacity(state_row_indices.len());
|
|
240
|
-
let mut change_ids =
|
|
241
|
-
Vec::with_capacity(state_row_indices.len() + commit_row.selected_change_refs.len());
|
|
242
|
-
for &row_index in state_row_indices {
|
|
243
|
-
let row = &state_rows[row_index];
|
|
244
|
-
let change_id = row.change_id.as_ref().ok_or_else(|| {
|
|
245
|
-
LixError::new(
|
|
246
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
247
|
-
"tracked staged row is missing change_id before changelog append",
|
|
248
|
-
)
|
|
249
|
-
})?;
|
|
250
|
-
refs.push(commit_change_ref_from_state_row(row, change_id));
|
|
251
|
-
change_ids.push(change_id.clone());
|
|
252
|
-
changes.push(change_record_from_state_row(row)?);
|
|
253
|
-
}
|
|
254
|
-
for change_ref in &commit_row.selected_change_refs {
|
|
255
|
-
refs.push(commit_change_ref_from_selected_change_ref(change_ref));
|
|
256
|
-
change_ids.push(change_ref.change_id.clone());
|
|
257
|
-
}
|
|
258
|
-
commits.push(CommitRecord {
|
|
259
|
-
format_version: 1,
|
|
260
|
-
commit_id: commit_row.commit_id.clone(),
|
|
261
|
-
parent_commit_ids: commit_row.parent_commit_ids.clone(),
|
|
262
|
-
change_id: commit_row.change_id.clone(),
|
|
263
|
-
author_account_ids: Vec::new(),
|
|
264
|
-
created_at: commit_row.created_at.clone(),
|
|
265
|
-
});
|
|
266
|
-
commit_change_refs.push(CommitChangeRefSet {
|
|
267
|
-
commit_id: commit_row.commit_id.clone(),
|
|
268
|
-
entries: refs,
|
|
269
|
-
});
|
|
270
|
-
staged.insert(
|
|
271
|
-
commit_row.commit_id.clone(),
|
|
272
|
-
StagedChangelogCommit {
|
|
273
|
-
change_ids,
|
|
274
|
-
selected_change_refs: commit_row.selected_change_refs.clone(),
|
|
275
|
-
commit_change_id: commit_row.change_id.clone(),
|
|
276
|
-
commit_created_at: commit_row.created_at.clone(),
|
|
277
|
-
},
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let append = ChangelogAppend {
|
|
282
|
-
commits,
|
|
283
|
-
changes,
|
|
284
|
-
commit_change_refs,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
let mut writer = ChangelogContext::new().writer(read, writes);
|
|
288
|
-
writer.stage_append(append).await?;
|
|
289
|
-
stage_commit_row_json_payloads(writes, commit_rows)?;
|
|
290
|
-
Ok(staged)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
fn change_record_from_state_row(row: &PreparedStateRow) -> Result<ChangeRecord, LixError> {
|
|
294
|
-
let Some(change_id) = row.change_id.as_ref() else {
|
|
295
|
-
return Err(LixError::new(
|
|
296
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
297
|
-
"tracked staged row is missing change_id before changelog change construction",
|
|
298
|
-
));
|
|
299
|
-
};
|
|
300
|
-
Ok(ChangeRecord {
|
|
301
|
-
format_version: 1,
|
|
302
|
-
change_id: change_id.clone(),
|
|
303
|
-
entity_pk: row.entity_pk.clone(),
|
|
304
|
-
schema_key: row.schema_key.clone(),
|
|
305
|
-
file_id: row.file_id.clone(),
|
|
306
|
-
snapshot_ref: row.snapshot.as_ref().map(|snapshot| snapshot.json_ref),
|
|
307
|
-
metadata_ref: row.metadata.as_ref().map(|metadata| metadata.json_ref),
|
|
308
|
-
created_at: row.updated_at.clone(),
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
fn stage_commit_row_json_payloads(
|
|
313
|
-
writes: &mut StorageWriteSet,
|
|
314
|
-
commit_rows: &[FinalizedCommitRow],
|
|
315
|
-
) -> Result<(), LixError> {
|
|
316
|
-
let snapshots = commit_rows
|
|
317
|
-
.iter()
|
|
318
|
-
.map(|row| commit_row_snapshot_content(&row.commit_id))
|
|
319
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
320
|
-
JsonStoreContext::new().writer().stage_batch(
|
|
321
|
-
writes,
|
|
322
|
-
JsonWritePlacementRef::OutOfBand,
|
|
323
|
-
snapshots
|
|
324
|
-
.iter()
|
|
325
|
-
.map(|snapshot| NormalizedJsonRef::new(snapshot.as_str())),
|
|
326
|
-
)?;
|
|
327
|
-
Ok(())
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
|
|
331
|
-
serde_json::to_string(&serde_json::json!({
|
|
332
|
-
"id": commit_id,
|
|
333
|
-
}))
|
|
334
|
-
.map_err(|error| {
|
|
335
|
-
LixError::new(
|
|
336
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
337
|
-
format!("failed to encode lix_commit snapshot: {error}"),
|
|
338
|
-
)
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
fn commit_change_ref_from_state_row(row: &PreparedStateRow, change_id: &str) -> CommitChangeRef {
|
|
343
|
-
CommitChangeRef {
|
|
344
|
-
schema_key: row.schema_key.clone(),
|
|
345
|
-
file_id: row.file_id.clone(),
|
|
346
|
-
entity_pk: row.entity_pk.clone(),
|
|
347
|
-
change_id: change_id.to_string(),
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
fn commit_change_ref_from_selected_change_ref(
|
|
352
|
-
change_ref: &StagedCommitChangeRef,
|
|
353
|
-
) -> CommitChangeRef {
|
|
354
|
-
CommitChangeRef {
|
|
355
|
-
schema_key: change_ref.schema_key.clone(),
|
|
356
|
-
file_id: change_ref.file_id.clone(),
|
|
357
|
-
entity_pk: change_ref.entity_pk.clone(),
|
|
358
|
-
change_id: change_ref.change_id.clone(),
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
fn tracked_delta_from_state_row(
|
|
363
|
-
row: &PreparedStateRow,
|
|
364
|
-
) -> Result<TrackedStateDeltaRef<'_>, LixError> {
|
|
365
|
-
let Some(change_id) = row.change_id.as_deref() else {
|
|
366
|
-
return Err(LixError::new(
|
|
367
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
368
|
-
"tracked staged row is missing change_id before tracked root staging",
|
|
369
|
-
));
|
|
370
|
-
};
|
|
371
|
-
let Some(commit_id) = row.commit_id.as_deref() else {
|
|
372
|
-
return Err(LixError::new(
|
|
373
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
374
|
-
"tracked staged row is missing commit_id before tracked root staging",
|
|
375
|
-
));
|
|
376
|
-
};
|
|
377
|
-
Ok(TrackedStateDeltaRef {
|
|
378
|
-
schema_key: &row.schema_key,
|
|
379
|
-
file_id: row.file_id.as_deref(),
|
|
380
|
-
entity_pk: &row.entity_pk,
|
|
381
|
-
change_id,
|
|
382
|
-
commit_id,
|
|
383
|
-
snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
|
|
384
|
-
metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
|
|
385
|
-
deleted: row.snapshot.is_none(),
|
|
386
|
-
created_at: &row.created_at,
|
|
387
|
-
updated_at: &row.updated_at,
|
|
388
|
-
})
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
fn tracked_delta_from_selected_change_ref<'a>(
|
|
392
|
-
change_ref: &'a StagedCommitChangeRef,
|
|
393
|
-
commit_id: &'a str,
|
|
394
|
-
) -> Result<TrackedStateDeltaRef<'a>, LixError> {
|
|
395
|
-
Ok(TrackedStateDeltaRef {
|
|
396
|
-
schema_key: &change_ref.schema_key,
|
|
397
|
-
file_id: change_ref.file_id.as_deref(),
|
|
398
|
-
entity_pk: &change_ref.entity_pk,
|
|
399
|
-
change_id: &change_ref.change_id,
|
|
400
|
-
commit_id,
|
|
401
|
-
snapshot_ref: change_ref.snapshot_ref.as_ref(),
|
|
402
|
-
metadata_ref: change_ref.metadata_ref.as_ref(),
|
|
403
|
-
deleted: change_ref.deleted,
|
|
404
|
-
created_at: &change_ref.created_at,
|
|
405
|
-
updated_at: &change_ref.updated_at,
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async fn stage_tracked_roots(
|
|
410
|
-
read: &(impl StorageRead + Send + Sync + ?Sized),
|
|
411
|
-
writes: &mut StorageWriteSet,
|
|
412
|
-
state_rows: &[PreparedStateRow],
|
|
413
|
-
tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
|
|
414
|
-
tracked_roots: Vec<PendingTrackedRoot>,
|
|
415
|
-
staged_commits: BTreeMap<String, StagedChangelogCommit>,
|
|
416
|
-
) -> Result<(), LixError> {
|
|
417
|
-
let tracked_state = TrackedStateContext::new();
|
|
418
|
-
let mut tracked_writer = tracked_state.writer(read, writes);
|
|
419
|
-
for root in tracked_roots_parent_first(&tracked_roots)? {
|
|
420
|
-
let staged = staged_commits.get(&root.commit_id).ok_or_else(|| {
|
|
421
|
-
LixError::new(
|
|
422
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
423
|
-
format!(
|
|
424
|
-
"tracked-state root for commit '{}' has no staged changelog facts",
|
|
425
|
-
root.commit_id
|
|
426
|
-
),
|
|
427
|
-
)
|
|
428
|
-
})?;
|
|
429
|
-
let state_row_indices = tracked_row_indices_by_commit
|
|
430
|
-
.get(&root.commit_id)
|
|
431
|
-
.map(Vec::as_slice)
|
|
432
|
-
.unwrap_or_default();
|
|
433
|
-
if state_row_indices.len() > staged.change_ids.len() {
|
|
434
|
-
return Err(LixError::new(
|
|
435
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
436
|
-
format!(
|
|
437
|
-
"commit '{}' has {} tracked rows but only {} changelog changes",
|
|
438
|
-
root.commit_id,
|
|
439
|
-
state_row_indices.len(),
|
|
440
|
-
staged.change_ids.len()
|
|
441
|
-
),
|
|
442
|
-
));
|
|
443
|
-
}
|
|
444
|
-
let commit_snapshot = commit_row_snapshot_content(&root.commit_id)?;
|
|
445
|
-
let commit_snapshot_ref = JsonRef::for_content(commit_snapshot.as_bytes());
|
|
446
|
-
let commit_entity_pk = EntityPk::single(root.commit_id.clone());
|
|
447
|
-
let mut deltas = state_row_indices
|
|
448
|
-
.iter()
|
|
449
|
-
.map(|&row_index| tracked_delta_from_state_row(&state_rows[row_index]))
|
|
450
|
-
.chain(staged.selected_change_refs.iter().map(|change_ref| {
|
|
451
|
-
tracked_delta_from_selected_change_ref(change_ref, &root.commit_id)
|
|
452
|
-
}))
|
|
453
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
454
|
-
deltas.push(TrackedStateDeltaRef {
|
|
455
|
-
schema_key: "lix_commit",
|
|
456
|
-
file_id: None,
|
|
457
|
-
entity_pk: &commit_entity_pk,
|
|
458
|
-
change_id: &staged.commit_change_id,
|
|
459
|
-
commit_id: &root.commit_id,
|
|
460
|
-
snapshot_ref: Some(&commit_snapshot_ref),
|
|
461
|
-
metadata_ref: None,
|
|
462
|
-
deleted: false,
|
|
463
|
-
created_at: &staged.commit_created_at,
|
|
464
|
-
updated_at: &staged.commit_created_at,
|
|
465
|
-
});
|
|
466
|
-
tracked_writer
|
|
467
|
-
.stage_commit_root(&root.commit_id, root.parent_commit_id.as_deref(), deltas)
|
|
468
|
-
.await?;
|
|
469
|
-
}
|
|
470
|
-
let rooted_commit_ids = tracked_roots
|
|
471
|
-
.iter()
|
|
472
|
-
.map(|root| root.commit_id.as_str())
|
|
473
|
-
.collect::<BTreeSet<_>>();
|
|
474
|
-
let extra_tracked = tracked_row_indices_by_commit
|
|
475
|
-
.keys()
|
|
476
|
-
.filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
|
|
477
|
-
.cloned()
|
|
478
|
-
.collect::<BTreeSet<_>>();
|
|
479
|
-
if !extra_tracked.is_empty() {
|
|
480
|
-
let mut commit_ids = tracked_row_indices_by_commit
|
|
481
|
-
.keys()
|
|
482
|
-
.cloned()
|
|
483
|
-
.collect::<Vec<_>>();
|
|
484
|
-
commit_ids.sort();
|
|
485
|
-
commit_ids.dedup();
|
|
486
|
-
return Err(LixError::new(
|
|
487
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
488
|
-
format!(
|
|
489
|
-
"tracked live_state rows have no finalized root metadata for commit ids: {}",
|
|
490
|
-
commit_ids.join(", ")
|
|
491
|
-
),
|
|
492
|
-
));
|
|
493
|
-
}
|
|
494
|
-
if !staged_commits.is_empty() {
|
|
495
|
-
let commit_ids = staged_commits
|
|
496
|
-
.keys()
|
|
497
|
-
.filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
|
|
498
|
-
.cloned()
|
|
499
|
-
.collect::<Vec<_>>();
|
|
500
|
-
if commit_ids.is_empty() {
|
|
501
|
-
return Ok(());
|
|
502
|
-
}
|
|
503
|
-
return Err(LixError::new(
|
|
504
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
505
|
-
format!(
|
|
506
|
-
"changelog staged commits without tracked root metadata: {}",
|
|
507
|
-
commit_ids.join(", ")
|
|
508
|
-
),
|
|
509
|
-
));
|
|
510
|
-
}
|
|
511
|
-
Ok(())
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
fn tracked_roots_parent_first(
|
|
515
|
-
tracked_roots: &[PendingTrackedRoot],
|
|
516
|
-
) -> Result<Vec<&PendingTrackedRoot>, LixError> {
|
|
517
|
-
let mut roots_by_id = BTreeMap::new();
|
|
518
|
-
for root in tracked_roots {
|
|
519
|
-
if roots_by_id.insert(root.commit_id.as_str(), root).is_some() {
|
|
520
|
-
return Err(LixError::unknown(format!(
|
|
521
|
-
"cannot stage duplicate tracked_state root '{}'",
|
|
522
|
-
root.commit_id
|
|
523
|
-
)));
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
let mut ordered = Vec::with_capacity(tracked_roots.len());
|
|
528
|
-
let mut visiting = BTreeSet::new();
|
|
529
|
-
let mut visited = BTreeSet::new();
|
|
530
|
-
for root in tracked_roots {
|
|
531
|
-
visit_tracked_root_parent_first(
|
|
532
|
-
root.commit_id.as_str(),
|
|
533
|
-
&roots_by_id,
|
|
534
|
-
&mut visiting,
|
|
535
|
-
&mut visited,
|
|
536
|
-
&mut ordered,
|
|
537
|
-
)?;
|
|
538
|
-
}
|
|
539
|
-
Ok(ordered)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
fn visit_tracked_root_parent_first<'a>(
|
|
543
|
-
commit_id: &str,
|
|
544
|
-
roots_by_id: &BTreeMap<&'a str, &'a PendingTrackedRoot>,
|
|
545
|
-
visiting: &mut BTreeSet<&'a str>,
|
|
546
|
-
visited: &mut BTreeSet<&'a str>,
|
|
547
|
-
ordered: &mut Vec<&'a PendingTrackedRoot>,
|
|
548
|
-
) -> Result<(), LixError> {
|
|
549
|
-
if visited.contains(commit_id) {
|
|
550
|
-
return Ok(());
|
|
551
|
-
}
|
|
552
|
-
let Some(root) = roots_by_id.get(commit_id).copied() else {
|
|
553
|
-
return Ok(());
|
|
554
|
-
};
|
|
555
|
-
if !visiting.insert(root.commit_id.as_str()) {
|
|
556
|
-
return Err(LixError::unknown(format!(
|
|
557
|
-
"cannot stage tracked_state root '{}' because staged root parents contain a cycle",
|
|
558
|
-
root.commit_id
|
|
559
|
-
)));
|
|
560
|
-
}
|
|
561
|
-
if let Some(parent_id) = root.parent_commit_id.as_deref() {
|
|
562
|
-
if roots_by_id.contains_key(parent_id) {
|
|
563
|
-
visit_tracked_root_parent_first(parent_id, roots_by_id, visiting, visited, ordered)?;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
visiting.remove(root.commit_id.as_str());
|
|
567
|
-
visited.insert(root.commit_id.as_str());
|
|
568
|
-
ordered.push(root);
|
|
569
|
-
Ok(())
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRowRef<'_> {
|
|
573
|
-
UntrackedStateRowRef {
|
|
574
|
-
entity_pk: &row.entity_pk,
|
|
575
|
-
schema_key: &row.schema_key,
|
|
576
|
-
file_id: row.file_id.as_deref(),
|
|
577
|
-
snapshot_content: row
|
|
578
|
-
.snapshot
|
|
579
|
-
.as_ref()
|
|
580
|
-
.map(|snapshot| snapshot.normalized.as_ref()),
|
|
581
|
-
metadata: row
|
|
582
|
-
.metadata
|
|
583
|
-
.as_ref()
|
|
584
|
-
.map(|metadata| metadata.normalized.as_ref()),
|
|
585
|
-
created_at: &row.created_at,
|
|
586
|
-
updated_at: &row.updated_at,
|
|
587
|
-
global: row.global,
|
|
588
|
-
branch_id: &row.branch_id,
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
fn untracked_identity_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateIdentityRef<'_> {
|
|
593
|
-
UntrackedStateIdentityRef {
|
|
594
|
-
branch_id: &row.branch_id,
|
|
595
|
-
schema_key: &row.schema_key,
|
|
596
|
-
entity_pk: &row.entity_pk,
|
|
597
|
-
file_id: row.file_id.as_deref(),
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/// Materializes tracked staged change refs into changelog commits.
|
|
602
|
-
///
|
|
603
|
-
/// Staging only accumulates `branch_id -> change_ids` because commit ids,
|
|
604
|
-
/// parent heads, and commit-row timestamps belong to transaction finalization.
|
|
605
|
-
/// The `change_ids` list is the ordered set of canonical changes whose effects
|
|
606
|
-
/// the commit introduces relative to its first parent.
|
|
607
|
-
/// This function turns those change-ref sets into finalized commit facts.
|
|
608
|
-
///
|
|
609
|
-
/// Commit finalization output split by durability target.
|
|
610
|
-
///
|
|
611
|
-
/// `commit_rows` are canonical changelog commit facts. tracked_state roots store
|
|
612
|
-
/// serving commit roots keyed by the corresponding commit id.
|
|
613
|
-
///
|
|
614
|
-
/// `branch_heads` are moving refs. They are written through `BranchContext`,
|
|
615
|
-
/// not the canonical changelog.
|
|
616
|
-
struct FinalizedCommitRows {
|
|
617
|
-
commit_rows: Vec<FinalizedCommitRow>,
|
|
618
|
-
branch_heads: Vec<PendingBranchHead>,
|
|
619
|
-
tracked_roots: Vec<PendingTrackedRoot>,
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
struct FinalizedCommitRow {
|
|
623
|
-
commit_id: String,
|
|
624
|
-
parent_commit_ids: Vec<String>,
|
|
625
|
-
created_at: String,
|
|
626
|
-
change_id: String,
|
|
627
|
-
selected_change_refs: Vec<StagedCommitChangeRef>,
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
struct PendingBranchHead {
|
|
631
|
-
branch_id: String,
|
|
632
|
-
commit_id: String,
|
|
633
|
-
timestamp: String,
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
struct PendingTrackedRoot {
|
|
637
|
-
commit_id: String,
|
|
638
|
-
parent_commit_id: Option<String>,
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
async fn finalize_commit_rows(
|
|
642
|
-
commit_change_refs_by_branch: BTreeMap<String, StagedCommitChangeRefs>,
|
|
643
|
-
extra_commit_parents_by_branch: BTreeMap<String, Vec<String>>,
|
|
644
|
-
branch_ctx: &BranchContext,
|
|
645
|
-
read: &(impl StorageRead + Send + Sync + ?Sized),
|
|
646
|
-
) -> Result<FinalizedCommitRows, LixError> {
|
|
647
|
-
let mut commit_rows = Vec::new();
|
|
648
|
-
let mut branch_heads = Vec::new();
|
|
649
|
-
let mut tracked_roots = Vec::new();
|
|
650
|
-
|
|
651
|
-
for (branch_id, change_refs) in commit_change_refs_by_branch {
|
|
652
|
-
if change_refs.is_empty() && !change_refs.allow_empty {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
let commit_id = change_refs.commit_id;
|
|
657
|
-
let commit_change_id = change_refs.commit_change_id;
|
|
658
|
-
let timestamp = change_refs.created_at;
|
|
659
|
-
let selected_change_refs = change_refs.selected_change_refs;
|
|
660
|
-
let parent_commit_ids = branch_ctx
|
|
661
|
-
.ref_reader(read)
|
|
662
|
-
.load_head_commit_id(&branch_id)
|
|
663
|
-
.await?
|
|
664
|
-
.into_iter()
|
|
665
|
-
.collect::<Vec<_>>();
|
|
666
|
-
let parent_commit_ids = merge_parent_commit_ids(
|
|
667
|
-
parent_commit_ids,
|
|
668
|
-
extra_commit_parents_by_branch
|
|
669
|
-
.get(&branch_id)
|
|
670
|
-
.cloned()
|
|
671
|
-
.unwrap_or_default(),
|
|
672
|
-
);
|
|
673
|
-
let parent_commit_id = parent_commit_ids.first().cloned();
|
|
674
|
-
|
|
675
|
-
commit_rows.push(FinalizedCommitRow {
|
|
676
|
-
commit_id: commit_id.clone(),
|
|
677
|
-
parent_commit_ids: parent_commit_ids.clone(),
|
|
678
|
-
created_at: timestamp.clone(),
|
|
679
|
-
change_id: commit_change_id,
|
|
680
|
-
selected_change_refs,
|
|
681
|
-
});
|
|
682
|
-
branch_heads.push(PendingBranchHead {
|
|
683
|
-
branch_id: branch_id.clone(),
|
|
684
|
-
commit_id: commit_id.clone(),
|
|
685
|
-
timestamp,
|
|
686
|
-
});
|
|
687
|
-
tracked_roots.push(PendingTrackedRoot {
|
|
688
|
-
commit_id,
|
|
689
|
-
parent_commit_id,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
Ok(FinalizedCommitRows {
|
|
694
|
-
commit_rows,
|
|
695
|
-
branch_heads,
|
|
696
|
-
tracked_roots,
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
fn merge_parent_commit_ids(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
|
|
701
|
-
for parent in extra {
|
|
702
|
-
if !base.contains(&parent) {
|
|
703
|
-
base.push(parent);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
base
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
#[cfg(test)]
|
|
710
|
-
mod tests {
|
|
711
|
-
use std::collections::BTreeMap;
|
|
712
|
-
use std::sync::{
|
|
713
|
-
atomic::{AtomicUsize, Ordering},
|
|
714
|
-
Arc,
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
use super::*;
|
|
718
|
-
use crate::backend::{
|
|
719
|
-
Backend, BackendCapabilities, BackendError, BackendWrite, CommitResult, DurableWriteLock,
|
|
720
|
-
KeyRange, PutBatch,
|
|
721
|
-
};
|
|
722
|
-
use crate::branch::BranchContext;
|
|
723
|
-
use crate::catalog::SchemaPlanId;
|
|
724
|
-
use crate::changelog::ChangelogReader;
|
|
725
|
-
use crate::live_state::{LiveStateContext, LiveStateRowRequest};
|
|
726
|
-
use crate::storage::{
|
|
727
|
-
InMemoryStorageBackend, InMemoryStorageRead, InMemoryStorageWrite, StorageContext,
|
|
728
|
-
StorageKey, StorageReadOptions, StorageWriteOptions,
|
|
729
|
-
};
|
|
730
|
-
use crate::transaction::types::PreparedRowFacts;
|
|
731
|
-
use crate::untracked_state::{
|
|
732
|
-
MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateRowRequest,
|
|
733
|
-
};
|
|
734
|
-
use crate::NullableKeyFilter;
|
|
735
|
-
use crate::GLOBAL_BRANCH_ID;
|
|
736
|
-
|
|
737
|
-
const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
|
|
738
|
-
const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
|
|
739
|
-
|
|
740
|
-
fn live_state_context() -> LiveStateContext {
|
|
741
|
-
LiveStateContext::new(
|
|
742
|
-
crate::tracked_state::TrackedStateContext::new(),
|
|
743
|
-
crate::untracked_state::UntrackedStateContext::new(),
|
|
744
|
-
crate::commit_graph::CommitGraphContext::new(),
|
|
745
|
-
)
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
#[tokio::test]
|
|
749
|
-
async fn commit_staged_writes_appends_changelog_and_updates_commit_root() {
|
|
750
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
751
|
-
let binary_cas = BinaryCasContext::new();
|
|
752
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
753
|
-
let mut read = storage
|
|
754
|
-
.begin_read(StorageReadOptions::default())
|
|
755
|
-
.expect("read should open");
|
|
756
|
-
|
|
757
|
-
let state_rows = vec![tracked_global_row("change-1")];
|
|
758
|
-
let writes = commit_prepared_writes(
|
|
759
|
-
&binary_cas,
|
|
760
|
-
&branch_ctx,
|
|
761
|
-
None,
|
|
762
|
-
&mut read,
|
|
763
|
-
PreparedWriteSet {
|
|
764
|
-
insert_identities: BTreeMap::new(),
|
|
765
|
-
state_rows,
|
|
766
|
-
commit_change_refs_by_branch: BTreeMap::from([(
|
|
767
|
-
GLOBAL_BRANCH_ID.to_string(),
|
|
768
|
-
change_refs(["change-1"]),
|
|
769
|
-
)]),
|
|
770
|
-
extra_commit_parents_by_branch: BTreeMap::new(),
|
|
771
|
-
file_data_writes: Vec::new(),
|
|
772
|
-
},
|
|
773
|
-
)
|
|
774
|
-
.await
|
|
775
|
-
.expect("commit should flush staged rows");
|
|
776
|
-
storage
|
|
777
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
778
|
-
.expect("writes should commit");
|
|
779
|
-
|
|
780
|
-
let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
|
|
781
|
-
storage
|
|
782
|
-
.begin_read(StorageReadOptions::default())
|
|
783
|
-
.expect("read should open"),
|
|
784
|
-
);
|
|
785
|
-
let commits = changelog_reader
|
|
786
|
-
.load_commits(crate::changelog::CommitLoadRequest {
|
|
787
|
-
commit_ids: &["test-uuid-1".to_string()],
|
|
788
|
-
projection: crate::changelog::CommitProjection::Full,
|
|
789
|
-
})
|
|
790
|
-
.await
|
|
791
|
-
.expect("changelog commit should load");
|
|
792
|
-
let Some(crate::changelog::CommitLoadEntry::Full {
|
|
793
|
-
record,
|
|
794
|
-
change_ref_chunks,
|
|
795
|
-
}) = commits.entries.into_iter().next().flatten()
|
|
796
|
-
else {
|
|
797
|
-
panic!("changelog commit should exist");
|
|
798
|
-
};
|
|
799
|
-
assert_eq!(record.change_id, "test-uuid-2");
|
|
800
|
-
assert!(change_ref_chunks
|
|
801
|
-
.iter()
|
|
802
|
-
.flat_map(|chunk| chunk.entries.iter())
|
|
803
|
-
.any(|entry| entry.change_id == "change-1"));
|
|
804
|
-
let changes = changelog_reader
|
|
805
|
-
.load_changes(crate::changelog::ChangeLoadRequest {
|
|
806
|
-
change_ids: &["change-1".to_string(), record.change_id.clone()],
|
|
807
|
-
})
|
|
808
|
-
.await
|
|
809
|
-
.expect("changelog change should load");
|
|
810
|
-
let mut loaded_changes = changes.entries.into_iter();
|
|
811
|
-
let Some(change) = loaded_changes.next().flatten() else {
|
|
812
|
-
panic!("changelog change should exist");
|
|
813
|
-
};
|
|
814
|
-
assert_eq!(change.change_id, "change-1");
|
|
815
|
-
assert_eq!(change.schema_key, "test_schema");
|
|
816
|
-
assert!(
|
|
817
|
-
loaded_changes.next().flatten().is_none(),
|
|
818
|
-
"commit row change is derived from changelog.commit, not stored as changelog.change"
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
let mut tracked_reader = crate::tracked_state::TrackedStateContext::new().reader(
|
|
822
|
-
storage
|
|
823
|
-
.begin_read(StorageReadOptions::default())
|
|
824
|
-
.expect("read should open"),
|
|
825
|
-
);
|
|
826
|
-
let commit_rows = tracked_reader
|
|
827
|
-
.scan_rows_at_commit(
|
|
828
|
-
"test-uuid-1",
|
|
829
|
-
&crate::tracked_state::TrackedStateScanRequest {
|
|
830
|
-
filter: crate::tracked_state::TrackedStateFilter {
|
|
831
|
-
schema_keys: vec!["lix_commit".to_string()],
|
|
832
|
-
..Default::default()
|
|
833
|
-
},
|
|
834
|
-
..Default::default()
|
|
835
|
-
},
|
|
836
|
-
)
|
|
837
|
-
.await
|
|
838
|
-
.expect("commit root should scan");
|
|
839
|
-
assert!(
|
|
840
|
-
commit_rows
|
|
841
|
-
.iter()
|
|
842
|
-
.any(|row| row.change_id == record.change_id && row.snapshot_content.is_some()),
|
|
843
|
-
"commit root should surface the derived lix_commit row"
|
|
844
|
-
);
|
|
845
|
-
|
|
846
|
-
let loaded_head = branch_ctx
|
|
847
|
-
.ref_reader(
|
|
848
|
-
storage
|
|
849
|
-
.begin_read(StorageReadOptions::default())
|
|
850
|
-
.expect("read should open"),
|
|
851
|
-
)
|
|
852
|
-
.load_head_commit_id(GLOBAL_BRANCH_ID)
|
|
853
|
-
.await
|
|
854
|
-
.expect("branch ref load should succeed");
|
|
855
|
-
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
#[tokio::test]
|
|
859
|
-
async fn stage_changelog_commits_orders_staged_parents_before_children() {
|
|
860
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
861
|
-
let mut writes = StorageWriteSet::new();
|
|
862
|
-
let mut read = storage
|
|
863
|
-
.begin_read(StorageReadOptions::default())
|
|
864
|
-
.expect("read should open");
|
|
865
|
-
let mut parent_row = tracked_global_row("parent-change");
|
|
866
|
-
parent_row.commit_id = Some("parent-commit".to_string());
|
|
867
|
-
let mut child_row = tracked_global_row("child-change");
|
|
868
|
-
child_row.commit_id = Some("child-commit".to_string());
|
|
869
|
-
|
|
870
|
-
let commits = vec![
|
|
871
|
-
FinalizedCommitRow {
|
|
872
|
-
commit_id: "child-commit".to_string(),
|
|
873
|
-
parent_commit_ids: vec!["parent-commit".to_string()],
|
|
874
|
-
created_at: "2026-01-01T00:00:01Z".to_string(),
|
|
875
|
-
change_id: "child-commit-change".to_string(),
|
|
876
|
-
selected_change_refs: Vec::new(),
|
|
877
|
-
},
|
|
878
|
-
FinalizedCommitRow {
|
|
879
|
-
commit_id: "parent-commit".to_string(),
|
|
880
|
-
parent_commit_ids: Vec::new(),
|
|
881
|
-
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
882
|
-
change_id: "parent-commit-change".to_string(),
|
|
883
|
-
selected_change_refs: Vec::new(),
|
|
884
|
-
},
|
|
885
|
-
];
|
|
886
|
-
stage_changelog_commits(
|
|
887
|
-
&mut read,
|
|
888
|
-
&mut writes,
|
|
889
|
-
&[parent_row, child_row],
|
|
890
|
-
&BTreeMap::from([
|
|
891
|
-
("parent-commit".to_string(), vec![0]),
|
|
892
|
-
("child-commit".to_string(), vec![1]),
|
|
893
|
-
]),
|
|
894
|
-
&commits,
|
|
895
|
-
)
|
|
896
|
-
.await
|
|
897
|
-
.expect("child-before-parent input should still stage parent first");
|
|
898
|
-
storage
|
|
899
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
900
|
-
.expect("writes should persist");
|
|
901
|
-
|
|
902
|
-
let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
|
|
903
|
-
storage
|
|
904
|
-
.begin_read(StorageReadOptions::default())
|
|
905
|
-
.expect("read should open"),
|
|
906
|
-
);
|
|
907
|
-
let commits = changelog_reader
|
|
908
|
-
.load_commits(crate::changelog::CommitLoadRequest {
|
|
909
|
-
commit_ids: &["parent-commit".to_string(), "child-commit".to_string()],
|
|
910
|
-
projection: crate::changelog::CommitProjection::Record,
|
|
911
|
-
})
|
|
912
|
-
.await
|
|
913
|
-
.expect("commits should load");
|
|
914
|
-
assert!(commits.entries.iter().all(Option::is_some));
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
#[tokio::test]
|
|
918
|
-
async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
|
|
919
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
920
|
-
let binary_cas = BinaryCasContext::new();
|
|
921
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
922
|
-
let untracked_state = UntrackedStateContext::new();
|
|
923
|
-
let mut read = storage
|
|
924
|
-
.begin_read(StorageReadOptions::default())
|
|
925
|
-
.expect("read should open");
|
|
926
|
-
|
|
927
|
-
let state_rows = vec![untracked_global_row("change-untracked")];
|
|
928
|
-
let writes = commit_prepared_writes(
|
|
929
|
-
&binary_cas,
|
|
930
|
-
&branch_ctx,
|
|
931
|
-
None,
|
|
932
|
-
&mut read,
|
|
933
|
-
PreparedWriteSet {
|
|
934
|
-
insert_identities: BTreeMap::new(),
|
|
935
|
-
state_rows,
|
|
936
|
-
commit_change_refs_by_branch: BTreeMap::new(),
|
|
937
|
-
extra_commit_parents_by_branch: BTreeMap::new(),
|
|
938
|
-
file_data_writes: Vec::new(),
|
|
939
|
-
},
|
|
940
|
-
)
|
|
941
|
-
.await
|
|
942
|
-
.expect("commit should flush untracked row");
|
|
943
|
-
storage
|
|
944
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
945
|
-
.expect("writes should commit");
|
|
946
|
-
|
|
947
|
-
let loaded = {
|
|
948
|
-
let mut untracked_reader = untracked_state.reader(
|
|
949
|
-
storage
|
|
950
|
-
.begin_read(StorageReadOptions::default())
|
|
951
|
-
.expect("read should open"),
|
|
952
|
-
);
|
|
953
|
-
untracked_reader
|
|
954
|
-
.load_row(&UntrackedStateRowRequest {
|
|
955
|
-
schema_key: "test_schema".to_string(),
|
|
956
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
957
|
-
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
958
|
-
file_id: NullableKeyFilter::Null,
|
|
959
|
-
})
|
|
960
|
-
.await
|
|
961
|
-
}
|
|
962
|
-
.expect("untracked row load should succeed")
|
|
963
|
-
.expect("untracked row should be persisted");
|
|
964
|
-
assert_eq!(
|
|
965
|
-
loaded.snapshot_content.as_deref(),
|
|
966
|
-
Some("{\"value\":\"untracked\"}")
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
#[tokio::test]
|
|
971
|
-
async fn tracked_write_deletes_matching_untracked_overlay() {
|
|
972
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
973
|
-
let binary_cas = BinaryCasContext::new();
|
|
974
|
-
let untracked_state = UntrackedStateContext::new();
|
|
975
|
-
let live_state = Arc::new(live_state_context());
|
|
976
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
977
|
-
|
|
978
|
-
let mut writes = StorageWriteSet::new();
|
|
979
|
-
let staged_row = untracked_global_row("change-untracked");
|
|
980
|
-
let canonical_row = crate::test_support::untracked_state_row_from_materialized(
|
|
981
|
-
&mut writes,
|
|
982
|
-
&MaterializedUntrackedStateRow::from(staged_row),
|
|
983
|
-
)
|
|
984
|
-
.expect("untracked seed should canonicalize");
|
|
985
|
-
untracked_state
|
|
986
|
-
.writer(&mut writes)
|
|
987
|
-
.stage_rows(std::iter::once(canonical_row.as_ref()))
|
|
988
|
-
.expect("untracked seed should write");
|
|
989
|
-
storage
|
|
990
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
991
|
-
.expect("untracked seed should commit");
|
|
992
|
-
|
|
993
|
-
let mut read = storage
|
|
994
|
-
.begin_read(StorageReadOptions::default())
|
|
995
|
-
.expect("read should open");
|
|
996
|
-
let state_rows = vec![tracked_global_row("change-tracked")];
|
|
997
|
-
let writes = commit_prepared_writes(
|
|
998
|
-
&binary_cas,
|
|
999
|
-
&branch_ctx,
|
|
1000
|
-
None,
|
|
1001
|
-
&mut read,
|
|
1002
|
-
PreparedWriteSet {
|
|
1003
|
-
insert_identities: BTreeMap::new(),
|
|
1004
|
-
state_rows,
|
|
1005
|
-
commit_change_refs_by_branch: BTreeMap::from([(
|
|
1006
|
-
GLOBAL_BRANCH_ID.to_string(),
|
|
1007
|
-
change_refs(["change-tracked"]),
|
|
1008
|
-
)]),
|
|
1009
|
-
extra_commit_parents_by_branch: BTreeMap::new(),
|
|
1010
|
-
file_data_writes: Vec::new(),
|
|
1011
|
-
},
|
|
1012
|
-
)
|
|
1013
|
-
.await
|
|
1014
|
-
.expect("tracked commit should flush");
|
|
1015
|
-
storage
|
|
1016
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1017
|
-
.expect("writes should commit");
|
|
1018
|
-
|
|
1019
|
-
let untracked = {
|
|
1020
|
-
let mut untracked_reader = untracked_state.reader(
|
|
1021
|
-
storage
|
|
1022
|
-
.begin_read(StorageReadOptions::default())
|
|
1023
|
-
.expect("read should open"),
|
|
1024
|
-
);
|
|
1025
|
-
untracked_reader.load_row(&untracked_request()).await
|
|
1026
|
-
}
|
|
1027
|
-
.expect("untracked load should succeed");
|
|
1028
|
-
assert_eq!(untracked, None);
|
|
1029
|
-
|
|
1030
|
-
let visible = live_state
|
|
1031
|
-
.reader(
|
|
1032
|
-
storage
|
|
1033
|
-
.begin_read(StorageReadOptions::default())
|
|
1034
|
-
.expect("read should open"),
|
|
1035
|
-
)
|
|
1036
|
-
.load_row(&live_state_request())
|
|
1037
|
-
.await
|
|
1038
|
-
.expect("live-state load should succeed")
|
|
1039
|
-
.expect("tracked row should be visible");
|
|
1040
|
-
assert!(!visible.untracked);
|
|
1041
|
-
assert_eq!(visible.change_id.as_deref(), Some("change-tracked"));
|
|
1042
|
-
assert_eq!(visible.snapshot_content.as_deref(), Some("{\"value\":1}"));
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
#[tokio::test]
|
|
1046
|
-
async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
|
|
1047
|
-
let counting_backend = CountingBackend::new();
|
|
1048
|
-
let write_batches = counting_backend.write_batches();
|
|
1049
|
-
let storage = StorageContext::new(counting_backend);
|
|
1050
|
-
let binary_cas = BinaryCasContext::new();
|
|
1051
|
-
let live_state = Arc::new(live_state_context());
|
|
1052
|
-
let untracked_state = UntrackedStateContext::new();
|
|
1053
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1054
|
-
{
|
|
1055
|
-
let mut read = storage
|
|
1056
|
-
.begin_read(StorageReadOptions::default())
|
|
1057
|
-
.expect("seed read should open");
|
|
1058
|
-
let mut writes = storage.new_write_set();
|
|
1059
|
-
crate::test_support::stage_tracked_root_from_materialized(
|
|
1060
|
-
&mut read,
|
|
1061
|
-
&mut writes,
|
|
1062
|
-
&crate::tracked_state::TrackedStateContext::new(),
|
|
1063
|
-
crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
|
|
1064
|
-
None,
|
|
1065
|
-
&[],
|
|
1066
|
-
)
|
|
1067
|
-
.await
|
|
1068
|
-
.expect("empty tracked root should stage");
|
|
1069
|
-
let branch_ref_row = crate::transaction::prepare_branch_ref_row(
|
|
1070
|
-
GLOBAL_BRANCH_ID,
|
|
1071
|
-
crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
|
|
1072
|
-
"1970-01-01T00:00:00.000Z",
|
|
1073
|
-
)
|
|
1074
|
-
.expect("global branch ref should stage");
|
|
1075
|
-
UntrackedStateContext::new()
|
|
1076
|
-
.writer(&mut writes)
|
|
1077
|
-
.stage_rows([branch_ref_row.row.as_ref()])
|
|
1078
|
-
.expect("global branch ref should stage");
|
|
1079
|
-
storage
|
|
1080
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1081
|
-
.expect("global branch ref should commit");
|
|
1082
|
-
}
|
|
1083
|
-
{
|
|
1084
|
-
let mut writes = StorageWriteSet::new();
|
|
1085
|
-
let mode_snapshot = serde_json::to_string(&serde_json::json!({
|
|
1086
|
-
"key": DETERMINISTIC_MODE_KEY,
|
|
1087
|
-
"value": { "enabled": true },
|
|
1088
|
-
}))
|
|
1089
|
-
.expect("mode snapshot should serialize");
|
|
1090
|
-
JsonStoreContext::new()
|
|
1091
|
-
.writer()
|
|
1092
|
-
.stage_batch(
|
|
1093
|
-
&mut writes,
|
|
1094
|
-
JsonWritePlacementRef::OutOfBand,
|
|
1095
|
-
[NormalizedJsonRef::new(mode_snapshot.as_str())],
|
|
1096
|
-
)
|
|
1097
|
-
.expect("deterministic mode snapshot should stage");
|
|
1098
|
-
let row = crate::untracked_state::UntrackedStateRow {
|
|
1099
|
-
entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_MODE_KEY),
|
|
1100
|
-
schema_key: "lix_key_value".to_string(),
|
|
1101
|
-
file_id: None,
|
|
1102
|
-
snapshot_content: Some(mode_snapshot.to_string()),
|
|
1103
|
-
metadata: None,
|
|
1104
|
-
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1105
|
-
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1106
|
-
global: true,
|
|
1107
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
1108
|
-
};
|
|
1109
|
-
UntrackedStateContext::new()
|
|
1110
|
-
.writer(&mut writes)
|
|
1111
|
-
.stage_rows(std::iter::once(row.as_ref()))
|
|
1112
|
-
.expect("deterministic mode should stage");
|
|
1113
|
-
storage
|
|
1114
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1115
|
-
.expect("deterministic mode should commit");
|
|
1116
|
-
}
|
|
1117
|
-
write_batches.store(0, Ordering::SeqCst);
|
|
1118
|
-
let runtime_functions = {
|
|
1119
|
-
let reader = live_state.reader(
|
|
1120
|
-
storage
|
|
1121
|
-
.begin_read(StorageReadOptions::default())
|
|
1122
|
-
.expect("read should open"),
|
|
1123
|
-
);
|
|
1124
|
-
FunctionContext::prepare(&reader)
|
|
1125
|
-
.await
|
|
1126
|
-
.expect("runtime context should prepare")
|
|
1127
|
-
};
|
|
1128
|
-
runtime_functions.provider().call_uuid_v7();
|
|
1129
|
-
let mut read = storage
|
|
1130
|
-
.begin_read(StorageReadOptions::default())
|
|
1131
|
-
.expect("read should open");
|
|
1132
|
-
|
|
1133
|
-
let tracked_row = tracked_global_row("change-tracked");
|
|
1134
|
-
let mut untracked_row = untracked_global_row("change-untracked");
|
|
1135
|
-
untracked_row.entity_pk = crate::entity_pk::EntityPk::single("entity-2");
|
|
1136
|
-
|
|
1137
|
-
let writes = commit_prepared_writes(
|
|
1138
|
-
&binary_cas,
|
|
1139
|
-
&branch_ctx,
|
|
1140
|
-
Some(&runtime_functions),
|
|
1141
|
-
&mut read,
|
|
1142
|
-
PreparedWriteSet {
|
|
1143
|
-
insert_identities: BTreeMap::new(),
|
|
1144
|
-
state_rows: vec![tracked_row, untracked_row],
|
|
1145
|
-
commit_change_refs_by_branch: BTreeMap::from([(
|
|
1146
|
-
GLOBAL_BRANCH_ID.to_string(),
|
|
1147
|
-
change_refs(["change-tracked"]),
|
|
1148
|
-
)]),
|
|
1149
|
-
extra_commit_parents_by_branch: BTreeMap::new(),
|
|
1150
|
-
file_data_writes: Vec::new(),
|
|
1151
|
-
},
|
|
1152
|
-
)
|
|
1153
|
-
.await
|
|
1154
|
-
.expect("cross-subsystem commit should stage and apply");
|
|
1155
|
-
|
|
1156
|
-
assert_eq!(
|
|
1157
|
-
write_batches.load(Ordering::SeqCst),
|
|
1158
|
-
0,
|
|
1159
|
-
"prepared writes should not touch the backend before the write set is committed"
|
|
1160
|
-
);
|
|
1161
|
-
storage
|
|
1162
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1163
|
-
.expect("writes should commit");
|
|
1164
|
-
assert_eq!(write_batches.load(Ordering::SeqCst), 1);
|
|
1165
|
-
|
|
1166
|
-
let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
|
|
1167
|
-
storage
|
|
1168
|
-
.begin_read(StorageReadOptions::default())
|
|
1169
|
-
.expect("read should open"),
|
|
1170
|
-
);
|
|
1171
|
-
let commits = changelog_reader
|
|
1172
|
-
.load_commits(crate::changelog::CommitLoadRequest {
|
|
1173
|
-
commit_ids: &["test-uuid-1".to_string()],
|
|
1174
|
-
projection: crate::changelog::CommitProjection::Record,
|
|
1175
|
-
})
|
|
1176
|
-
.await
|
|
1177
|
-
.expect("changelog commit should load");
|
|
1178
|
-
let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
|
|
1179
|
-
commits.entries.into_iter().next().flatten()
|
|
1180
|
-
else {
|
|
1181
|
-
panic!("changelog commit should exist");
|
|
1182
|
-
};
|
|
1183
|
-
assert_eq!(commit.change_id, "test-uuid-2");
|
|
1184
|
-
let changes = changelog_reader
|
|
1185
|
-
.load_changes(crate::changelog::ChangeLoadRequest {
|
|
1186
|
-
change_ids: &["change-tracked".to_string()],
|
|
1187
|
-
})
|
|
1188
|
-
.await
|
|
1189
|
-
.expect("changelog change should load");
|
|
1190
|
-
assert!(matches!(
|
|
1191
|
-
changes.entries.as_slice(),
|
|
1192
|
-
[Some(change)] if change.change_id == "change-tracked"
|
|
1193
|
-
));
|
|
1194
|
-
|
|
1195
|
-
let loaded_head = branch_ctx
|
|
1196
|
-
.ref_reader(
|
|
1197
|
-
storage
|
|
1198
|
-
.begin_read(StorageReadOptions::default())
|
|
1199
|
-
.expect("read should open"),
|
|
1200
|
-
)
|
|
1201
|
-
.load_head_commit_id(GLOBAL_BRANCH_ID)
|
|
1202
|
-
.await
|
|
1203
|
-
.expect("branch ref load should succeed");
|
|
1204
|
-
assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
|
|
1205
|
-
|
|
1206
|
-
let untracked = {
|
|
1207
|
-
let mut untracked_reader = untracked_state.reader(
|
|
1208
|
-
storage
|
|
1209
|
-
.begin_read(StorageReadOptions::default())
|
|
1210
|
-
.expect("read should open"),
|
|
1211
|
-
);
|
|
1212
|
-
untracked_reader
|
|
1213
|
-
.load_row(&UntrackedStateRowRequest {
|
|
1214
|
-
schema_key: "test_schema".to_string(),
|
|
1215
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
1216
|
-
entity_pk: crate::entity_pk::EntityPk::single("entity-2"),
|
|
1217
|
-
file_id: NullableKeyFilter::Null,
|
|
1218
|
-
})
|
|
1219
|
-
.await
|
|
1220
|
-
}
|
|
1221
|
-
.expect("untracked row load should succeed")
|
|
1222
|
-
.expect("untracked row should persist");
|
|
1223
|
-
assert_eq!(
|
|
1224
|
-
untracked.snapshot_content.as_deref(),
|
|
1225
|
-
Some("{\"value\":\"untracked\"}")
|
|
1226
|
-
);
|
|
1227
|
-
|
|
1228
|
-
let sequence_row = live_state
|
|
1229
|
-
.reader(
|
|
1230
|
-
storage
|
|
1231
|
-
.begin_read(StorageReadOptions::default())
|
|
1232
|
-
.expect("read should open"),
|
|
1233
|
-
)
|
|
1234
|
-
.load_row(&LiveStateRowRequest {
|
|
1235
|
-
schema_key: "lix_key_value".to_string(),
|
|
1236
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
1237
|
-
entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_SEQUENCE_KEY),
|
|
1238
|
-
file_id: NullableKeyFilter::Null,
|
|
1239
|
-
})
|
|
1240
|
-
.await
|
|
1241
|
-
.expect("deterministic sequence should load")
|
|
1242
|
-
.expect("deterministic sequence should persist");
|
|
1243
|
-
assert_eq!(
|
|
1244
|
-
sequence_row.snapshot_content.as_deref(),
|
|
1245
|
-
Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":0}")
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
#[tokio::test]
|
|
1250
|
-
async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_branch() {
|
|
1251
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1252
|
-
let binary_cas = BinaryCasContext::new();
|
|
1253
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1254
|
-
crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
|
|
1255
|
-
.await;
|
|
1256
|
-
crate::test_support::seed_branch_head(storage.clone(), "branch-a", "branch-a-before").await;
|
|
1257
|
-
|
|
1258
|
-
let mut read = storage
|
|
1259
|
-
.begin_read(StorageReadOptions::default())
|
|
1260
|
-
.expect("read should open");
|
|
1261
|
-
let state_rows = vec![tracked_branch_row("branch-a", "change-branch-a")];
|
|
1262
|
-
let writes = commit_prepared_writes(
|
|
1263
|
-
&binary_cas,
|
|
1264
|
-
&branch_ctx,
|
|
1265
|
-
None,
|
|
1266
|
-
&mut read,
|
|
1267
|
-
PreparedWriteSet {
|
|
1268
|
-
insert_identities: BTreeMap::new(),
|
|
1269
|
-
state_rows,
|
|
1270
|
-
commit_change_refs_by_branch: BTreeMap::from([(
|
|
1271
|
-
"branch-a".to_string(),
|
|
1272
|
-
change_refs(["change-branch-a"]),
|
|
1273
|
-
)]),
|
|
1274
|
-
extra_commit_parents_by_branch: BTreeMap::new(),
|
|
1275
|
-
file_data_writes: Vec::new(),
|
|
1276
|
-
},
|
|
1277
|
-
)
|
|
1278
|
-
.await
|
|
1279
|
-
.expect("branch commit should flush");
|
|
1280
|
-
storage
|
|
1281
|
-
.commit_write_set(writes, StorageWriteOptions::default())
|
|
1282
|
-
.expect("writes should commit");
|
|
1283
|
-
|
|
1284
|
-
let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
|
|
1285
|
-
storage
|
|
1286
|
-
.begin_read(StorageReadOptions::default())
|
|
1287
|
-
.expect("read should open"),
|
|
1288
|
-
);
|
|
1289
|
-
let commits = changelog_reader
|
|
1290
|
-
.load_commits(crate::changelog::CommitLoadRequest {
|
|
1291
|
-
commit_ids: &["test-uuid-1".to_string()],
|
|
1292
|
-
projection: crate::changelog::CommitProjection::Record,
|
|
1293
|
-
})
|
|
1294
|
-
.await
|
|
1295
|
-
.expect("changelog commit should load");
|
|
1296
|
-
let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
|
|
1297
|
-
commits.entries.into_iter().next().flatten()
|
|
1298
|
-
else {
|
|
1299
|
-
panic!("changelog commit should exist");
|
|
1300
|
-
};
|
|
1301
|
-
assert_eq!(commit.change_id, "test-uuid-2");
|
|
1302
|
-
assert_eq!(commit.parent_commit_ids, vec!["branch-a-before"]);
|
|
1303
|
-
|
|
1304
|
-
let global_head = branch_ctx
|
|
1305
|
-
.ref_reader(
|
|
1306
|
-
storage
|
|
1307
|
-
.begin_read(StorageReadOptions::default())
|
|
1308
|
-
.expect("read should open"),
|
|
1309
|
-
)
|
|
1310
|
-
.load_head_commit_id(GLOBAL_BRANCH_ID)
|
|
1311
|
-
.await
|
|
1312
|
-
.expect("global head should load");
|
|
1313
|
-
let branch_head = branch_ctx
|
|
1314
|
-
.ref_reader(
|
|
1315
|
-
storage
|
|
1316
|
-
.begin_read(StorageReadOptions::default())
|
|
1317
|
-
.expect("read should open"),
|
|
1318
|
-
)
|
|
1319
|
-
.load_head_commit_id("branch-a")
|
|
1320
|
-
.await
|
|
1321
|
-
.expect("branch head should load");
|
|
1322
|
-
assert_eq!(global_head.as_deref(), Some("global-before"));
|
|
1323
|
-
assert_eq!(branch_head.as_deref(), Some("test-uuid-1"));
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
#[tokio::test]
|
|
1327
|
-
async fn finalize_commit_rows_parents_global_commit_to_existing_branch_ref() {
|
|
1328
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1329
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1330
|
-
crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "initial-commit")
|
|
1331
|
-
.await;
|
|
1332
|
-
|
|
1333
|
-
let mut read = storage
|
|
1334
|
-
.begin_read(StorageReadOptions::default())
|
|
1335
|
-
.expect("read should open");
|
|
1336
|
-
let rows = finalize_commit_rows(
|
|
1337
|
-
BTreeMap::from([(
|
|
1338
|
-
GLOBAL_BRANCH_ID.to_string(),
|
|
1339
|
-
change_refs(["change-a", "change-b"]),
|
|
1340
|
-
)]),
|
|
1341
|
-
BTreeMap::new(),
|
|
1342
|
-
&branch_ctx,
|
|
1343
|
-
&mut read,
|
|
1344
|
-
)
|
|
1345
|
-
.await
|
|
1346
|
-
.expect("global commit row should finalize");
|
|
1347
|
-
|
|
1348
|
-
assert_eq!(rows.commit_rows.len(), 1);
|
|
1349
|
-
assert_eq!(rows.branch_heads.len(), 1);
|
|
1350
|
-
let row = &rows.commit_rows[0];
|
|
1351
|
-
assert_eq!(row.commit_id, "test-uuid-1");
|
|
1352
|
-
assert_eq!(row.change_id, "test-uuid-2");
|
|
1353
|
-
assert_eq!(row.created_at, "test-timestamp-1");
|
|
1354
|
-
assert_eq!(row.parent_commit_ids, vec!["initial-commit"]);
|
|
1355
|
-
|
|
1356
|
-
let branch_head = &rows.branch_heads[0];
|
|
1357
|
-
assert_eq!(branch_head.branch_id, GLOBAL_BRANCH_ID);
|
|
1358
|
-
assert_eq!(branch_head.commit_id, "test-uuid-1");
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
#[tokio::test]
|
|
1362
|
-
async fn finalize_commit_rows_skips_empty_members() {
|
|
1363
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1364
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1365
|
-
let mut read = storage
|
|
1366
|
-
.begin_read(StorageReadOptions::default())
|
|
1367
|
-
.expect("read should open");
|
|
1368
|
-
let rows = finalize_commit_rows(
|
|
1369
|
-
BTreeMap::from([(
|
|
1370
|
-
GLOBAL_BRANCH_ID.to_string(),
|
|
1371
|
-
StagedCommitChangeRefs::default(),
|
|
1372
|
-
)]),
|
|
1373
|
-
BTreeMap::new(),
|
|
1374
|
-
&branch_ctx,
|
|
1375
|
-
&mut read,
|
|
1376
|
-
)
|
|
1377
|
-
.await
|
|
1378
|
-
.expect("empty change_refs should be ignored");
|
|
1379
|
-
|
|
1380
|
-
assert!(rows.commit_rows.is_empty());
|
|
1381
|
-
assert!(rows.branch_heads.is_empty());
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
#[tokio::test]
|
|
1385
|
-
async fn finalize_commit_rows_uses_existing_branch_ref_as_parent() {
|
|
1386
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1387
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1388
|
-
crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
|
|
1389
|
-
.await;
|
|
1390
|
-
crate::test_support::seed_branch_head(storage.clone(), "branch-a", "previous-commit").await;
|
|
1391
|
-
|
|
1392
|
-
let mut read = storage
|
|
1393
|
-
.begin_read(StorageReadOptions::default())
|
|
1394
|
-
.expect("read should open");
|
|
1395
|
-
let rows = finalize_commit_rows(
|
|
1396
|
-
BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
|
|
1397
|
-
BTreeMap::new(),
|
|
1398
|
-
&branch_ctx,
|
|
1399
|
-
&mut read,
|
|
1400
|
-
)
|
|
1401
|
-
.await
|
|
1402
|
-
.expect("active-branch commit finalization should resolve parent");
|
|
1403
|
-
|
|
1404
|
-
assert_eq!(
|
|
1405
|
-
rows.commit_rows[0].parent_commit_ids,
|
|
1406
|
-
vec!["previous-commit"]
|
|
1407
|
-
);
|
|
1408
|
-
assert_eq!(rows.branch_heads[0].branch_id, "branch-a");
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
#[tokio::test]
|
|
1412
|
-
async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
|
|
1413
|
-
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
1414
|
-
let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
|
|
1415
|
-
crate::test_support::seed_branch_head(storage.clone(), "branch-a", "target-head").await;
|
|
1416
|
-
|
|
1417
|
-
let mut read = storage
|
|
1418
|
-
.begin_read(StorageReadOptions::default())
|
|
1419
|
-
.expect("read should open");
|
|
1420
|
-
let rows = finalize_commit_rows(
|
|
1421
|
-
BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
|
|
1422
|
-
BTreeMap::from([("branch-a".to_string(), vec!["source-head".to_string()])]),
|
|
1423
|
-
&branch_ctx,
|
|
1424
|
-
&mut read,
|
|
1425
|
-
)
|
|
1426
|
-
.await
|
|
1427
|
-
.expect("merge commit finalization should resolve parents");
|
|
1428
|
-
|
|
1429
|
-
assert_eq!(
|
|
1430
|
-
rows.commit_rows[0].parent_commit_ids,
|
|
1431
|
-
vec!["target-head", "source-head"]
|
|
1432
|
-
);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
fn change_refs<const N: usize>(change_ids: [&str; N]) -> StagedCommitChangeRefs {
|
|
1436
|
-
let mut change_refs = StagedCommitChangeRefs::new(
|
|
1437
|
-
"test-uuid-1".to_string(),
|
|
1438
|
-
"test-uuid-2".to_string(),
|
|
1439
|
-
"test-timestamp-1".to_string(),
|
|
1440
|
-
);
|
|
1441
|
-
for change_id in change_ids {
|
|
1442
|
-
change_refs.add_change_id(change_id.to_string());
|
|
1443
|
-
}
|
|
1444
|
-
change_refs
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
fn tracked_global_row(change_id: &str) -> PreparedStateRow {
|
|
1448
|
-
tracked_branch_row(GLOBAL_BRANCH_ID, change_id)
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
fn tracked_branch_row(branch_id: &str, change_id: &str) -> PreparedStateRow {
|
|
1452
|
-
PreparedStateRow {
|
|
1453
|
-
schema_plan_id: SchemaPlanId::for_test(0),
|
|
1454
|
-
facts: PreparedRowFacts::default(),
|
|
1455
|
-
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
1456
|
-
schema_key: "test_schema".to_string(),
|
|
1457
|
-
file_id: None,
|
|
1458
|
-
snapshot: Some(
|
|
1459
|
-
crate::transaction::types::stage_json_from_value(
|
|
1460
|
-
crate::transaction::types::TransactionJson::from_value_for_test(
|
|
1461
|
-
serde_json::json!({ "value": 1 }),
|
|
1462
|
-
),
|
|
1463
|
-
"test tracked row snapshot",
|
|
1464
|
-
)
|
|
1465
|
-
.expect("test snapshot should stage"),
|
|
1466
|
-
),
|
|
1467
|
-
metadata: None,
|
|
1468
|
-
origin: None,
|
|
1469
|
-
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1470
|
-
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
1471
|
-
global: branch_id == GLOBAL_BRANCH_ID,
|
|
1472
|
-
change_id: Some(change_id.to_string()),
|
|
1473
|
-
commit_id: Some("test-uuid-1".to_string()),
|
|
1474
|
-
untracked: false,
|
|
1475
|
-
branch_id: branch_id.to_string(),
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
fn untracked_global_row(change_id: &str) -> PreparedStateRow {
|
|
1480
|
-
let mut row = tracked_global_row(change_id);
|
|
1481
|
-
row.snapshot = Some(
|
|
1482
|
-
crate::transaction::types::stage_json_from_value(
|
|
1483
|
-
crate::transaction::types::TransactionJson::from_value_for_test(
|
|
1484
|
-
serde_json::json!({ "value": "untracked" }),
|
|
1485
|
-
),
|
|
1486
|
-
"test untracked row snapshot",
|
|
1487
|
-
)
|
|
1488
|
-
.expect("test snapshot should stage"),
|
|
1489
|
-
);
|
|
1490
|
-
PreparedStateRow {
|
|
1491
|
-
change_id: None,
|
|
1492
|
-
commit_id: None,
|
|
1493
|
-
untracked: true,
|
|
1494
|
-
..row
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
fn untracked_request() -> UntrackedStateRowRequest {
|
|
1499
|
-
UntrackedStateRowRequest {
|
|
1500
|
-
schema_key: "test_schema".to_string(),
|
|
1501
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
1502
|
-
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
1503
|
-
file_id: NullableKeyFilter::Null,
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
fn live_state_request() -> LiveStateRowRequest {
|
|
1508
|
-
LiveStateRowRequest {
|
|
1509
|
-
schema_key: "test_schema".to_string(),
|
|
1510
|
-
branch_id: GLOBAL_BRANCH_ID.to_string(),
|
|
1511
|
-
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
1512
|
-
file_id: NullableKeyFilter::Null,
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
struct CountingBackend {
|
|
1517
|
-
inner: InMemoryStorageBackend,
|
|
1518
|
-
write_batches: Arc<AtomicUsize>,
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
impl CountingBackend {
|
|
1522
|
-
fn new() -> Self {
|
|
1523
|
-
Self {
|
|
1524
|
-
inner: InMemoryStorageBackend::new(),
|
|
1525
|
-
write_batches: Arc::new(AtomicUsize::new(0)),
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
fn write_batches(&self) -> Arc<AtomicUsize> {
|
|
1530
|
-
Arc::clone(&self.write_batches)
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
impl Backend for CountingBackend {
|
|
1535
|
-
type Read<'a>
|
|
1536
|
-
= InMemoryStorageRead
|
|
1537
|
-
where
|
|
1538
|
-
Self: 'a;
|
|
1539
|
-
|
|
1540
|
-
type Write<'a>
|
|
1541
|
-
= CountingWrite
|
|
1542
|
-
where
|
|
1543
|
-
Self: 'a;
|
|
1544
|
-
|
|
1545
|
-
fn capabilities(&self) -> BackendCapabilities {
|
|
1546
|
-
self.inner.capabilities()
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
fn begin_read(&self, opts: StorageReadOptions) -> Result<Self::Read<'_>, BackendError> {
|
|
1550
|
-
self.inner.begin_read(opts)
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
fn begin_write(&self, opts: StorageWriteOptions) -> Result<Self::Write<'_>, BackendError> {
|
|
1554
|
-
Ok(CountingWrite {
|
|
1555
|
-
inner: self.inner.begin_write(opts)?,
|
|
1556
|
-
write_batches: Arc::clone(&self.write_batches),
|
|
1557
|
-
})
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
fn durable_write_lock(&self) -> DurableWriteLock {
|
|
1561
|
-
self.inner.durable_write_lock()
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
struct CountingWrite {
|
|
1566
|
-
inner: InMemoryStorageWrite,
|
|
1567
|
-
write_batches: Arc<AtomicUsize>,
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
impl BackendWrite for CountingWrite {
|
|
1571
|
-
fn put_many(&mut self, entries: PutBatch) -> Result<(), BackendError> {
|
|
1572
|
-
self.inner.put_many(entries)
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
fn delete_many(&mut self, keys: &[StorageKey]) -> Result<(), BackendError> {
|
|
1576
|
-
self.inner.delete_many(keys)
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
fn delete_range(&mut self, range: KeyRange) -> Result<(), BackendError> {
|
|
1580
|
-
self.inner.delete_range(range)
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
fn commit(self) -> Result<CommitResult, BackendError> {
|
|
1584
|
-
self.write_batches.fetch_add(1, Ordering::SeqCst);
|
|
1585
|
-
self.inner.commit()
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
fn rollback(self) -> Result<(), BackendError> {
|
|
1589
|
-
self.inner.rollback()
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|