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