@lix-js/sdk 0.6.0-preview.4 → 0.6.0
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 +39 -201
- 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 +20 -50
- package/SKILL.md +0 -506
- 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 -821
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -26
- 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 -303
- package/dist-engine-src/README.md +0 -18
- package/dist-engine-src/src/backend/kv.rs +0 -358
- package/dist-engine-src/src/backend/mod.rs +0 -12
- package/dist-engine-src/src/backend/testing.rs +0 -658
- package/dist-engine-src/src/backend/types.rs +0 -96
- 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 -1063
- 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/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/commit_graph/context.rs +0 -901
- package/dist-engine-src/src/commit_graph/mod.rs +0 -11
- package/dist-engine-src/src/commit_graph/types.rs +0 -109
- package/dist-engine-src/src/commit_graph/walker.rs +0 -756
- package/dist-engine-src/src/commit_store/codec.rs +0 -887
- package/dist-engine-src/src/commit_store/context.rs +0 -944
- package/dist-engine-src/src/commit_store/materialization.rs +0 -84
- package/dist-engine-src/src/commit_store/mod.rs +0 -16
- package/dist-engine-src/src/commit_store/storage.rs +0 -600
- package/dist-engine-src/src/commit_store/types.rs +0 -215
- package/dist-engine-src/src/common/error.rs +0 -313
- 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 -324
- package/dist-engine-src/src/engine.rs +0 -225
- package/dist-engine-src/src/entity_identity.rs +0 -405
- package/dist-engine-src/src/functions/context.rs +0 -292
- 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 -336
- package/dist-engine-src/src/functions/types.rs +0 -37
- package/dist-engine-src/src/init.rs +0 -558
- package/dist-engine-src/src/json_store/compression.rs +0 -77
- package/dist-engine-src/src/json_store/context.rs +0 -423
- package/dist-engine-src/src/json_store/encoded.rs +0 -15
- package/dist-engine-src/src/json_store/mod.rs +0 -12
- package/dist-engine-src/src/json_store/store.rs +0 -1109
- package/dist-engine-src/src/json_store/types.rs +0 -217
- package/dist-engine-src/src/lib.rs +0 -62
- package/dist-engine-src/src/live_state/context.rs +0 -2019
- package/dist-engine-src/src/live_state/mod.rs +0 -15
- 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 -222
- package/dist-engine-src/src/live_state/visibility.rs +0 -223
- 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 -477
- package/dist-engine-src/src/plugin/mod.rs +0 -33
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -118
- 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_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/lix_version_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
- package/dist-engine-src/src/schema/builtin/mod.rs +0 -222
- 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 -404
- package/dist-engine-src/src/session/create_version.rs +0 -88
- package/dist-engine-src/src/session/execute.rs +0 -541
- package/dist-engine-src/src/session/merge/analysis.rs +0 -102
- package/dist-engine-src/src/session/merge/apply.rs +0 -23
- package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
- package/dist-engine-src/src/session/merge/mod.rs +0 -11
- package/dist-engine-src/src/session/merge/stats.rs +0 -65
- package/dist-engine-src/src/session/merge/version.rs +0 -427
- package/dist-engine-src/src/session/mod.rs +0 -27
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
- package/dist-engine-src/src/session/switch_version.rs +0 -110
- package/dist-engine-src/src/session/transaction.rs +0 -76
- package/dist-engine-src/src/sql2/change_provider.rs +0 -331
- package/dist-engine-src/src/sql2/classify.rs +0 -174
- package/dist-engine-src/src/sql2/context.rs +0 -311
- package/dist-engine-src/src/sql2/directory_history_provider.rs +0 -631
- package/dist-engine-src/src/sql2/directory_provider.rs +0 -2453
- package/dist-engine-src/src/sql2/dml.rs +0 -148
- package/dist-engine-src/src/sql2/entity_history_provider.rs +0 -440
- package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
- package/dist-engine-src/src/sql2/error.rs +0 -215
- package/dist-engine-src/src/sql2/execute.rs +0 -3533
- package/dist-engine-src/src/sql2/file_history_provider.rs +0 -910
- package/dist-engine-src/src/sql2/file_provider.rs +0 -3679
- package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1490
- 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_provider.rs +0 -412
- package/dist-engine-src/src/sql2/history_route.rs +0 -657
- package/dist-engine-src/src/sql2/lix_state_provider.rs +0 -2512
- package/dist-engine-src/src/sql2/mod.rs +0 -47
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -246
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
- package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
- package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
- package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
- package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
- 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 -132
- package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
- package/dist-engine-src/src/sql2/udfs/lix_active_version_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 -89
- package/dist-engine-src/src/sql2/udfs/public_call.rs +0 -238
- package/dist-engine-src/src/sql2/version_provider.rs +0 -1202
- package/dist-engine-src/src/sql2/version_scope.rs +0 -394
- package/dist-engine-src/src/sql2/write_normalization.rs +0 -345
- package/dist-engine-src/src/storage/context.rs +0 -356
- package/dist-engine-src/src/storage/mod.rs +0 -14
- package/dist-engine-src/src/storage/read_scope.rs +0 -88
- package/dist-engine-src/src/storage/types.rs +0 -501
- package/dist-engine-src/src/storage_bench.rs +0 -4863
- package/dist-engine-src/src/test_support.rs +0 -228
- package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
- package/dist-engine-src/src/tracked_state/codec.rs +0 -2085
- package/dist-engine-src/src/tracked_state/context.rs +0 -1867
- package/dist-engine-src/src/tracked_state/diff.rs +0 -686
- package/dist-engine-src/src/tracked_state/materialization.rs +0 -403
- package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
- package/dist-engine-src/src/tracked_state/merge.rs +0 -492
- package/dist-engine-src/src/tracked_state/mod.rs +0 -32
- package/dist-engine-src/src/tracked_state/storage.rs +0 -375
- package/dist-engine-src/src/tracked_state/tree.rs +0 -3187
- package/dist-engine-src/src/tracked_state/types.rs +0 -231
- package/dist-engine-src/src/transaction/commit.rs +0 -1484
- package/dist-engine-src/src/transaction/context.rs +0 -1548
- package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
- package/dist-engine-src/src/transaction/mod.rs +0 -13
- package/dist-engine-src/src/transaction/normalization.rs +0 -890
- package/dist-engine-src/src/transaction/prep.rs +0 -37
- package/dist-engine-src/src/transaction/schema_resolver.rs +0 -149
- package/dist-engine-src/src/transaction/staging.rs +0 -1731
- package/dist-engine-src/src/transaction/types.rs +0 -460
- package/dist-engine-src/src/transaction/validation.rs +0 -5830
- package/dist-engine-src/src/untracked_state/codec.rs +0 -307
- 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 -396
- package/dist-engine-src/src/untracked_state/types.rs +0 -146
- package/dist-engine-src/src/version/context.rs +0 -40
- package/dist-engine-src/src/version/lifecycle.rs +0 -221
- package/dist-engine-src/src/version/mod.rs +0 -13
- package/dist-engine-src/src/version/refs.rs +0 -330
- package/dist-engine-src/src/version/stage_rows.rs +0 -67
- package/dist-engine-src/src/version/types.rs +0 -21
- package/dist-engine-src/src/wasm/mod.rs +0 -60
|
@@ -1,1548 +0,0 @@
|
|
|
1
|
-
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
-
use std::sync::Arc;
|
|
3
|
-
|
|
4
|
-
use async_trait::async_trait;
|
|
5
|
-
use serde_json::Value as JsonValue;
|
|
6
|
-
|
|
7
|
-
use crate::binary_cas::{BinaryCasContext, BlobBytesBatch, BlobHash};
|
|
8
|
-
use crate::catalog::CatalogContext;
|
|
9
|
-
use crate::commit_graph::{CommitGraphContext, CommitGraphStoreReader};
|
|
10
|
-
use crate::commit_store::CommitStoreContext;
|
|
11
|
-
use crate::domain::Domain;
|
|
12
|
-
use crate::entity_identity::EntityIdentity;
|
|
13
|
-
use crate::functions::{FunctionContext, FunctionProviderHandle};
|
|
14
|
-
use crate::live_state::{
|
|
15
|
-
LiveStateContext, LiveStateRowRequest, LiveStateScanRequest, MaterializedLiveStateRow,
|
|
16
|
-
};
|
|
17
|
-
use crate::session::{SessionMode, WORKSPACE_VERSION_KEY};
|
|
18
|
-
use crate::sql2::SqlWriteExecutionContext;
|
|
19
|
-
use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
|
|
20
|
-
use crate::tracked_state::{TrackedStateContext, TrackedStateStoreReader};
|
|
21
|
-
use crate::transaction::commit;
|
|
22
|
-
use crate::transaction::live_state_overlay::overlay_scan_rows;
|
|
23
|
-
use crate::transaction::normalization::{
|
|
24
|
-
normalize_transaction_write_row, remember_pending_registered_schema,
|
|
25
|
-
NormalizedTransactionWriteRow, REGISTERED_SCHEMA_KEY,
|
|
26
|
-
};
|
|
27
|
-
use crate::transaction::prepare_version_ref_row;
|
|
28
|
-
use crate::transaction::schema_resolver::TransactionSchemaResolver;
|
|
29
|
-
use crate::transaction::staging::{PreparedWriteSet, TransactionWriteBuffer};
|
|
30
|
-
use crate::transaction::types::{
|
|
31
|
-
stage_json_from_value, PreparedAdoptedStateRow, PreparedRowFacts, PreparedStateRow,
|
|
32
|
-
PreparedTransactionWrite, TransactionAdoptedChange, TransactionFileData, TransactionJson,
|
|
33
|
-
TransactionWrite, TransactionWriteMode, TransactionWriteOutcome, TransactionWriteRow,
|
|
34
|
-
};
|
|
35
|
-
use crate::transaction::validation::{validate_prepared_writes, TransactionValidationInput};
|
|
36
|
-
use crate::version::{VersionContext, VersionRefReader};
|
|
37
|
-
use crate::GLOBAL_VERSION_ID;
|
|
38
|
-
use crate::{LixError, NullableKeyFilter};
|
|
39
|
-
|
|
40
|
-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
41
|
-
pub(crate) struct TransactionCommitOutcome;
|
|
42
|
-
|
|
43
|
-
/// One execution-scoped transaction capability for engine write paths.
|
|
44
|
-
///
|
|
45
|
-
/// This is intentionally not a session-wide kitchen sink. It owns the backend
|
|
46
|
-
/// write transaction for one `SessionContext::execute(...)` call and projects
|
|
47
|
-
/// accepted SQL/provider writes back into the SQL DAG through an engine-local live-state
|
|
48
|
-
/// overlay.
|
|
49
|
-
///
|
|
50
|
-
/// Transaction invariant: this is the capability for engine operations
|
|
51
|
-
/// that may write. Write-relevant reads must be exposed from this transaction,
|
|
52
|
-
/// after the backend write transaction has begun, rather than from session-level
|
|
53
|
-
/// helpers.
|
|
54
|
-
pub(crate) struct Transaction {
|
|
55
|
-
active_version_id: String,
|
|
56
|
-
live_state: Arc<LiveStateContext>,
|
|
57
|
-
tracked_state: Arc<TrackedStateContext>,
|
|
58
|
-
binary_cas: Arc<BinaryCasContext>,
|
|
59
|
-
commit_store: Arc<CommitStoreContext>,
|
|
60
|
-
version_ctx: Arc<VersionContext>,
|
|
61
|
-
schema_resolver: TransactionSchemaResolver,
|
|
62
|
-
staged_writes: Arc<TransactionWriteBuffer>,
|
|
63
|
-
storage_transaction: Box<dyn StorageWriteTransaction + Send + Sync + 'static>,
|
|
64
|
-
visible_schemas: Vec<JsonValue>,
|
|
65
|
-
functions: FunctionProviderHandle,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
impl Transaction {
|
|
69
|
-
/// Opens a backend write transaction and creates an execution-scoped
|
|
70
|
-
/// staging area for SQL/provider hooks.
|
|
71
|
-
async fn open(
|
|
72
|
-
mode: &SessionMode,
|
|
73
|
-
storage: StorageContext,
|
|
74
|
-
live_state: Arc<LiveStateContext>,
|
|
75
|
-
tracked_state: Arc<TrackedStateContext>,
|
|
76
|
-
binary_cas: Arc<BinaryCasContext>,
|
|
77
|
-
commit_store: Arc<CommitStoreContext>,
|
|
78
|
-
version_ctx: Arc<VersionContext>,
|
|
79
|
-
catalog_context: Arc<CatalogContext>,
|
|
80
|
-
) -> Result<OpenTransaction, LixError> {
|
|
81
|
-
let mut storage_transaction = storage.begin_write_transaction().await?;
|
|
82
|
-
let setup_result = async {
|
|
83
|
-
let active_version_id = resolve_active_version_id(
|
|
84
|
-
mode,
|
|
85
|
-
live_state.as_ref(),
|
|
86
|
-
version_ctx.as_ref(),
|
|
87
|
-
storage_transaction.as_mut(),
|
|
88
|
-
)
|
|
89
|
-
.await?;
|
|
90
|
-
let runtime_functions = {
|
|
91
|
-
let runtime_live_state = live_state.reader(storage_transaction.as_mut());
|
|
92
|
-
FunctionContext::prepare(&runtime_live_state).await?
|
|
93
|
-
};
|
|
94
|
-
let functions = runtime_functions.provider();
|
|
95
|
-
let visible_schemas = {
|
|
96
|
-
let visible_live_state = live_state.reader(storage_transaction.as_mut());
|
|
97
|
-
catalog_context
|
|
98
|
-
.schema_jsons_for_sql_read_planning(&visible_live_state, &active_version_id)
|
|
99
|
-
.await?
|
|
100
|
-
};
|
|
101
|
-
let schema_facts = {
|
|
102
|
-
let visible_live_state = live_state.reader(storage_transaction.as_mut());
|
|
103
|
-
catalog_context
|
|
104
|
-
.schema_facts_for_domain(
|
|
105
|
-
&visible_live_state,
|
|
106
|
-
&Domain::schema_catalog(active_version_id.clone(), true),
|
|
107
|
-
)
|
|
108
|
-
.await?
|
|
109
|
-
};
|
|
110
|
-
Ok::<_, LixError>((
|
|
111
|
-
active_version_id,
|
|
112
|
-
runtime_functions,
|
|
113
|
-
functions,
|
|
114
|
-
visible_schemas,
|
|
115
|
-
schema_facts,
|
|
116
|
-
))
|
|
117
|
-
}
|
|
118
|
-
.await;
|
|
119
|
-
let (active_version_id, runtime_functions, functions, visible_schemas, schema_facts) =
|
|
120
|
-
match setup_result {
|
|
121
|
-
Ok(result) => result,
|
|
122
|
-
Err(error) => {
|
|
123
|
-
let _ = storage_transaction.rollback().await;
|
|
124
|
-
return Err(error);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
let mut schema_resolver = TransactionSchemaResolver::new(catalog_context);
|
|
128
|
-
schema_resolver.remember_schema_facts(
|
|
129
|
-
&Domain::schema_catalog(active_version_id.clone(), true),
|
|
130
|
-
schema_facts,
|
|
131
|
-
);
|
|
132
|
-
let staged_writes = Arc::new(TransactionWriteBuffer::new(functions.clone()));
|
|
133
|
-
Ok(OpenTransaction {
|
|
134
|
-
transaction: Self {
|
|
135
|
-
active_version_id,
|
|
136
|
-
live_state,
|
|
137
|
-
tracked_state,
|
|
138
|
-
binary_cas,
|
|
139
|
-
commit_store,
|
|
140
|
-
version_ctx,
|
|
141
|
-
schema_resolver,
|
|
142
|
-
staged_writes,
|
|
143
|
-
storage_transaction,
|
|
144
|
-
visible_schemas,
|
|
145
|
-
functions,
|
|
146
|
-
},
|
|
147
|
-
runtime_functions,
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/// Commits prepared writes, runtime function state, and the backend transaction.
|
|
152
|
-
///
|
|
153
|
-
/// Commit owns the execution boundary: prepared rows become commit-store
|
|
154
|
-
/// facts, version-ref updates, and visible live_state rows before the
|
|
155
|
-
/// backend transaction is committed.
|
|
156
|
-
pub(crate) async fn commit(
|
|
157
|
-
mut self,
|
|
158
|
-
runtime_functions: &FunctionContext,
|
|
159
|
-
) -> Result<TransactionCommitOutcome, LixError> {
|
|
160
|
-
let prepared_writes = match self.staged_writes.drain() {
|
|
161
|
-
Ok(prepared_writes) => prepared_writes,
|
|
162
|
-
Err(error) => {
|
|
163
|
-
let _ = self.storage_transaction.rollback().await;
|
|
164
|
-
return Err(error);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
if let Err(error) = self
|
|
168
|
-
.validate_prepared_writes_by_version(&prepared_writes)
|
|
169
|
-
.await
|
|
170
|
-
{
|
|
171
|
-
let _ = self.storage_transaction.rollback().await;
|
|
172
|
-
return Err(error);
|
|
173
|
-
}
|
|
174
|
-
if let Err(error) = commit::commit_prepared_writes(
|
|
175
|
-
&self.binary_cas,
|
|
176
|
-
&self.commit_store,
|
|
177
|
-
self.version_ctx.as_ref(),
|
|
178
|
-
Some(runtime_functions),
|
|
179
|
-
self.storage_transaction.as_mut(),
|
|
180
|
-
prepared_writes,
|
|
181
|
-
)
|
|
182
|
-
.await
|
|
183
|
-
{
|
|
184
|
-
let _ = self.storage_transaction.rollback().await;
|
|
185
|
-
return Err(error);
|
|
186
|
-
}
|
|
187
|
-
self.storage_transaction.commit().await?;
|
|
188
|
-
Ok(TransactionCommitOutcome::default())
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// Rolls back the backend transaction.
|
|
192
|
-
///
|
|
193
|
-
/// This is the explicit failure path for a write execution. Dropping the
|
|
194
|
-
/// buffered transaction without commit is not the API we want callers to
|
|
195
|
-
/// rely on.
|
|
196
|
-
#[allow(dead_code)]
|
|
197
|
-
pub(crate) async fn rollback(self) -> Result<(), LixError> {
|
|
198
|
-
self.storage_transaction.rollback().await
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/// Stages one decoded write batch into this transaction.
|
|
202
|
-
///
|
|
203
|
-
/// This is the programmatic write entrypoint used by non-SQL APIs. The
|
|
204
|
-
/// transaction still owns preparation from `TransactionWriteRow` into
|
|
205
|
-
/// `PreparedStateRow`, so generated timestamps, change ids, commit ids, and
|
|
206
|
-
/// commit membership stay in one place.
|
|
207
|
-
#[allow(dead_code)]
|
|
208
|
-
pub(crate) async fn stage_write(
|
|
209
|
-
&mut self,
|
|
210
|
-
write: TransactionWrite,
|
|
211
|
-
) -> Result<TransactionWriteOutcome, LixError> {
|
|
212
|
-
require_valid_transaction_write_storage_scopes(&write)?;
|
|
213
|
-
#[cfg(feature = "storage-benches")]
|
|
214
|
-
{
|
|
215
|
-
crate::storage_bench::record_transaction_rows_staged(transaction_write_row_count(
|
|
216
|
-
&write,
|
|
217
|
-
));
|
|
218
|
-
crate::storage_bench::record_transaction_untracked_rows(
|
|
219
|
-
transaction_write_untracked_row_count(&write),
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
self.require_existing_transaction_write_version_ids(&write)
|
|
223
|
-
.await?;
|
|
224
|
-
let write = self.prepare_transaction_write(write).await?;
|
|
225
|
-
self.staged_writes.stage_write(write)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async fn prepare_transaction_write(
|
|
229
|
-
&mut self,
|
|
230
|
-
write: TransactionWrite,
|
|
231
|
-
) -> Result<PreparedTransactionWrite, LixError> {
|
|
232
|
-
Ok(match write {
|
|
233
|
-
TransactionWrite::Rows { mode, rows } => PreparedTransactionWrite::Rows {
|
|
234
|
-
mode,
|
|
235
|
-
rows: self.prepare_transaction_rows(rows).await?,
|
|
236
|
-
},
|
|
237
|
-
TransactionWrite::RowsWithFileData {
|
|
238
|
-
mode,
|
|
239
|
-
rows,
|
|
240
|
-
file_data,
|
|
241
|
-
count,
|
|
242
|
-
} => PreparedTransactionWrite::RowsWithFileData {
|
|
243
|
-
mode,
|
|
244
|
-
rows: self.prepare_transaction_rows(rows).await?,
|
|
245
|
-
file_data,
|
|
246
|
-
count,
|
|
247
|
-
},
|
|
248
|
-
TransactionWrite::AdoptedChanges { changes } => {
|
|
249
|
-
PreparedTransactionWrite::AdoptedChanges {
|
|
250
|
-
rows: self.prepare_adopted_changes(changes).await?,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async fn prepare_transaction_rows(
|
|
257
|
-
&mut self,
|
|
258
|
-
rows: Vec<TransactionWriteRow>,
|
|
259
|
-
) -> Result<Vec<PreparedStateRow>, LixError> {
|
|
260
|
-
let row_count = rows.len();
|
|
261
|
-
let staged = self.staged_writes.staging_overlay()?;
|
|
262
|
-
let live_state = self.live_state.reader(self.storage_transaction.as_mut());
|
|
263
|
-
let mut rows_by_scope = BTreeMap::<Domain, Vec<(usize, TransactionWriteRow)>>::new();
|
|
264
|
-
for (index, row) in rows.into_iter().enumerate() {
|
|
265
|
-
rows_by_scope
|
|
266
|
-
.entry(Domain::schema_catalog(
|
|
267
|
-
row.schema_scope_version_id().to_string(),
|
|
268
|
-
row.untracked,
|
|
269
|
-
))
|
|
270
|
-
.or_default()
|
|
271
|
-
.push((index, row));
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
let mut prepared_rows = Vec::with_capacity(row_count);
|
|
275
|
-
prepared_rows.resize_with(row_count, || None);
|
|
276
|
-
for (domain, rows) in rows_by_scope {
|
|
277
|
-
let functions = self.functions.clone();
|
|
278
|
-
let catalog = self
|
|
279
|
-
.schema_resolver
|
|
280
|
-
.catalog_for_row_normalization(&live_state, &staged, &domain)
|
|
281
|
-
.await?;
|
|
282
|
-
for (_, row) in &rows {
|
|
283
|
-
if row.schema_key != REGISTERED_SCHEMA_KEY {
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if row.file_id.is_some() {
|
|
287
|
-
return Err(LixError::new(
|
|
288
|
-
LixError::CODE_SCHEMA_DEFINITION,
|
|
289
|
-
"lix_registered_schema rows must not be scoped to a file",
|
|
290
|
-
)
|
|
291
|
-
.with_hint("Schema definitions are scoped by version and durability only; write them with null file_id."));
|
|
292
|
-
}
|
|
293
|
-
remember_pending_registered_schema(
|
|
294
|
-
row.snapshot.as_ref().map(TransactionJson::value),
|
|
295
|
-
Domain::schema_catalog(
|
|
296
|
-
row.schema_scope_version_id().to_string(),
|
|
297
|
-
row.untracked,
|
|
298
|
-
),
|
|
299
|
-
catalog,
|
|
300
|
-
)?;
|
|
301
|
-
}
|
|
302
|
-
let normalized_rows = rows
|
|
303
|
-
.into_iter()
|
|
304
|
-
.map(|(index, row)| {
|
|
305
|
-
normalize_transaction_write_row(row, catalog, functions.clone())
|
|
306
|
-
.map(|row| (index, row))
|
|
307
|
-
})
|
|
308
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
309
|
-
for (index, row) in normalized_rows {
|
|
310
|
-
prepared_rows[index] = Some(prepare_state_row(row, &functions)?);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
Ok(prepared_rows
|
|
314
|
-
.into_iter()
|
|
315
|
-
.map(|row| {
|
|
316
|
-
row.expect("every row should be prepared exactly once by schema scope grouping")
|
|
317
|
-
})
|
|
318
|
-
.collect())
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async fn prepare_adopted_changes(
|
|
322
|
-
&mut self,
|
|
323
|
-
changes: Vec<TransactionAdoptedChange>,
|
|
324
|
-
) -> Result<Vec<PreparedAdoptedStateRow>, LixError> {
|
|
325
|
-
let change_count = changes.len();
|
|
326
|
-
let staged = self.staged_writes.staging_overlay()?;
|
|
327
|
-
let live_state = self.live_state.reader(self.storage_transaction.as_mut());
|
|
328
|
-
let mut changes_by_scope =
|
|
329
|
-
BTreeMap::<Domain, Vec<(usize, TransactionAdoptedChange)>>::new();
|
|
330
|
-
for (index, change) in changes.into_iter().enumerate() {
|
|
331
|
-
let schema_scope_version_id = if change.version_id == GLOBAL_VERSION_ID {
|
|
332
|
-
GLOBAL_VERSION_ID
|
|
333
|
-
} else {
|
|
334
|
-
change.version_id.as_str()
|
|
335
|
-
};
|
|
336
|
-
changes_by_scope
|
|
337
|
-
.entry(Domain::schema_catalog(
|
|
338
|
-
schema_scope_version_id.to_string(),
|
|
339
|
-
false,
|
|
340
|
-
))
|
|
341
|
-
.or_default()
|
|
342
|
-
.push((index, change));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
let mut prepared_rows = Vec::with_capacity(change_count);
|
|
346
|
-
prepared_rows.resize_with(change_count, || None);
|
|
347
|
-
for (domain, changes) in changes_by_scope {
|
|
348
|
-
let catalog = self
|
|
349
|
-
.schema_resolver
|
|
350
|
-
.catalog_for_row_normalization(&live_state, &staged, &domain)
|
|
351
|
-
.await?;
|
|
352
|
-
for (_, change) in &changes {
|
|
353
|
-
let row = &change.projected_row;
|
|
354
|
-
if row.schema_key != REGISTERED_SCHEMA_KEY {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if row.file_id.is_some() {
|
|
358
|
-
return Err(LixError::new(
|
|
359
|
-
LixError::CODE_SCHEMA_DEFINITION,
|
|
360
|
-
"lix_registered_schema rows must not be scoped to a file",
|
|
361
|
-
)
|
|
362
|
-
.with_hint("Schema definitions are scoped by version and durability only; write them with null file_id."));
|
|
363
|
-
}
|
|
364
|
-
remember_adopted_registered_schema(
|
|
365
|
-
Domain::schema_catalog(change.version_id.clone(), false),
|
|
366
|
-
row.snapshot_content.as_deref(),
|
|
367
|
-
catalog,
|
|
368
|
-
)?;
|
|
369
|
-
}
|
|
370
|
-
let mut planned_changes = Vec::with_capacity(changes.len());
|
|
371
|
-
for (index, change) in changes {
|
|
372
|
-
let row = &change.projected_row;
|
|
373
|
-
let Some((schema_plan_id, _)) = catalog.plan_for_key(&row.schema_key) else {
|
|
374
|
-
return Err(LixError::new(
|
|
375
|
-
LixError::CODE_SCHEMA_DEFINITION,
|
|
376
|
-
format!(
|
|
377
|
-
"schema '{}' is not visible to this transaction",
|
|
378
|
-
row.schema_key
|
|
379
|
-
),
|
|
380
|
-
));
|
|
381
|
-
};
|
|
382
|
-
if row.schema_key == REGISTERED_SCHEMA_KEY {
|
|
383
|
-
if row.file_id.is_some() {
|
|
384
|
-
return Err(LixError::new(
|
|
385
|
-
LixError::CODE_SCHEMA_DEFINITION,
|
|
386
|
-
"lix_registered_schema rows must not be scoped to a file",
|
|
387
|
-
)
|
|
388
|
-
.with_hint("Schema definitions are scoped by version and durability only; write them with null file_id."));
|
|
389
|
-
}
|
|
390
|
-
remember_adopted_registered_schema(
|
|
391
|
-
Domain::schema_catalog(change.version_id.clone(), false),
|
|
392
|
-
row.snapshot_content.as_deref(),
|
|
393
|
-
catalog,
|
|
394
|
-
)?;
|
|
395
|
-
}
|
|
396
|
-
planned_changes.push((index, change, schema_plan_id));
|
|
397
|
-
}
|
|
398
|
-
for (index, change, schema_plan_id) in planned_changes {
|
|
399
|
-
prepared_rows[index] = Some(prepare_adopted_state_row(change, schema_plan_id)?);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
Ok(prepared_rows
|
|
403
|
-
.into_iter()
|
|
404
|
-
.map(|row| row.expect("every adopted row should be prepared exactly once"))
|
|
405
|
-
.collect())
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async fn validate_prepared_writes_by_version(
|
|
409
|
-
&mut self,
|
|
410
|
-
prepared_writes: &PreparedWriteSet,
|
|
411
|
-
) -> Result<(), LixError> {
|
|
412
|
-
let validation_index = prepared_writes.validation_index();
|
|
413
|
-
for scope in validation_index.schema_scopes() {
|
|
414
|
-
#[cfg(feature = "storage-benches")]
|
|
415
|
-
crate::storage_bench::record_transaction_validation_version();
|
|
416
|
-
let version_prepared_writes = validation_index.validation_set_for_schema_scope(scope);
|
|
417
|
-
let live_state = self.live_state.reader(self.storage_transaction.as_mut());
|
|
418
|
-
let schema_catalog = self
|
|
419
|
-
.schema_resolver
|
|
420
|
-
.catalog_for_validation(&live_state, scope)
|
|
421
|
-
.await?;
|
|
422
|
-
validate_prepared_writes(TransactionValidationInput::new(
|
|
423
|
-
&version_prepared_writes,
|
|
424
|
-
&schema_catalog,
|
|
425
|
-
&live_state,
|
|
426
|
-
))
|
|
427
|
-
.await?;
|
|
428
|
-
}
|
|
429
|
-
Ok(())
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/// Convenience helper for programmatic APIs that only stage state rows.
|
|
433
|
-
#[allow(dead_code)]
|
|
434
|
-
pub(crate) async fn stage_rows(
|
|
435
|
-
&mut self,
|
|
436
|
-
rows: Vec<TransactionWriteRow>,
|
|
437
|
-
) -> Result<TransactionWriteOutcome, LixError> {
|
|
438
|
-
self.stage_write(TransactionWrite::Rows {
|
|
439
|
-
mode: TransactionWriteMode::Replace,
|
|
440
|
-
rows,
|
|
441
|
-
})
|
|
442
|
-
.await
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async fn require_existing_transaction_write_version_ids(
|
|
446
|
-
&mut self,
|
|
447
|
-
write: &TransactionWrite,
|
|
448
|
-
) -> Result<(), LixError> {
|
|
449
|
-
let version_ids = transaction_write_version_ids(write);
|
|
450
|
-
let reader = self
|
|
451
|
-
.version_ctx
|
|
452
|
-
.ref_reader(self.storage_transaction.as_mut());
|
|
453
|
-
for version_id in version_ids {
|
|
454
|
-
if version_id == GLOBAL_VERSION_ID {
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
if reader.load_head_commit_id(&version_id).await?.is_none() {
|
|
458
|
-
return Err(LixError::version_not_found(
|
|
459
|
-
version_id,
|
|
460
|
-
"stage_write",
|
|
461
|
-
"target",
|
|
462
|
-
));
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
Ok(())
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/// Returns the active version resolved inside this write transaction.
|
|
469
|
-
pub(crate) fn active_version_id(&self) -> &str {
|
|
470
|
-
&self.active_version_id
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/// Returns this transaction's prepared runtime functions.
|
|
474
|
-
pub(crate) fn functions(&self) -> FunctionProviderHandle {
|
|
475
|
-
self.functions.clone()
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/// Adds an extra parent to the commit generated for `version_id`.
|
|
479
|
-
///
|
|
480
|
-
/// Merge uses this to preserve source-branch ancestry. Ordinary writes do
|
|
481
|
-
/// not call this because commit finalization already parents to the
|
|
482
|
-
/// version's previous head.
|
|
483
|
-
pub(crate) fn add_commit_parent(
|
|
484
|
-
&self,
|
|
485
|
-
version_id: String,
|
|
486
|
-
parent_commit_id: String,
|
|
487
|
-
) -> Result<(), LixError> {
|
|
488
|
-
self.staged_writes
|
|
489
|
-
.add_commit_parent(version_id, parent_commit_id)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/// Advances a version ref without staging tracked rows.
|
|
493
|
-
///
|
|
494
|
-
/// Fast-forward merges use this path because the commit graph already
|
|
495
|
-
/// contains the source head; the target ref only needs to move to it.
|
|
496
|
-
pub(crate) async fn advance_version_ref(
|
|
497
|
-
&mut self,
|
|
498
|
-
version_id: &str,
|
|
499
|
-
commit_id: &str,
|
|
500
|
-
) -> Result<(), LixError> {
|
|
501
|
-
let timestamp = self.functions.call_timestamp();
|
|
502
|
-
let mut writes = StorageWriteSet::new();
|
|
503
|
-
let canonical_row = prepare_version_ref_row(version_id, commit_id, ×tamp)?;
|
|
504
|
-
self.version_ctx
|
|
505
|
-
.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
|
|
506
|
-
writes
|
|
507
|
-
.apply(&mut self.storage_transaction.as_mut())
|
|
508
|
-
.await
|
|
509
|
-
.map(|_| ())
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/// Returns the commit id currently staged for `version_id`, if tracked rows
|
|
513
|
-
/// have been staged for that version.
|
|
514
|
-
pub(crate) fn staged_commit_id(&self, version_id: &str) -> Result<Option<String>, LixError> {
|
|
515
|
-
self.staged_writes.staged_commit_id(version_id)
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/// Stages a commit for `version_id` even if no tracked rows changed.
|
|
519
|
-
pub(crate) fn stage_empty_commit(&self, version_id: String) -> Result<String, LixError> {
|
|
520
|
-
self.staged_writes.stage_empty_commit(version_id)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/// Creates a version-ref reader scoped to this write transaction.
|
|
524
|
-
pub(crate) fn version_ref_reader(&mut self) -> impl VersionRefReader + '_ {
|
|
525
|
-
self.version_ctx
|
|
526
|
-
.ref_reader(self.storage_transaction.as_mut())
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/// Creates a tracked-state reader scoped to this write transaction.
|
|
530
|
-
pub(crate) fn tracked_state_reader(
|
|
531
|
-
&mut self,
|
|
532
|
-
) -> TrackedStateStoreReader<&mut dyn StorageWriteTransaction> {
|
|
533
|
-
self.tracked_state.reader(self.storage_transaction.as_mut())
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/// Creates a commit-graph reader scoped to this write transaction.
|
|
537
|
-
pub(crate) fn commit_graph_reader(
|
|
538
|
-
&mut self,
|
|
539
|
-
) -> CommitGraphStoreReader<&mut dyn StorageWriteTransaction> {
|
|
540
|
-
CommitGraphContext::new().reader(self.storage_transaction.as_mut())
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
fn prepare_state_row(
|
|
545
|
-
normalized: NormalizedTransactionWriteRow,
|
|
546
|
-
functions: &FunctionProviderHandle,
|
|
547
|
-
) -> Result<PreparedStateRow, LixError> {
|
|
548
|
-
let NormalizedTransactionWriteRow {
|
|
549
|
-
row,
|
|
550
|
-
snapshot,
|
|
551
|
-
schema_plan_id,
|
|
552
|
-
facts,
|
|
553
|
-
} = normalized;
|
|
554
|
-
let updated_at = row.updated_at.unwrap_or_else(|| functions.call_timestamp());
|
|
555
|
-
let snapshot = snapshot
|
|
556
|
-
.map(|value| stage_json_from_value(value, "prepared row snapshot_content"))
|
|
557
|
-
.transpose()?;
|
|
558
|
-
let metadata = row
|
|
559
|
-
.metadata
|
|
560
|
-
.map(|value| stage_json_from_value(value, "prepared row metadata"))
|
|
561
|
-
.transpose()?;
|
|
562
|
-
Ok(PreparedStateRow {
|
|
563
|
-
schema_plan_id,
|
|
564
|
-
facts,
|
|
565
|
-
entity_id: row.entity_id.ok_or_else(|| {
|
|
566
|
-
LixError::new(
|
|
567
|
-
"LIX_ERROR_UNKNOWN",
|
|
568
|
-
"normalized transaction write row is missing entity_id",
|
|
569
|
-
)
|
|
570
|
-
})?,
|
|
571
|
-
schema_key: row.schema_key,
|
|
572
|
-
file_id: row.file_id,
|
|
573
|
-
snapshot,
|
|
574
|
-
metadata,
|
|
575
|
-
origin: row.origin,
|
|
576
|
-
created_at: row.created_at.unwrap_or_else(|| updated_at.clone()),
|
|
577
|
-
updated_at,
|
|
578
|
-
global: row.global,
|
|
579
|
-
change_id: if row.untracked {
|
|
580
|
-
row.change_id
|
|
581
|
-
} else {
|
|
582
|
-
Some(row.change_id.unwrap_or_else(|| functions.call_uuid_v7()))
|
|
583
|
-
},
|
|
584
|
-
commit_id: row.commit_id,
|
|
585
|
-
untracked: row.untracked,
|
|
586
|
-
version_id: row.version_id,
|
|
587
|
-
})
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
fn remember_adopted_registered_schema(
|
|
591
|
-
domain: Domain,
|
|
592
|
-
snapshot_content: Option<&str>,
|
|
593
|
-
catalog: &mut crate::catalog::CatalogSnapshot,
|
|
594
|
-
) -> Result<(), LixError> {
|
|
595
|
-
let snapshot = snapshot_content
|
|
596
|
-
.map(|value| {
|
|
597
|
-
serde_json::from_str::<JsonValue>(value).map_err(|error| {
|
|
598
|
-
LixError::new(
|
|
599
|
-
LixError::CODE_UNKNOWN,
|
|
600
|
-
format!("adopted registered schema snapshot_content is invalid JSON: {error}"),
|
|
601
|
-
)
|
|
602
|
-
})
|
|
603
|
-
})
|
|
604
|
-
.transpose()?;
|
|
605
|
-
remember_pending_registered_schema(snapshot.as_ref(), domain, catalog)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
fn prepare_adopted_state_row(
|
|
609
|
-
change: TransactionAdoptedChange,
|
|
610
|
-
schema_plan_id: crate::catalog::SchemaPlanId,
|
|
611
|
-
) -> Result<PreparedAdoptedStateRow, LixError> {
|
|
612
|
-
if change.change_id != change.projected_row.change_id {
|
|
613
|
-
return Err(LixError::new(
|
|
614
|
-
LixError::CODE_INTERNAL_ERROR,
|
|
615
|
-
format!(
|
|
616
|
-
"adopted change '{}' does not match projected row change_id '{}'",
|
|
617
|
-
change.change_id, change.projected_row.change_id
|
|
618
|
-
),
|
|
619
|
-
));
|
|
620
|
-
}
|
|
621
|
-
let row = change.projected_row;
|
|
622
|
-
let snapshot = row
|
|
623
|
-
.snapshot_content
|
|
624
|
-
.as_deref()
|
|
625
|
-
.map(|value| stage_materialized_json_text(value, "adopted row snapshot_content"))
|
|
626
|
-
.transpose()?;
|
|
627
|
-
let metadata = row
|
|
628
|
-
.metadata
|
|
629
|
-
.as_deref()
|
|
630
|
-
.map(|value| stage_materialized_json_text(value, "adopted row metadata"))
|
|
631
|
-
.transpose()?;
|
|
632
|
-
Ok(PreparedAdoptedStateRow {
|
|
633
|
-
schema_plan_id,
|
|
634
|
-
facts: PreparedRowFacts::default(),
|
|
635
|
-
entity_id: row.entity_id,
|
|
636
|
-
schema_key: row.schema_key,
|
|
637
|
-
file_id: row.file_id,
|
|
638
|
-
snapshot,
|
|
639
|
-
metadata,
|
|
640
|
-
created_at: row.created_at,
|
|
641
|
-
updated_at: row.updated_at,
|
|
642
|
-
global: change.version_id == GLOBAL_VERSION_ID,
|
|
643
|
-
change_id: change.change_id,
|
|
644
|
-
commit_id: String::new(),
|
|
645
|
-
version_id: change.version_id,
|
|
646
|
-
})
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
fn stage_materialized_json_text(
|
|
650
|
-
value: &str,
|
|
651
|
-
context: &str,
|
|
652
|
-
) -> Result<crate::transaction::types::StageJson, LixError> {
|
|
653
|
-
let parsed = serde_json::from_str::<serde_json::Value>(value).map_err(|error| {
|
|
654
|
-
LixError::new(
|
|
655
|
-
LixError::CODE_UNKNOWN,
|
|
656
|
-
format!("{context} is invalid JSON: {error}"),
|
|
657
|
-
)
|
|
658
|
-
})?;
|
|
659
|
-
let prepared = TransactionJson::from_value(parsed, context)?;
|
|
660
|
-
stage_json_from_value(prepared, context)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
pub(crate) struct OpenTransaction {
|
|
664
|
-
pub(crate) transaction: Transaction,
|
|
665
|
-
pub(crate) runtime_functions: FunctionContext,
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
pub(crate) async fn open_transaction(
|
|
669
|
-
mode: &SessionMode,
|
|
670
|
-
storage: StorageContext,
|
|
671
|
-
live_state: Arc<LiveStateContext>,
|
|
672
|
-
tracked_state: Arc<TrackedStateContext>,
|
|
673
|
-
binary_cas: Arc<BinaryCasContext>,
|
|
674
|
-
commit_store: Arc<CommitStoreContext>,
|
|
675
|
-
version_ctx: Arc<VersionContext>,
|
|
676
|
-
catalog_context: Arc<CatalogContext>,
|
|
677
|
-
) -> Result<OpenTransaction, LixError> {
|
|
678
|
-
Transaction::open(
|
|
679
|
-
mode,
|
|
680
|
-
storage,
|
|
681
|
-
live_state,
|
|
682
|
-
tracked_state,
|
|
683
|
-
binary_cas,
|
|
684
|
-
commit_store,
|
|
685
|
-
version_ctx,
|
|
686
|
-
catalog_context,
|
|
687
|
-
)
|
|
688
|
-
.await
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
#[async_trait]
|
|
692
|
-
impl SqlWriteExecutionContext for Transaction {
|
|
693
|
-
fn active_version_id(&self) -> &str {
|
|
694
|
-
&self.active_version_id
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
fn functions(&self) -> FunctionProviderHandle {
|
|
698
|
-
self.functions.clone()
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
|
|
702
|
-
Ok(self.visible_schemas.clone())
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
async fn load_bytes_many(&mut self, hashes: &[BlobHash]) -> Result<BlobBytesBatch, LixError> {
|
|
706
|
-
self.binary_cas
|
|
707
|
-
.reader(self.storage_transaction.as_mut())
|
|
708
|
-
.load_bytes_many(hashes)
|
|
709
|
-
.await
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
async fn scan_live_state(
|
|
713
|
-
&mut self,
|
|
714
|
-
request: &LiveStateScanRequest,
|
|
715
|
-
) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
|
|
716
|
-
let staged = self.staged_writes.staging_overlay()?;
|
|
717
|
-
let base = self.live_state.reader(self.storage_transaction.as_mut());
|
|
718
|
-
overlay_scan_rows(&base, &staged, request).await
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async fn load_version_head(&mut self, version_id: &str) -> Result<Option<String>, LixError> {
|
|
722
|
-
self.version_ctx
|
|
723
|
-
.ref_reader(self.storage_transaction.as_mut())
|
|
724
|
-
.load_head_commit_id(version_id)
|
|
725
|
-
.await
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
async fn stage_write(
|
|
729
|
-
&mut self,
|
|
730
|
-
write: TransactionWrite,
|
|
731
|
-
) -> Result<TransactionWriteOutcome, LixError> {
|
|
732
|
-
Transaction::stage_write(self, write).await
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
fn transaction_write_version_ids(write: &TransactionWrite) -> BTreeSet<String> {
|
|
737
|
-
match write {
|
|
738
|
-
TransactionWrite::Rows { rows, .. } => transaction_write_row_version_ids(rows),
|
|
739
|
-
TransactionWrite::RowsWithFileData {
|
|
740
|
-
rows, file_data, ..
|
|
741
|
-
} => transaction_write_row_version_ids(rows)
|
|
742
|
-
.into_iter()
|
|
743
|
-
.chain(stage_file_data_version_ids(file_data))
|
|
744
|
-
.collect(),
|
|
745
|
-
TransactionWrite::AdoptedChanges { changes } => changes
|
|
746
|
-
.iter()
|
|
747
|
-
.map(|change| change.version_id.clone())
|
|
748
|
-
.collect(),
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
#[cfg(feature = "storage-benches")]
|
|
753
|
-
fn transaction_write_row_count(write: &TransactionWrite) -> usize {
|
|
754
|
-
match write {
|
|
755
|
-
TransactionWrite::Rows { rows, .. } => rows.len(),
|
|
756
|
-
TransactionWrite::RowsWithFileData { rows, .. } => rows.len(),
|
|
757
|
-
TransactionWrite::AdoptedChanges { changes } => changes.len(),
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
#[cfg(feature = "storage-benches")]
|
|
762
|
-
fn transaction_write_untracked_row_count(write: &TransactionWrite) -> usize {
|
|
763
|
-
match write {
|
|
764
|
-
TransactionWrite::Rows { rows, .. } => rows.iter().filter(|row| row.untracked).count(),
|
|
765
|
-
TransactionWrite::RowsWithFileData { rows, .. } => {
|
|
766
|
-
rows.iter().filter(|row| row.untracked).count()
|
|
767
|
-
}
|
|
768
|
-
TransactionWrite::AdoptedChanges { .. } => 0,
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
fn require_valid_transaction_write_storage_scopes(
|
|
773
|
-
write: &TransactionWrite,
|
|
774
|
-
) -> Result<(), LixError> {
|
|
775
|
-
match write {
|
|
776
|
-
TransactionWrite::Rows { rows, .. } => {
|
|
777
|
-
require_valid_transaction_write_row_storage_scopes(rows)
|
|
778
|
-
}
|
|
779
|
-
TransactionWrite::RowsWithFileData { rows, .. } => {
|
|
780
|
-
require_valid_transaction_write_row_storage_scopes(rows)
|
|
781
|
-
}
|
|
782
|
-
TransactionWrite::AdoptedChanges { .. } => Ok(()),
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
fn require_valid_transaction_write_row_storage_scopes(
|
|
787
|
-
rows: &[TransactionWriteRow],
|
|
788
|
-
) -> Result<(), LixError> {
|
|
789
|
-
for row in rows {
|
|
790
|
-
require_valid_storage_scope(row.version_id.as_str(), row.global)?;
|
|
791
|
-
}
|
|
792
|
-
Ok(())
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
fn require_valid_storage_scope(version_id: &str, global: bool) -> Result<(), LixError> {
|
|
796
|
-
if global != (version_id == GLOBAL_VERSION_ID) {
|
|
797
|
-
return Err(LixError::new(
|
|
798
|
-
LixError::CODE_INVALID_STORAGE_SCOPE,
|
|
799
|
-
format!("invalid storage scope: version_id='{version_id}', global={global}"),
|
|
800
|
-
));
|
|
801
|
-
}
|
|
802
|
-
Ok(())
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
fn transaction_write_row_version_ids(rows: &[TransactionWriteRow]) -> BTreeSet<String> {
|
|
806
|
-
rows.iter().map(|row| row.version_id.clone()).collect()
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
fn stage_file_data_version_ids(file_data: &[TransactionFileData]) -> BTreeSet<String> {
|
|
810
|
-
file_data
|
|
811
|
-
.iter()
|
|
812
|
-
.map(|write| write.version_id.clone())
|
|
813
|
-
.collect()
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
async fn resolve_active_version_id(
|
|
817
|
-
mode: &SessionMode,
|
|
818
|
-
live_state: &LiveStateContext,
|
|
819
|
-
version_ctx: &VersionContext,
|
|
820
|
-
transaction: &mut dyn StorageWriteTransaction,
|
|
821
|
-
) -> Result<String, LixError> {
|
|
822
|
-
match mode {
|
|
823
|
-
SessionMode::Pinned { version_id } => Ok(version_id.clone()),
|
|
824
|
-
SessionMode::Workspace => {
|
|
825
|
-
load_workspace_version_id(live_state, version_ctx, transaction).await
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
async fn load_workspace_version_id(
|
|
831
|
-
live_state: &LiveStateContext,
|
|
832
|
-
version_ctx: &VersionContext,
|
|
833
|
-
transaction: &mut dyn StorageWriteTransaction,
|
|
834
|
-
) -> Result<String, LixError> {
|
|
835
|
-
let row = live_state
|
|
836
|
-
.reader(&mut *transaction)
|
|
837
|
-
.load_row(&LiveStateRowRequest {
|
|
838
|
-
schema_key: "lix_key_value".to_string(),
|
|
839
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
840
|
-
entity_id: EntityIdentity::single(WORKSPACE_VERSION_KEY),
|
|
841
|
-
file_id: NullableKeyFilter::Null,
|
|
842
|
-
})
|
|
843
|
-
.await?
|
|
844
|
-
.ok_or_else(|| {
|
|
845
|
-
LixError::new(
|
|
846
|
-
"LIX_ERROR_UNKNOWN",
|
|
847
|
-
"workspace version selector is missing lix_key_value:lix_workspace_version_id",
|
|
848
|
-
)
|
|
849
|
-
})?;
|
|
850
|
-
let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
|
|
851
|
-
LixError::new(
|
|
852
|
-
"LIX_ERROR_UNKNOWN",
|
|
853
|
-
"workspace version selector is missing snapshot_content",
|
|
854
|
-
)
|
|
855
|
-
})?;
|
|
856
|
-
let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
|
|
857
|
-
LixError::new(
|
|
858
|
-
"LIX_ERROR_UNKNOWN",
|
|
859
|
-
format!("workspace version selector snapshot is invalid JSON: {error}"),
|
|
860
|
-
)
|
|
861
|
-
})?;
|
|
862
|
-
let version_id = snapshot
|
|
863
|
-
.get("value")
|
|
864
|
-
.and_then(JsonValue::as_str)
|
|
865
|
-
.filter(|value| !value.is_empty())
|
|
866
|
-
.ok_or_else(|| {
|
|
867
|
-
LixError::new(
|
|
868
|
-
"LIX_ERROR_UNKNOWN",
|
|
869
|
-
"workspace version selector value must be a non-empty string",
|
|
870
|
-
)
|
|
871
|
-
})?
|
|
872
|
-
.to_string();
|
|
873
|
-
|
|
874
|
-
let head = version_ctx
|
|
875
|
-
.ref_reader(&mut *transaction)
|
|
876
|
-
.load_head_commit_id(&version_id)
|
|
877
|
-
.await?;
|
|
878
|
-
if head.is_none() {
|
|
879
|
-
return Err(LixError::version_not_found(
|
|
880
|
-
version_id,
|
|
881
|
-
"load_workspace_version_id",
|
|
882
|
-
"workspace_selector",
|
|
883
|
-
));
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
Ok(version_id)
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
#[cfg(test)]
|
|
890
|
-
mod tests {
|
|
891
|
-
use std::sync::Arc;
|
|
892
|
-
|
|
893
|
-
use serde_json::json;
|
|
894
|
-
|
|
895
|
-
use super::*;
|
|
896
|
-
use crate::backend::testing::UnitTestBackend;
|
|
897
|
-
use crate::commit_store::{ChangeScanRequest, CommitStoreContext};
|
|
898
|
-
use crate::tracked_state::{TrackedStateRowRequest, TrackedStateScanRequest};
|
|
899
|
-
use crate::transaction::types::TransactionJson;
|
|
900
|
-
use crate::untracked_state::{UntrackedStateContext, UntrackedStateRowRequest};
|
|
901
|
-
use crate::version::VersionContext;
|
|
902
|
-
use crate::Backend;
|
|
903
|
-
use crate::NullableKeyFilter;
|
|
904
|
-
use crate::GLOBAL_VERSION_ID;
|
|
905
|
-
|
|
906
|
-
fn live_state_context() -> LiveStateContext {
|
|
907
|
-
LiveStateContext::new(
|
|
908
|
-
crate::tracked_state::TrackedStateContext::new(),
|
|
909
|
-
crate::untracked_state::UntrackedStateContext::new(),
|
|
910
|
-
crate::commit_graph::CommitGraphContext::new(),
|
|
911
|
-
)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const SCHEMA_FIXTURE_COMMIT_ID: &str = "schema-fixture-commit";
|
|
915
|
-
|
|
916
|
-
#[tokio::test]
|
|
917
|
-
async fn stage_rows_routes_tracked_and_untracked_rows_without_sql() {
|
|
918
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
919
|
-
let storage = StorageContext::new(Arc::clone(&backend));
|
|
920
|
-
let live_state = Arc::new(live_state_context());
|
|
921
|
-
seed_visible_schema_rows(storage.clone()).await;
|
|
922
|
-
let binary_cas = Arc::new(BinaryCasContext::new());
|
|
923
|
-
let changelog = Arc::new(CommitStoreContext::new());
|
|
924
|
-
let commit_store = Arc::new(CommitStoreContext::new());
|
|
925
|
-
let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
|
|
926
|
-
let catalog_context = Arc::new(CatalogContext::new());
|
|
927
|
-
let opened = open_transaction(
|
|
928
|
-
&SessionMode::Pinned {
|
|
929
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
930
|
-
},
|
|
931
|
-
storage.clone(),
|
|
932
|
-
Arc::clone(&live_state),
|
|
933
|
-
Arc::new(crate::tracked_state::TrackedStateContext::new()),
|
|
934
|
-
Arc::clone(&binary_cas),
|
|
935
|
-
Arc::clone(&commit_store),
|
|
936
|
-
Arc::clone(&version_ctx),
|
|
937
|
-
Arc::clone(&catalog_context),
|
|
938
|
-
)
|
|
939
|
-
.await
|
|
940
|
-
.expect("transaction should open");
|
|
941
|
-
let mut transaction = opened.transaction;
|
|
942
|
-
let runtime_functions = opened.runtime_functions;
|
|
943
|
-
|
|
944
|
-
transaction
|
|
945
|
-
.stage_rows(vec![
|
|
946
|
-
key_value_stage_row("tracked-programmatic", "tracked", false),
|
|
947
|
-
key_value_stage_row("untracked-programmatic", "untracked", true),
|
|
948
|
-
])
|
|
949
|
-
.await
|
|
950
|
-
.expect("programmatic rows should stage");
|
|
951
|
-
transaction
|
|
952
|
-
.commit(&runtime_functions)
|
|
953
|
-
.await
|
|
954
|
-
.expect("transaction should commit");
|
|
955
|
-
|
|
956
|
-
let changes = changelog
|
|
957
|
-
.reader(storage.clone())
|
|
958
|
-
.scan_changes(&ChangeScanRequest::default())
|
|
959
|
-
.await
|
|
960
|
-
.expect("changelog should scan");
|
|
961
|
-
assert!(
|
|
962
|
-
changes.iter().any(|change| change
|
|
963
|
-
.record
|
|
964
|
-
.entity_id
|
|
965
|
-
.as_single_string_owned()
|
|
966
|
-
.as_deref()
|
|
967
|
-
== Ok("tracked-programmatic")),
|
|
968
|
-
"tracked staged row should be appended to changelog"
|
|
969
|
-
);
|
|
970
|
-
assert!(
|
|
971
|
-
!changes.iter().any(|change| change
|
|
972
|
-
.record
|
|
973
|
-
.entity_id
|
|
974
|
-
.as_single_string_owned()
|
|
975
|
-
.as_deref()
|
|
976
|
-
== Ok("untracked-programmatic")),
|
|
977
|
-
"untracked staged row must not be appended to changelog"
|
|
978
|
-
);
|
|
979
|
-
|
|
980
|
-
let head_commit_id = version_ctx
|
|
981
|
-
.ref_reader(storage.clone())
|
|
982
|
-
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
983
|
-
.await
|
|
984
|
-
.expect("version ref should load")
|
|
985
|
-
.expect("tracked commit should advance the global version ref");
|
|
986
|
-
|
|
987
|
-
let tracked_row = crate::tracked_state::TrackedStateContext::new()
|
|
988
|
-
.reader(storage.clone())
|
|
989
|
-
.load_rows_at_commit(
|
|
990
|
-
&head_commit_id,
|
|
991
|
-
&[TrackedStateRowRequest {
|
|
992
|
-
schema_key: "lix_key_value".to_string(),
|
|
993
|
-
entity_id: crate::entity_identity::EntityIdentity::single(
|
|
994
|
-
"tracked-programmatic",
|
|
995
|
-
),
|
|
996
|
-
file_id: NullableKeyFilter::Null,
|
|
997
|
-
}],
|
|
998
|
-
)
|
|
999
|
-
.await
|
|
1000
|
-
.expect("tracked state should load")
|
|
1001
|
-
.pop()
|
|
1002
|
-
.flatten()
|
|
1003
|
-
.expect("tracked row should be present in tracked state");
|
|
1004
|
-
assert_eq!(tracked_row.commit_id, head_commit_id);
|
|
1005
|
-
assert_eq!(
|
|
1006
|
-
tracked_row.snapshot_content.as_deref(),
|
|
1007
|
-
Some(r#"{"key":"tracked-programmatic","value":"tracked"}"#)
|
|
1008
|
-
);
|
|
1009
|
-
|
|
1010
|
-
let untracked_row = crate::untracked_state::UntrackedStateContext::new()
|
|
1011
|
-
.reader(storage.clone())
|
|
1012
|
-
.load_row(&UntrackedStateRowRequest {
|
|
1013
|
-
schema_key: "lix_key_value".to_string(),
|
|
1014
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1015
|
-
entity_id: crate::entity_identity::EntityIdentity::single("untracked-programmatic"),
|
|
1016
|
-
file_id: NullableKeyFilter::Null,
|
|
1017
|
-
})
|
|
1018
|
-
.await
|
|
1019
|
-
.expect("untracked state should load")
|
|
1020
|
-
.expect("untracked row should be present in untracked state");
|
|
1021
|
-
assert_eq!(
|
|
1022
|
-
untracked_row.snapshot_content.as_deref(),
|
|
1023
|
-
Some(r#"{"key":"untracked-programmatic","value":"untracked"}"#)
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
let live_untracked_row = live_state
|
|
1027
|
-
.reader(storage.clone())
|
|
1028
|
-
.load_row(&crate::live_state::LiveStateRowRequest {
|
|
1029
|
-
schema_key: "lix_key_value".to_string(),
|
|
1030
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1031
|
-
entity_id: crate::entity_identity::EntityIdentity::single("untracked-programmatic"),
|
|
1032
|
-
file_id: NullableKeyFilter::Null,
|
|
1033
|
-
})
|
|
1034
|
-
.await
|
|
1035
|
-
.expect("live state should load")
|
|
1036
|
-
.expect("untracked row should be visible through live state");
|
|
1037
|
-
assert!(live_untracked_row.untracked);
|
|
1038
|
-
assert!(live_untracked_row.global);
|
|
1039
|
-
assert_eq!(live_untracked_row.version_id, GLOBAL_VERSION_ID);
|
|
1040
|
-
|
|
1041
|
-
let tracked_rows = crate::tracked_state::TrackedStateContext::new()
|
|
1042
|
-
.reader(storage.clone())
|
|
1043
|
-
.scan_rows_at_commit(&head_commit_id, &TrackedStateScanRequest::default())
|
|
1044
|
-
.await
|
|
1045
|
-
.expect("tracked state should scan");
|
|
1046
|
-
assert!(
|
|
1047
|
-
tracked_rows
|
|
1048
|
-
.iter()
|
|
1049
|
-
.all(|row| row.entity_id.as_single_string_owned().as_deref()
|
|
1050
|
-
!= Ok("untracked-programmatic")),
|
|
1051
|
-
"untracked staged rows should not be written into tracked state"
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
#[tokio::test]
|
|
1056
|
-
async fn commit_validates_staged_rows_before_persistence() {
|
|
1057
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1058
|
-
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1059
|
-
let live_state = Arc::new(live_state_context());
|
|
1060
|
-
seed_visible_schema_rows(storage.clone()).await;
|
|
1061
|
-
let binary_cas = Arc::new(BinaryCasContext::new());
|
|
1062
|
-
let changelog = Arc::new(CommitStoreContext::new());
|
|
1063
|
-
let commit_store = Arc::new(CommitStoreContext::new());
|
|
1064
|
-
let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
|
|
1065
|
-
let catalog_context = Arc::new(CatalogContext::new());
|
|
1066
|
-
let opened = open_transaction(
|
|
1067
|
-
&SessionMode::Pinned {
|
|
1068
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1069
|
-
},
|
|
1070
|
-
storage.clone(),
|
|
1071
|
-
Arc::clone(&live_state),
|
|
1072
|
-
Arc::new(crate::tracked_state::TrackedStateContext::new()),
|
|
1073
|
-
Arc::clone(&binary_cas),
|
|
1074
|
-
Arc::clone(&commit_store),
|
|
1075
|
-
Arc::clone(&version_ctx),
|
|
1076
|
-
Arc::clone(&catalog_context),
|
|
1077
|
-
)
|
|
1078
|
-
.await
|
|
1079
|
-
.expect("transaction should open");
|
|
1080
|
-
let mut transaction = opened.transaction;
|
|
1081
|
-
let runtime_functions = opened.runtime_functions;
|
|
1082
|
-
|
|
1083
|
-
let mut invalid_row = key_value_stage_row("invalid-programmatic", "invalid", false);
|
|
1084
|
-
invalid_row.snapshot = Some(TransactionJson::from_value_for_test(
|
|
1085
|
-
json!({"key": "invalid-programmatic"}),
|
|
1086
|
-
));
|
|
1087
|
-
transaction
|
|
1088
|
-
.stage_rows(vec![invalid_row])
|
|
1089
|
-
.await
|
|
1090
|
-
.expect("invalid row should still reach commit validation");
|
|
1091
|
-
|
|
1092
|
-
let error = transaction
|
|
1093
|
-
.commit(&runtime_functions)
|
|
1094
|
-
.await
|
|
1095
|
-
.expect_err("validation should reject before persistence");
|
|
1096
|
-
assert!(
|
|
1097
|
-
error.message.contains("snapshot_content validation failed"),
|
|
1098
|
-
"validation error should explain the rejected schema data: {error:?}"
|
|
1099
|
-
);
|
|
1100
|
-
|
|
1101
|
-
let changes = changelog
|
|
1102
|
-
.reader(storage.clone())
|
|
1103
|
-
.scan_changes(&ChangeScanRequest::default())
|
|
1104
|
-
.await
|
|
1105
|
-
.expect("changelog should scan after failed commit");
|
|
1106
|
-
assert!(
|
|
1107
|
-
changes.iter().all(|change| change
|
|
1108
|
-
.record
|
|
1109
|
-
.entity_id
|
|
1110
|
-
.as_single_string_owned()
|
|
1111
|
-
.as_deref()
|
|
1112
|
-
!= Ok("invalid-programmatic")),
|
|
1113
|
-
"validation failure must happen before changelog persistence"
|
|
1114
|
-
);
|
|
1115
|
-
let head = version_ctx
|
|
1116
|
-
.ref_reader(storage.clone())
|
|
1117
|
-
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
1118
|
-
.await
|
|
1119
|
-
.expect("version ref should load after failed commit");
|
|
1120
|
-
assert_eq!(
|
|
1121
|
-
head.as_deref(),
|
|
1122
|
-
Some(SCHEMA_FIXTURE_COMMIT_ID),
|
|
1123
|
-
"validation failure must not advance the version ref"
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
#[tokio::test]
|
|
1128
|
-
async fn commit_rejects_non_object_metadata_without_sql() {
|
|
1129
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1130
|
-
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1131
|
-
let (live_state, _binary_cas, changelog, version_ref, runtime_functions, mut transaction) =
|
|
1132
|
-
open_test_transaction(&backend).await;
|
|
1133
|
-
|
|
1134
|
-
let mut row = key_value_stage_row("invalid-metadata", "value", false);
|
|
1135
|
-
row.metadata = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
|
|
1136
|
-
transaction
|
|
1137
|
-
.stage_rows(vec![row])
|
|
1138
|
-
.await
|
|
1139
|
-
.expect("row should stage before metadata validation");
|
|
1140
|
-
|
|
1141
|
-
let error = transaction
|
|
1142
|
-
.commit(&runtime_functions)
|
|
1143
|
-
.await
|
|
1144
|
-
.expect_err("non-object metadata should fail commit validation");
|
|
1145
|
-
|
|
1146
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
|
|
1147
|
-
assert!(
|
|
1148
|
-
error.message.contains("metadata") && error.message.contains("JSON object"),
|
|
1149
|
-
"error should explain metadata object validation: {error:?}"
|
|
1150
|
-
);
|
|
1151
|
-
assert_no_persistence_after_validation_failure(
|
|
1152
|
-
storage.clone(),
|
|
1153
|
-
&live_state,
|
|
1154
|
-
&changelog,
|
|
1155
|
-
&version_ref,
|
|
1156
|
-
"invalid-metadata",
|
|
1157
|
-
)
|
|
1158
|
-
.await;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
#[tokio::test]
|
|
1162
|
-
async fn stage_rows_rejects_unknown_schema_key_without_sql() {
|
|
1163
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1164
|
-
let (
|
|
1165
|
-
_live_state,
|
|
1166
|
-
_binary_cas,
|
|
1167
|
-
_changelog,
|
|
1168
|
-
_version_ref,
|
|
1169
|
-
_runtime_functions,
|
|
1170
|
-
mut transaction,
|
|
1171
|
-
) = open_test_transaction(&backend).await;
|
|
1172
|
-
|
|
1173
|
-
let mut row = key_value_stage_row("unknown-schema", "value", false);
|
|
1174
|
-
row.schema_key = "missing_schema".to_string();
|
|
1175
|
-
|
|
1176
|
-
let error = transaction
|
|
1177
|
-
.stage_rows(vec![row])
|
|
1178
|
-
.await
|
|
1179
|
-
.expect_err("unknown schema should be rejected while staging");
|
|
1180
|
-
|
|
1181
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
|
|
1182
|
-
assert!(
|
|
1183
|
-
error
|
|
1184
|
-
.message
|
|
1185
|
-
.contains("schema 'missing_schema' is not visible"),
|
|
1186
|
-
"error should explain missing schema visibility: {error:?}"
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
#[tokio::test]
|
|
1191
|
-
async fn stage_rows_rejects_missing_version_without_sql() {
|
|
1192
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1193
|
-
let (
|
|
1194
|
-
_live_state,
|
|
1195
|
-
_binary_cas,
|
|
1196
|
-
_changelog,
|
|
1197
|
-
_version_ref,
|
|
1198
|
-
_runtime_functions,
|
|
1199
|
-
mut transaction,
|
|
1200
|
-
) = open_test_transaction(&backend).await;
|
|
1201
|
-
|
|
1202
|
-
let mut row = key_value_stage_row("ghost-version-row", "value", false);
|
|
1203
|
-
row.version_id = "ghost-version".to_string();
|
|
1204
|
-
row.global = false;
|
|
1205
|
-
|
|
1206
|
-
let error = transaction
|
|
1207
|
-
.stage_rows(vec![row])
|
|
1208
|
-
.await
|
|
1209
|
-
.expect_err("missing version should be rejected before staging");
|
|
1210
|
-
|
|
1211
|
-
assert_eq!(error.code, LixError::CODE_VERSION_NOT_FOUND);
|
|
1212
|
-
assert!(
|
|
1213
|
-
error
|
|
1214
|
-
.message
|
|
1215
|
-
.contains("version 'ghost-version' was not found"),
|
|
1216
|
-
"error should explain missing version: {error:?}"
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
#[tokio::test]
|
|
1221
|
-
async fn stage_rows_rejects_invalid_storage_scope_without_sql() {
|
|
1222
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1223
|
-
let (
|
|
1224
|
-
_live_state,
|
|
1225
|
-
_binary_cas,
|
|
1226
|
-
_changelog,
|
|
1227
|
-
_version_ref,
|
|
1228
|
-
_runtime_functions,
|
|
1229
|
-
mut transaction,
|
|
1230
|
-
) = open_test_transaction(&backend).await;
|
|
1231
|
-
|
|
1232
|
-
let mut row = key_value_stage_row("invalid-storage-scope", "value", false);
|
|
1233
|
-
row.version_id = GLOBAL_VERSION_ID.to_string();
|
|
1234
|
-
row.global = false;
|
|
1235
|
-
|
|
1236
|
-
let error = transaction
|
|
1237
|
-
.stage_rows(vec![row])
|
|
1238
|
-
.await
|
|
1239
|
-
.expect_err("invalid storage scope should be rejected before staging");
|
|
1240
|
-
|
|
1241
|
-
assert_eq!(error.code, LixError::CODE_INVALID_STORAGE_SCOPE);
|
|
1242
|
-
assert!(
|
|
1243
|
-
error.message.contains("version_id='global', global=false"),
|
|
1244
|
-
"error should explain invalid storage scope: {error:?}"
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
#[tokio::test]
|
|
1249
|
-
async fn stage_rows_rejects_invalid_snapshot_json_without_sql() {
|
|
1250
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1251
|
-
let (
|
|
1252
|
-
_live_state,
|
|
1253
|
-
_binary_cas,
|
|
1254
|
-
_changelog,
|
|
1255
|
-
_version_ref,
|
|
1256
|
-
_runtime_functions,
|
|
1257
|
-
mut transaction,
|
|
1258
|
-
) = open_test_transaction(&backend).await;
|
|
1259
|
-
|
|
1260
|
-
let mut row = key_value_stage_row("invalid-json", "value", false);
|
|
1261
|
-
row.snapshot = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
|
|
1262
|
-
|
|
1263
|
-
let error = transaction
|
|
1264
|
-
.stage_rows(vec![row])
|
|
1265
|
-
.await
|
|
1266
|
-
.expect_err("non-object snapshot should be rejected while staging");
|
|
1267
|
-
|
|
1268
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
|
|
1269
|
-
assert!(
|
|
1270
|
-
error.message.contains("must be a JSON object"),
|
|
1271
|
-
"error should explain invalid snapshot shape: {error:?}"
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
#[tokio::test]
|
|
1276
|
-
async fn commit_rejects_snapshot_that_violates_json_schema_without_sql() {
|
|
1277
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1278
|
-
let storage = StorageContext::new(Arc::clone(&backend));
|
|
1279
|
-
let (live_state, _binary_cas, changelog, version_ref, runtime_functions, mut transaction) =
|
|
1280
|
-
open_test_transaction(&backend).await;
|
|
1281
|
-
|
|
1282
|
-
let mut row = key_value_stage_row("schema-mismatch", "value", false);
|
|
1283
|
-
row.snapshot = Some(TransactionJson::from_value_for_test(
|
|
1284
|
-
json!({"key": "schema-mismatch"}),
|
|
1285
|
-
));
|
|
1286
|
-
transaction
|
|
1287
|
-
.stage_rows(vec![row])
|
|
1288
|
-
.await
|
|
1289
|
-
.expect("row should stage before JSON Schema validation");
|
|
1290
|
-
|
|
1291
|
-
let error = transaction
|
|
1292
|
-
.commit(&runtime_functions)
|
|
1293
|
-
.await
|
|
1294
|
-
.expect_err("JSON Schema mismatch should fail commit validation");
|
|
1295
|
-
|
|
1296
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
|
|
1297
|
-
assert!(
|
|
1298
|
-
error.message.contains("snapshot_content validation failed"),
|
|
1299
|
-
"error should explain JSON Schema validation: {error:?}"
|
|
1300
|
-
);
|
|
1301
|
-
assert_no_persistence_after_validation_failure(
|
|
1302
|
-
storage.clone(),
|
|
1303
|
-
&live_state,
|
|
1304
|
-
&changelog,
|
|
1305
|
-
&version_ref,
|
|
1306
|
-
"schema-mismatch",
|
|
1307
|
-
)
|
|
1308
|
-
.await;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
#[tokio::test]
|
|
1312
|
-
async fn stage_rows_rejects_malformed_registered_schema_without_sql() {
|
|
1313
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1314
|
-
let (
|
|
1315
|
-
_live_state,
|
|
1316
|
-
_binary_cas,
|
|
1317
|
-
_changelog,
|
|
1318
|
-
_version_ref,
|
|
1319
|
-
_runtime_functions,
|
|
1320
|
-
mut transaction,
|
|
1321
|
-
) = open_test_transaction(&backend).await;
|
|
1322
|
-
|
|
1323
|
-
let mut row = key_value_stage_row("malformed-registered-schema", "value", false);
|
|
1324
|
-
row.schema_key = "lix_registered_schema".to_string();
|
|
1325
|
-
row.snapshot = Some(TransactionJson::from_value_for_test(json!({
|
|
1326
|
-
"value": {
|
|
1327
|
-
"x-lix-key": "malformed_registered_schema",
|
|
1328
|
-
"x-lix-primary-key": ["id"],
|
|
1329
|
-
"type": "object",
|
|
1330
|
-
"properties": {
|
|
1331
|
-
"id": { "type": "string" }
|
|
1332
|
-
},
|
|
1333
|
-
"required": ["id"],
|
|
1334
|
-
"additionalProperties": false
|
|
1335
|
-
}
|
|
1336
|
-
})));
|
|
1337
|
-
row.entity_id = None;
|
|
1338
|
-
|
|
1339
|
-
let error = transaction
|
|
1340
|
-
.stage_rows(vec![row])
|
|
1341
|
-
.await
|
|
1342
|
-
.expect_err("malformed registered schema should be rejected while staging");
|
|
1343
|
-
|
|
1344
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
|
|
1345
|
-
assert!(
|
|
1346
|
-
error.message.contains("x-lix-primary-key"),
|
|
1347
|
-
"error should explain malformed registered schema: {error:?}"
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
#[tokio::test]
|
|
1352
|
-
async fn stage_rows_rejects_primary_key_entity_id_mismatch_without_sql() {
|
|
1353
|
-
let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
|
|
1354
|
-
let (
|
|
1355
|
-
_live_state,
|
|
1356
|
-
_binary_cas,
|
|
1357
|
-
_changelog,
|
|
1358
|
-
_version_ref,
|
|
1359
|
-
_runtime_functions,
|
|
1360
|
-
mut transaction,
|
|
1361
|
-
) = open_test_transaction(&backend).await;
|
|
1362
|
-
|
|
1363
|
-
let mut row = key_value_stage_row("right-id", "value", false);
|
|
1364
|
-
row.entity_id = Some(crate::entity_identity::EntityIdentity::single("wrong-id"));
|
|
1365
|
-
|
|
1366
|
-
let error = transaction
|
|
1367
|
-
.stage_rows(vec![row])
|
|
1368
|
-
.await
|
|
1369
|
-
.expect_err("entity id mismatch should be rejected while staging");
|
|
1370
|
-
|
|
1371
|
-
assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
|
|
1372
|
-
assert!(
|
|
1373
|
-
error
|
|
1374
|
-
.message
|
|
1375
|
-
.contains("does not match x-lix-primary-key derived entity_id"),
|
|
1376
|
-
"error should explain entity id mismatch: {error:?}"
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
async fn open_test_transaction(
|
|
1381
|
-
backend: &Arc<dyn Backend + Send + Sync>,
|
|
1382
|
-
) -> (
|
|
1383
|
-
Arc<LiveStateContext>,
|
|
1384
|
-
Arc<BinaryCasContext>,
|
|
1385
|
-
Arc<CommitStoreContext>,
|
|
1386
|
-
Arc<VersionContext>,
|
|
1387
|
-
FunctionContext,
|
|
1388
|
-
Transaction,
|
|
1389
|
-
) {
|
|
1390
|
-
let storage = StorageContext::new(Arc::clone(backend));
|
|
1391
|
-
let live_state = Arc::new(live_state_context());
|
|
1392
|
-
seed_visible_schema_rows(storage.clone()).await;
|
|
1393
|
-
let binary_cas = Arc::new(BinaryCasContext::new());
|
|
1394
|
-
let changelog = Arc::new(CommitStoreContext::new());
|
|
1395
|
-
let commit_store = Arc::new(CommitStoreContext::new());
|
|
1396
|
-
let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
|
|
1397
|
-
let catalog_context = Arc::new(CatalogContext::new());
|
|
1398
|
-
let opened = open_transaction(
|
|
1399
|
-
&SessionMode::Pinned {
|
|
1400
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1401
|
-
},
|
|
1402
|
-
storage,
|
|
1403
|
-
Arc::clone(&live_state),
|
|
1404
|
-
Arc::new(crate::tracked_state::TrackedStateContext::new()),
|
|
1405
|
-
Arc::clone(&binary_cas),
|
|
1406
|
-
Arc::clone(&commit_store),
|
|
1407
|
-
Arc::clone(&version_ctx),
|
|
1408
|
-
catalog_context,
|
|
1409
|
-
)
|
|
1410
|
-
.await
|
|
1411
|
-
.expect("transaction should open");
|
|
1412
|
-
let transaction = opened.transaction;
|
|
1413
|
-
let runtime_functions = opened.runtime_functions;
|
|
1414
|
-
|
|
1415
|
-
(
|
|
1416
|
-
live_state,
|
|
1417
|
-
binary_cas,
|
|
1418
|
-
changelog,
|
|
1419
|
-
version_ctx,
|
|
1420
|
-
runtime_functions,
|
|
1421
|
-
transaction,
|
|
1422
|
-
)
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
async fn seed_visible_schema_rows(storage: StorageContext) {
|
|
1426
|
-
let mut writes = StorageWriteSet::new();
|
|
1427
|
-
let rows = crate::schema::seed_schema_definitions()
|
|
1428
|
-
.into_iter()
|
|
1429
|
-
.map(|schema| {
|
|
1430
|
-
let key = crate::schema::schema_key_from_definition(schema)
|
|
1431
|
-
.expect("seed schema key should derive");
|
|
1432
|
-
let snapshot_content = json!({ "value": schema }).to_string();
|
|
1433
|
-
crate::tracked_state::MaterializedTrackedStateRow {
|
|
1434
|
-
entity_id: crate::schema::registered_schema_entity_id(&key.schema_key)
|
|
1435
|
-
.expect("registered schema identity should derive"),
|
|
1436
|
-
schema_key: "lix_registered_schema".to_string(),
|
|
1437
|
-
file_id: None,
|
|
1438
|
-
snapshot_content: Some(snapshot_content),
|
|
1439
|
-
metadata: None,
|
|
1440
|
-
deleted: false,
|
|
1441
|
-
created_at: "1970-01-01T00:00:00.000Z".to_string(),
|
|
1442
|
-
updated_at: "1970-01-01T00:00:00.000Z".to_string(),
|
|
1443
|
-
change_id: format!("schema-fixture-{}", key.schema_key),
|
|
1444
|
-
commit_id: SCHEMA_FIXTURE_COMMIT_ID.to_string(),
|
|
1445
|
-
}
|
|
1446
|
-
})
|
|
1447
|
-
.collect::<Vec<_>>();
|
|
1448
|
-
let version_ref_row = prepare_version_ref_row(
|
|
1449
|
-
GLOBAL_VERSION_ID,
|
|
1450
|
-
SCHEMA_FIXTURE_COMMIT_ID,
|
|
1451
|
-
"1970-01-01T00:00:00.000Z",
|
|
1452
|
-
)
|
|
1453
|
-
.expect("schema fixture version ref should stage");
|
|
1454
|
-
let mut storage_transaction = storage
|
|
1455
|
-
.begin_write_transaction()
|
|
1456
|
-
.await
|
|
1457
|
-
.expect("schema fixture transaction should open");
|
|
1458
|
-
crate::test_support::stage_tracked_root_from_materialized(
|
|
1459
|
-
storage_transaction.as_mut(),
|
|
1460
|
-
&crate::tracked_state::TrackedStateContext::new(),
|
|
1461
|
-
SCHEMA_FIXTURE_COMMIT_ID,
|
|
1462
|
-
None,
|
|
1463
|
-
&rows,
|
|
1464
|
-
)
|
|
1465
|
-
.await
|
|
1466
|
-
.expect("schema fixture rows should stage");
|
|
1467
|
-
crate::untracked_state::UntrackedStateContext::new()
|
|
1468
|
-
.writer(&mut writes)
|
|
1469
|
-
.stage_rows([version_ref_row.row.as_ref()])
|
|
1470
|
-
.expect("schema fixture version ref should stage");
|
|
1471
|
-
writes
|
|
1472
|
-
.apply(&mut storage_transaction.as_mut())
|
|
1473
|
-
.await
|
|
1474
|
-
.expect("schema fixture rows should apply");
|
|
1475
|
-
storage_transaction
|
|
1476
|
-
.commit()
|
|
1477
|
-
.await
|
|
1478
|
-
.expect("schema fixture transaction should commit");
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
async fn assert_no_persistence_after_validation_failure(
|
|
1482
|
-
storage: StorageContext,
|
|
1483
|
-
live_state: &LiveStateContext,
|
|
1484
|
-
changelog: &CommitStoreContext,
|
|
1485
|
-
version_ctx: &VersionContext,
|
|
1486
|
-
rejected_entity_id: &str,
|
|
1487
|
-
) {
|
|
1488
|
-
let changes = changelog
|
|
1489
|
-
.reader(storage.clone())
|
|
1490
|
-
.scan_changes(&ChangeScanRequest::default())
|
|
1491
|
-
.await
|
|
1492
|
-
.expect("changelog should scan after failed commit");
|
|
1493
|
-
assert!(
|
|
1494
|
-
changes.iter().all(|change| change
|
|
1495
|
-
.record
|
|
1496
|
-
.entity_id
|
|
1497
|
-
.as_single_string_owned()
|
|
1498
|
-
.as_deref()
|
|
1499
|
-
!= Ok(rejected_entity_id)),
|
|
1500
|
-
"validation failure must happen before changelog persistence"
|
|
1501
|
-
);
|
|
1502
|
-
let head = version_ctx
|
|
1503
|
-
.ref_reader(storage.clone())
|
|
1504
|
-
.load_head_commit_id(GLOBAL_VERSION_ID)
|
|
1505
|
-
.await
|
|
1506
|
-
.expect("version ref should load after failed commit");
|
|
1507
|
-
assert_eq!(
|
|
1508
|
-
head.as_deref(),
|
|
1509
|
-
Some(SCHEMA_FIXTURE_COMMIT_ID),
|
|
1510
|
-
"validation failure must not advance the version ref"
|
|
1511
|
-
);
|
|
1512
|
-
let row = live_state
|
|
1513
|
-
.reader(storage)
|
|
1514
|
-
.load_row(&crate::live_state::LiveStateRowRequest {
|
|
1515
|
-
schema_key: "lix_key_value".to_string(),
|
|
1516
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1517
|
-
entity_id: crate::entity_identity::EntityIdentity::single(rejected_entity_id),
|
|
1518
|
-
file_id: NullableKeyFilter::Null,
|
|
1519
|
-
})
|
|
1520
|
-
.await
|
|
1521
|
-
.expect("live state should load after failed commit");
|
|
1522
|
-
assert_eq!(
|
|
1523
|
-
row, None,
|
|
1524
|
-
"validation failure must happen before live-state persistence"
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
fn key_value_stage_row(key: &str, value: &str, untracked: bool) -> TransactionWriteRow {
|
|
1529
|
-
TransactionWriteRow {
|
|
1530
|
-
entity_id: Some(crate::entity_identity::EntityIdentity::single(key)),
|
|
1531
|
-
schema_key: "lix_key_value".to_string(),
|
|
1532
|
-
file_id: None,
|
|
1533
|
-
snapshot: Some(TransactionJson::from_value_for_test(json!({
|
|
1534
|
-
"key": key,
|
|
1535
|
-
"value": value,
|
|
1536
|
-
}))),
|
|
1537
|
-
metadata: None,
|
|
1538
|
-
origin: None,
|
|
1539
|
-
created_at: None,
|
|
1540
|
-
updated_at: None,
|
|
1541
|
-
global: true,
|
|
1542
|
-
change_id: None,
|
|
1543
|
-
commit_id: None,
|
|
1544
|
-
untracked,
|
|
1545
|
-
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|