@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,1731 +0,0 @@
|
|
|
1
|
-
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
2
|
-
use std::sync::{Arc, Mutex};
|
|
3
|
-
|
|
4
|
-
use crate::catalog::SchemaPlanId;
|
|
5
|
-
use crate::domain::{Domain, DomainRowIdentity};
|
|
6
|
-
use crate::entity_identity::EntityIdentity;
|
|
7
|
-
use crate::functions::{FunctionProvider, FunctionProviderHandle};
|
|
8
|
-
#[cfg(test)]
|
|
9
|
-
use crate::live_state::LiveStateRowRequest;
|
|
10
|
-
use crate::live_state::{LiveStateScanRequest, MaterializedLiveStateRow};
|
|
11
|
-
#[cfg(test)]
|
|
12
|
-
use crate::transaction::types::{stage_json_from_value, TransactionJson};
|
|
13
|
-
use crate::transaction::types::{
|
|
14
|
-
LogicalPrimaryKey, PreparedTransactionWrite, TransactionFileData, TransactionWriteMode,
|
|
15
|
-
TransactionWriteOperation, TransactionWriteOrigin, TransactionWriteOutcome,
|
|
16
|
-
};
|
|
17
|
-
use crate::transaction::types::{PreparedAdoptedStateRow, PreparedStateRow, StagedCommitMembers};
|
|
18
|
-
use crate::GLOBAL_VERSION_ID;
|
|
19
|
-
use crate::{LixError, NullableKeyFilter};
|
|
20
|
-
|
|
21
|
-
/// Transaction-local write buffer after transaction-boundary preparation.
|
|
22
|
-
///
|
|
23
|
-
/// This is the engine seam between SQL execution and transaction ownership:
|
|
24
|
-
/// write frontends pass decoded `TransactionWriteRow`s to `Transaction`, the
|
|
25
|
-
/// transaction prepares them into stable `PreparedStateRow`s, reads build a
|
|
26
|
-
/// `PreparedStateRowOverlay` from those rows, and commit drains the same rows.
|
|
27
|
-
pub(crate) struct TransactionWriteBuffer {
|
|
28
|
-
functions: FunctionProviderHandle,
|
|
29
|
-
rows: Mutex<Vec<Option<PreparedStateRow>>>,
|
|
30
|
-
adopted_rows: Mutex<Vec<Option<PreparedAdoptedStateRow>>>,
|
|
31
|
-
by_identity: Mutex<HashMap<PreparedStateRowIdentity, RowSlot>>,
|
|
32
|
-
insert_identities: Mutex<BTreeMap<PreparedStateRowIdentity, Option<TransactionWriteOrigin>>>,
|
|
33
|
-
commit_members_by_version: Mutex<BTreeMap<String, StagedCommitMembers>>,
|
|
34
|
-
extra_commit_parents_by_version: Mutex<BTreeMap<String, Vec<String>>>,
|
|
35
|
-
file_data_writes: Mutex<Vec<TransactionFileData>>,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
39
|
-
pub(crate) enum RowSlot {
|
|
40
|
-
State(usize),
|
|
41
|
-
Adopted(usize),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/// Drained prepared transaction writes ready for commit.
|
|
45
|
-
pub(crate) struct PreparedWriteSet {
|
|
46
|
-
pub(crate) state_rows: Vec<PreparedStateRow>,
|
|
47
|
-
pub(crate) adopted_rows: Vec<PreparedAdoptedStateRow>,
|
|
48
|
-
pub(crate) insert_identities:
|
|
49
|
-
BTreeMap<PreparedStateRowIdentity, Option<TransactionWriteOrigin>>,
|
|
50
|
-
pub(crate) commit_members_by_version: BTreeMap<String, StagedCommitMembers>,
|
|
51
|
-
pub(crate) extra_commit_parents_by_version: BTreeMap<String, Vec<String>>,
|
|
52
|
-
pub(crate) file_data_writes: Vec<TransactionFileData>,
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
pub(crate) struct PreparedWriteValidationSet<'a> {
|
|
56
|
-
rows: Vec<PreparedValidationRow<'a>>,
|
|
57
|
-
constraint_rows: Vec<PreparedValidationRow<'a>>,
|
|
58
|
-
insert_identities: Vec<(
|
|
59
|
-
&'a PreparedStateRowIdentity,
|
|
60
|
-
Option<&'a TransactionWriteOrigin>,
|
|
61
|
-
)>,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
pub(crate) struct PreparedWriteValidationIndex<'a> {
|
|
65
|
-
rows_by_schema_scope: BTreeMap<Domain, Vec<PreparedValidationRow<'a>>>,
|
|
66
|
-
insert_identities_by_schema_scope: BTreeMap<
|
|
67
|
-
Domain,
|
|
68
|
-
Vec<(
|
|
69
|
-
&'a PreparedStateRowIdentity,
|
|
70
|
-
Option<&'a TransactionWriteOrigin>,
|
|
71
|
-
)>,
|
|
72
|
-
>,
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#[derive(Clone, Copy)]
|
|
76
|
-
pub(crate) enum PreparedValidationRow<'a> {
|
|
77
|
-
State(&'a PreparedStateRow),
|
|
78
|
-
Adopted(&'a PreparedAdoptedStateRow),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
impl<'a> PreparedValidationRow<'a> {
|
|
82
|
-
pub(crate) fn entity_id(&self) -> &EntityIdentity {
|
|
83
|
-
match self {
|
|
84
|
-
Self::State(row) => &row.entity_id,
|
|
85
|
-
Self::Adopted(row) => &row.entity_id,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
pub(crate) fn schema_plan_id(&self) -> SchemaPlanId {
|
|
90
|
-
match self {
|
|
91
|
-
Self::State(row) => row.schema_plan_id,
|
|
92
|
-
Self::Adopted(row) => row.schema_plan_id,
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
pub(crate) fn schema_key(&self) -> &str {
|
|
97
|
-
match self {
|
|
98
|
-
Self::State(row) => &row.schema_key,
|
|
99
|
-
Self::Adopted(row) => &row.schema_key,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
pub(crate) fn file_id(&self) -> &Option<String> {
|
|
104
|
-
match self {
|
|
105
|
-
Self::State(row) => &row.file_id,
|
|
106
|
-
Self::Adopted(row) => &row.file_id,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#[cfg(test)]
|
|
111
|
-
pub(crate) fn snapshot_content(&self) -> Option<&str> {
|
|
112
|
-
match self {
|
|
113
|
-
Self::State(row) => row
|
|
114
|
-
.snapshot
|
|
115
|
-
.as_ref()
|
|
116
|
-
.map(|snapshot| snapshot.normalized.as_ref()),
|
|
117
|
-
Self::Adopted(row) => row
|
|
118
|
-
.snapshot
|
|
119
|
-
.as_ref()
|
|
120
|
-
.map(|snapshot| snapshot.normalized.as_ref()),
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
pub(crate) fn snapshot_json(self) -> Option<&'a serde_json::Value> {
|
|
125
|
-
match self {
|
|
126
|
-
Self::State(row) => row
|
|
127
|
-
.snapshot
|
|
128
|
-
.as_ref()
|
|
129
|
-
.map(|snapshot| snapshot.value.as_ref()),
|
|
130
|
-
Self::Adopted(row) => row
|
|
131
|
-
.snapshot
|
|
132
|
-
.as_ref()
|
|
133
|
-
.map(|snapshot| snapshot.value.as_ref()),
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
pub(crate) fn metadata_json(self) -> Option<&'a serde_json::Value> {
|
|
138
|
-
match self {
|
|
139
|
-
Self::State(row) => row
|
|
140
|
-
.metadata
|
|
141
|
-
.as_ref()
|
|
142
|
-
.map(|metadata| metadata.value.as_ref()),
|
|
143
|
-
Self::Adopted(row) => row
|
|
144
|
-
.metadata
|
|
145
|
-
.as_ref()
|
|
146
|
-
.map(|metadata| metadata.value.as_ref()),
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
pub(crate) fn untracked(&self) -> bool {
|
|
151
|
-
match self {
|
|
152
|
-
Self::State(row) => row.untracked,
|
|
153
|
-
Self::Adopted(_) => false,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
pub(crate) fn version_id(&self) -> &str {
|
|
158
|
-
match self {
|
|
159
|
-
Self::State(row) => &row.version_id,
|
|
160
|
-
Self::Adopted(row) => &row.version_id,
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
pub(crate) fn domain(&self) -> Domain {
|
|
165
|
-
Domain::exact_file(
|
|
166
|
-
self.version_id().to_string(),
|
|
167
|
-
self.untracked(),
|
|
168
|
-
self.file_id().clone(),
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
pub(crate) fn domain_row_identity(&self) -> DomainRowIdentity {
|
|
173
|
-
DomainRowIdentity::in_domain(
|
|
174
|
-
self.domain(),
|
|
175
|
-
self.schema_key().to_string(),
|
|
176
|
-
self.entity_id().clone(),
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
impl<'a> PreparedWriteValidationIndex<'a> {
|
|
182
|
-
pub(crate) fn schema_scopes(&self) -> impl Iterator<Item = &Domain> {
|
|
183
|
-
self.rows_by_schema_scope.keys()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
pub(crate) fn validation_set_for_schema_scope(
|
|
187
|
-
&self,
|
|
188
|
-
schema_scope: &Domain,
|
|
189
|
-
) -> PreparedWriteValidationSet<'a> {
|
|
190
|
-
let constraint_rows = self
|
|
191
|
-
.rows_by_schema_scope
|
|
192
|
-
.iter()
|
|
193
|
-
.flat_map(|(target_scope, rows)| {
|
|
194
|
-
rows.iter().copied().filter(move |row| {
|
|
195
|
-
schema_scope.validation_scope_contains_constraint_domain(target_scope)
|
|
196
|
-
|| (row.snapshot_json().is_none()
|
|
197
|
-
&& target_scope.tombstone_domain_affects_validation_scope(schema_scope))
|
|
198
|
-
})
|
|
199
|
-
})
|
|
200
|
-
.collect();
|
|
201
|
-
PreparedWriteValidationSet {
|
|
202
|
-
rows: self
|
|
203
|
-
.rows_by_schema_scope
|
|
204
|
-
.get(schema_scope)
|
|
205
|
-
.cloned()
|
|
206
|
-
.unwrap_or_default(),
|
|
207
|
-
constraint_rows,
|
|
208
|
-
insert_identities: self
|
|
209
|
-
.insert_identities_by_schema_scope
|
|
210
|
-
.get(schema_scope)
|
|
211
|
-
.cloned()
|
|
212
|
-
.unwrap_or_default(),
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
impl<'a> PreparedWriteValidationSet<'a> {
|
|
218
|
-
pub(crate) fn rows(&self) -> impl Iterator<Item = PreparedValidationRow<'a>> + '_ {
|
|
219
|
-
self.rows.iter().copied()
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
pub(crate) fn constraint_rows(&self) -> impl Iterator<Item = PreparedValidationRow<'a>> + '_ {
|
|
223
|
-
self.constraint_rows.iter().copied()
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
pub(crate) fn insert_identities(
|
|
227
|
-
&self,
|
|
228
|
-
) -> impl Iterator<Item = (&PreparedStateRowIdentity, Option<&TransactionWriteOrigin>)> {
|
|
229
|
-
self.insert_identities
|
|
230
|
-
.iter()
|
|
231
|
-
.map(|(identity, origin)| (*identity, *origin))
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
impl PreparedWriteSet {
|
|
236
|
-
#[cfg(test)]
|
|
237
|
-
pub(crate) fn validation_rows(&self) -> impl Iterator<Item = PreparedValidationRow<'_>> + '_ {
|
|
238
|
-
self.state_rows
|
|
239
|
-
.iter()
|
|
240
|
-
.map(PreparedValidationRow::State)
|
|
241
|
-
.chain(self.adopted_rows.iter().map(PreparedValidationRow::Adopted))
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
pub(crate) fn validation_index(&self) -> PreparedWriteValidationIndex<'_> {
|
|
245
|
-
let mut rows_by_schema_scope = BTreeMap::<Domain, Vec<PreparedValidationRow<'_>>>::new();
|
|
246
|
-
for row in &self.state_rows {
|
|
247
|
-
let row = PreparedValidationRow::State(row);
|
|
248
|
-
rows_by_schema_scope
|
|
249
|
-
.entry(row.domain().schema_catalog_domain())
|
|
250
|
-
.or_default()
|
|
251
|
-
.push(row);
|
|
252
|
-
}
|
|
253
|
-
for row in &self.adopted_rows {
|
|
254
|
-
let row = PreparedValidationRow::Adopted(row);
|
|
255
|
-
rows_by_schema_scope
|
|
256
|
-
.entry(row.domain().schema_catalog_domain())
|
|
257
|
-
.or_default()
|
|
258
|
-
.push(row);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let mut insert_identities_by_schema_scope = BTreeMap::<
|
|
262
|
-
Domain,
|
|
263
|
-
Vec<(&PreparedStateRowIdentity, Option<&TransactionWriteOrigin>)>,
|
|
264
|
-
>::new();
|
|
265
|
-
for (identity, origin) in &self.insert_identities {
|
|
266
|
-
insert_identities_by_schema_scope
|
|
267
|
-
.entry(identity.domain().schema_catalog_domain())
|
|
268
|
-
.or_default()
|
|
269
|
-
.push((identity, origin.as_ref()));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
PreparedWriteValidationIndex {
|
|
273
|
-
rows_by_schema_scope,
|
|
274
|
-
insert_identities_by_schema_scope,
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
#[cfg(test)]
|
|
279
|
-
pub(crate) fn validation_set_for_tests(&self) -> PreparedWriteValidationSet<'_> {
|
|
280
|
-
let rows: Vec<_> = self.validation_rows().collect();
|
|
281
|
-
let insert_identities = self
|
|
282
|
-
.insert_identities
|
|
283
|
-
.iter()
|
|
284
|
-
.map(|(identity, origin)| (identity, origin.as_ref()))
|
|
285
|
-
.collect();
|
|
286
|
-
PreparedWriteValidationSet {
|
|
287
|
-
constraint_rows: rows.clone(),
|
|
288
|
-
rows,
|
|
289
|
-
insert_identities,
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
impl TransactionWriteBuffer {
|
|
295
|
-
pub(crate) fn new(functions: FunctionProviderHandle) -> Self {
|
|
296
|
-
Self {
|
|
297
|
-
functions,
|
|
298
|
-
rows: Mutex::new(Vec::new()),
|
|
299
|
-
adopted_rows: Mutex::new(Vec::new()),
|
|
300
|
-
by_identity: Mutex::new(HashMap::new()),
|
|
301
|
-
insert_identities: Mutex::new(BTreeMap::new()),
|
|
302
|
-
commit_members_by_version: Mutex::new(BTreeMap::new()),
|
|
303
|
-
extra_commit_parents_by_version: Mutex::new(BTreeMap::new()),
|
|
304
|
-
file_data_writes: Mutex::new(Vec::new()),
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/// Drains staged writes for commit.
|
|
309
|
-
pub(crate) fn drain(&self) -> Result<PreparedWriteSet, LixError> {
|
|
310
|
-
let mut rows_guard = self.rows.lock().map_err(|_| {
|
|
311
|
-
LixError::new(
|
|
312
|
-
"LIX_ERROR_UNKNOWN",
|
|
313
|
-
"failed to acquire transaction staged writes lock",
|
|
314
|
-
)
|
|
315
|
-
})?;
|
|
316
|
-
let mut adopted_rows_guard = self.adopted_rows.lock().map_err(|_| {
|
|
317
|
-
LixError::new(
|
|
318
|
-
"LIX_ERROR_UNKNOWN",
|
|
319
|
-
"failed to acquire transaction staged adopted writes lock",
|
|
320
|
-
)
|
|
321
|
-
})?;
|
|
322
|
-
let mut by_identity_guard = self.by_identity.lock().map_err(|_| {
|
|
323
|
-
LixError::new(
|
|
324
|
-
"LIX_ERROR_UNKNOWN",
|
|
325
|
-
"failed to acquire transaction staged identity index lock",
|
|
326
|
-
)
|
|
327
|
-
})?;
|
|
328
|
-
let mut file_data_guard = self.file_data_writes.lock().map_err(|_| {
|
|
329
|
-
LixError::new(
|
|
330
|
-
"LIX_ERROR_UNKNOWN",
|
|
331
|
-
"failed to acquire transaction staged file data lock",
|
|
332
|
-
)
|
|
333
|
-
})?;
|
|
334
|
-
let mut insert_identities_guard = self.insert_identities.lock().map_err(|_| {
|
|
335
|
-
LixError::new(
|
|
336
|
-
"LIX_ERROR_UNKNOWN",
|
|
337
|
-
"failed to acquire transaction staged insert identity lock",
|
|
338
|
-
)
|
|
339
|
-
})?;
|
|
340
|
-
let mut commit_members_guard = self.commit_members_by_version.lock().map_err(|_| {
|
|
341
|
-
LixError::new(
|
|
342
|
-
"LIX_ERROR_UNKNOWN",
|
|
343
|
-
"failed to acquire transaction staged commit membership lock",
|
|
344
|
-
)
|
|
345
|
-
})?;
|
|
346
|
-
let mut extra_parents_guard =
|
|
347
|
-
self.extra_commit_parents_by_version.lock().map_err(|_| {
|
|
348
|
-
LixError::new(
|
|
349
|
-
"LIX_ERROR_UNKNOWN",
|
|
350
|
-
"failed to acquire transaction staged extra commit parents lock",
|
|
351
|
-
)
|
|
352
|
-
})?;
|
|
353
|
-
let result = Ok(PreparedWriteSet {
|
|
354
|
-
state_rows: std::mem::take(&mut *rows_guard)
|
|
355
|
-
.into_iter()
|
|
356
|
-
.flatten()
|
|
357
|
-
.collect(),
|
|
358
|
-
adopted_rows: std::mem::take(&mut *adopted_rows_guard)
|
|
359
|
-
.into_iter()
|
|
360
|
-
.flatten()
|
|
361
|
-
.collect(),
|
|
362
|
-
insert_identities: std::mem::take(&mut *insert_identities_guard),
|
|
363
|
-
commit_members_by_version: std::mem::take(&mut *commit_members_guard),
|
|
364
|
-
extra_commit_parents_by_version: std::mem::take(&mut *extra_parents_guard),
|
|
365
|
-
file_data_writes: std::mem::take(&mut *file_data_guard),
|
|
366
|
-
});
|
|
367
|
-
by_identity_guard.clear();
|
|
368
|
-
result
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/// Records an additional parent for the commit generated for `version_id`.
|
|
372
|
-
///
|
|
373
|
-
/// Normal writes parent the new commit to the version's previous head.
|
|
374
|
-
/// Merges add the source version head as an extra parent so the commit graph
|
|
375
|
-
/// preserves branch ancestry while tracked-state roots still apply source
|
|
376
|
-
/// rows onto the target root.
|
|
377
|
-
pub(crate) fn add_commit_parent(
|
|
378
|
-
&self,
|
|
379
|
-
version_id: String,
|
|
380
|
-
parent_commit_id: String,
|
|
381
|
-
) -> Result<(), LixError> {
|
|
382
|
-
let mut guard = self.extra_commit_parents_by_version.lock().map_err(|_| {
|
|
383
|
-
LixError::new(
|
|
384
|
-
"LIX_ERROR_UNKNOWN",
|
|
385
|
-
"failed to acquire transaction staged extra commit parents lock",
|
|
386
|
-
)
|
|
387
|
-
})?;
|
|
388
|
-
let parents = guard.entry(version_id).or_default();
|
|
389
|
-
if !parents.contains(&parent_commit_id) {
|
|
390
|
-
parents.push(parent_commit_id);
|
|
391
|
-
}
|
|
392
|
-
Ok(())
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
pub(crate) fn staged_commit_id(&self, version_id: &str) -> Result<Option<String>, LixError> {
|
|
396
|
-
let guard = self.commit_members_by_version.lock().map_err(|_| {
|
|
397
|
-
LixError::new(
|
|
398
|
-
"LIX_ERROR_UNKNOWN",
|
|
399
|
-
"failed to acquire transaction staged commit membership lock",
|
|
400
|
-
)
|
|
401
|
-
})?;
|
|
402
|
-
Ok(guard
|
|
403
|
-
.get(version_id)
|
|
404
|
-
.map(|members| members.commit_id.clone()))
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/// Stages a commit for `version_id` even if no tracked state rows changed.
|
|
408
|
-
///
|
|
409
|
-
/// Merge uses this to record graph ancestry for convergent merges where the
|
|
410
|
-
/// target already has the same final state as the source, but the source
|
|
411
|
-
/// head is not reachable from the target head.
|
|
412
|
-
pub(crate) fn stage_empty_commit(&self, version_id: String) -> Result<String, LixError> {
|
|
413
|
-
let mut functions = self.functions.clone();
|
|
414
|
-
let mut guard = self.commit_members_by_version.lock().map_err(|_| {
|
|
415
|
-
LixError::new(
|
|
416
|
-
"LIX_ERROR_UNKNOWN",
|
|
417
|
-
"failed to acquire transaction staged commit membership lock",
|
|
418
|
-
)
|
|
419
|
-
})?;
|
|
420
|
-
let members = guard.entry(version_id).or_insert_with(|| {
|
|
421
|
-
StagedCommitMembers::new(
|
|
422
|
-
functions.uuid_v7(),
|
|
423
|
-
functions.uuid_v7(),
|
|
424
|
-
functions.timestamp(),
|
|
425
|
-
)
|
|
426
|
-
});
|
|
427
|
-
members.allow_empty();
|
|
428
|
-
Ok(members.commit_id.clone())
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/// Builds the transaction-local read overlay from currently staged writes.
|
|
432
|
-
pub(crate) fn staging_overlay(self: &Arc<Self>) -> Result<PreparedStateRowOverlay, LixError> {
|
|
433
|
-
let by_identity_guard = self.by_identity.lock().map_err(|_| {
|
|
434
|
-
LixError::new(
|
|
435
|
-
"LIX_ERROR_UNKNOWN",
|
|
436
|
-
"failed to acquire transaction staged identity index lock",
|
|
437
|
-
)
|
|
438
|
-
})?;
|
|
439
|
-
let slots = by_identity_guard
|
|
440
|
-
.iter()
|
|
441
|
-
.map(|(identity, slot)| (identity.clone(), *slot))
|
|
442
|
-
.collect();
|
|
443
|
-
Ok(PreparedStateRowOverlay {
|
|
444
|
-
staged_writes: Arc::clone(self),
|
|
445
|
-
slots,
|
|
446
|
-
})
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/// Stages one prepared write batch into this transaction.
|
|
450
|
-
///
|
|
451
|
-
/// Frontends hand raw `TransactionWriteRow`s to `Transaction`; normalization prepares
|
|
452
|
-
/// stable `PreparedStateRow`s before this method indexes them for transaction-
|
|
453
|
-
/// local reads and commit routing.
|
|
454
|
-
pub(crate) fn stage_write(
|
|
455
|
-
&self,
|
|
456
|
-
write: PreparedTransactionWrite,
|
|
457
|
-
) -> Result<TransactionWriteOutcome, LixError> {
|
|
458
|
-
let (mode, count) = match &write {
|
|
459
|
-
PreparedTransactionWrite::Rows { mode, rows } => (Some(*mode), rows.len() as u64),
|
|
460
|
-
PreparedTransactionWrite::RowsWithFileData { mode, count, .. } => (Some(*mode), *count),
|
|
461
|
-
PreparedTransactionWrite::AdoptedChanges { rows } => (None, rows.len() as u64),
|
|
462
|
-
};
|
|
463
|
-
let mut functions = self.functions.clone();
|
|
464
|
-
let (rows, adopted_rows, file_data_writes) = self.state_rows_from_stage_write(write)?;
|
|
465
|
-
for row in &rows {
|
|
466
|
-
validate_commit_membership_support(row)?;
|
|
467
|
-
}
|
|
468
|
-
for row in &adopted_rows {
|
|
469
|
-
validate_adopted_commit_membership_support(row)?;
|
|
470
|
-
}
|
|
471
|
-
reject_duplicate_present_rows_in_batch(&rows)?;
|
|
472
|
-
let mut guard = self.rows.lock().map_err(|_| {
|
|
473
|
-
LixError::new(
|
|
474
|
-
"LIX_ERROR_UNKNOWN",
|
|
475
|
-
"failed to acquire transaction staged writes lock",
|
|
476
|
-
)
|
|
477
|
-
})?;
|
|
478
|
-
let mut adopted_guard = self.adopted_rows.lock().map_err(|_| {
|
|
479
|
-
LixError::new(
|
|
480
|
-
"LIX_ERROR_UNKNOWN",
|
|
481
|
-
"failed to acquire transaction staged adopted writes lock",
|
|
482
|
-
)
|
|
483
|
-
})?;
|
|
484
|
-
let mut by_identity_guard = self.by_identity.lock().map_err(|_| {
|
|
485
|
-
LixError::new(
|
|
486
|
-
"LIX_ERROR_UNKNOWN",
|
|
487
|
-
"failed to acquire transaction staged identity index lock",
|
|
488
|
-
)
|
|
489
|
-
})?;
|
|
490
|
-
let mut commit_members_guard = self.commit_members_by_version.lock().map_err(|_| {
|
|
491
|
-
LixError::new(
|
|
492
|
-
"LIX_ERROR_UNKNOWN",
|
|
493
|
-
"failed to acquire transaction staged commit membership lock",
|
|
494
|
-
)
|
|
495
|
-
})?;
|
|
496
|
-
let mut insert_identities_guard = self.insert_identities.lock().map_err(|_| {
|
|
497
|
-
LixError::new(
|
|
498
|
-
"LIX_ERROR_UNKNOWN",
|
|
499
|
-
"failed to acquire transaction staged insert identity lock",
|
|
500
|
-
)
|
|
501
|
-
})?;
|
|
502
|
-
for mut row in rows {
|
|
503
|
-
let identity = PreparedStateRowIdentity::from(&row);
|
|
504
|
-
if mode == Some(TransactionWriteMode::Insert)
|
|
505
|
-
&& by_identity_guard.contains_key(&identity)
|
|
506
|
-
{
|
|
507
|
-
return Err(duplicate_insert_identity_error(&row));
|
|
508
|
-
}
|
|
509
|
-
if matches!(by_identity_guard.get(&identity), Some(RowSlot::Adopted(_))) {
|
|
510
|
-
return Err(conflicting_adopted_identity_error(&row));
|
|
511
|
-
}
|
|
512
|
-
let existing_slot = by_identity_guard.remove(&identity);
|
|
513
|
-
if let Some(RowSlot::State(index)) = existing_slot {
|
|
514
|
-
if let Some(previous) = guard.get_mut(index).and_then(Option::take) {
|
|
515
|
-
remove_row_from_commit_members(&mut commit_members_guard, &previous);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
add_row_to_commit_members(&mut commit_members_guard, &mut row, &mut functions);
|
|
519
|
-
let identity = PreparedStateRowIdentity::from(&row);
|
|
520
|
-
if mode == Some(TransactionWriteMode::Insert) {
|
|
521
|
-
insert_identities_guard.insert(identity.clone(), row.origin.clone());
|
|
522
|
-
}
|
|
523
|
-
let slot = match existing_slot {
|
|
524
|
-
Some(RowSlot::State(index)) => {
|
|
525
|
-
guard[index] = Some(row);
|
|
526
|
-
RowSlot::State(index)
|
|
527
|
-
}
|
|
528
|
-
_ => {
|
|
529
|
-
let index = guard.len();
|
|
530
|
-
guard.push(Some(row));
|
|
531
|
-
RowSlot::State(index)
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
by_identity_guard.insert(identity, slot);
|
|
535
|
-
}
|
|
536
|
-
for mut row in adopted_rows {
|
|
537
|
-
let identity = PreparedStateRowIdentity::from(&row);
|
|
538
|
-
if by_identity_guard.contains_key(&identity) {
|
|
539
|
-
return Err(conflicting_adopted_projection_error(&row));
|
|
540
|
-
}
|
|
541
|
-
add_adopted_row_to_commit_members(&mut commit_members_guard, &mut row, &mut functions);
|
|
542
|
-
let identity = PreparedStateRowIdentity::from(&row);
|
|
543
|
-
let index = adopted_guard.len();
|
|
544
|
-
adopted_guard.push(Some(row));
|
|
545
|
-
by_identity_guard.insert(identity, RowSlot::Adopted(index));
|
|
546
|
-
}
|
|
547
|
-
if !file_data_writes.is_empty() {
|
|
548
|
-
self.file_data_writes
|
|
549
|
-
.lock()
|
|
550
|
-
.map_err(|_| {
|
|
551
|
-
LixError::new(
|
|
552
|
-
"LIX_ERROR_UNKNOWN",
|
|
553
|
-
"failed to acquire transaction staged file data lock",
|
|
554
|
-
)
|
|
555
|
-
})?
|
|
556
|
-
.extend(file_data_writes);
|
|
557
|
-
}
|
|
558
|
-
Ok(TransactionWriteOutcome { count })
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
fn state_rows_from_stage_write(
|
|
562
|
-
&self,
|
|
563
|
-
write: PreparedTransactionWrite,
|
|
564
|
-
) -> Result<
|
|
565
|
-
(
|
|
566
|
-
Vec<PreparedStateRow>,
|
|
567
|
-
Vec<PreparedAdoptedStateRow>,
|
|
568
|
-
Vec<TransactionFileData>,
|
|
569
|
-
),
|
|
570
|
-
LixError,
|
|
571
|
-
> {
|
|
572
|
-
let mut state_rows = Vec::new();
|
|
573
|
-
let mut adopted_rows = Vec::new();
|
|
574
|
-
let mut file_data_writes = Vec::new();
|
|
575
|
-
match write {
|
|
576
|
-
PreparedTransactionWrite::Rows { rows, .. } => {
|
|
577
|
-
state_rows.extend(rows);
|
|
578
|
-
}
|
|
579
|
-
PreparedTransactionWrite::RowsWithFileData {
|
|
580
|
-
rows, file_data, ..
|
|
581
|
-
} => {
|
|
582
|
-
state_rows.extend(rows);
|
|
583
|
-
file_data_writes.extend(file_data);
|
|
584
|
-
}
|
|
585
|
-
PreparedTransactionWrite::AdoptedChanges { rows } => {
|
|
586
|
-
adopted_rows.extend(rows);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
Ok((state_rows, adopted_rows, file_data_writes))
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/// Read overlay derived from staged transaction writes.
|
|
594
|
-
pub(crate) struct PreparedStateRowOverlay {
|
|
595
|
-
staged_writes: Arc<TransactionWriteBuffer>,
|
|
596
|
-
slots: BTreeMap<PreparedStateRowIdentity, RowSlot>,
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
pub(crate) struct StagedScanParts {
|
|
600
|
-
pub(crate) rows: Vec<MaterializedLiveStateRow>,
|
|
601
|
-
pub(crate) hidden_identities: BTreeSet<PreparedStateRowIdentity>,
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
impl PreparedStateRowOverlay {
|
|
605
|
-
/// Returns staged rows visible for a scan request.
|
|
606
|
-
#[cfg(test)]
|
|
607
|
-
pub(crate) fn scan(
|
|
608
|
-
&self,
|
|
609
|
-
request: &LiveStateScanRequest,
|
|
610
|
-
) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
|
|
611
|
-
Ok(self.scan_parts(request)?.rows)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/// Returns staged rows and base-row identities hidden by staged rows in one pass.
|
|
615
|
-
///
|
|
616
|
-
/// Tombstones hide base rows even when the request does not include
|
|
617
|
-
/// tombstone rows in the visible result set.
|
|
618
|
-
pub(crate) fn scan_parts(
|
|
619
|
-
&self,
|
|
620
|
-
request: &LiveStateScanRequest,
|
|
621
|
-
) -> Result<StagedScanParts, LixError> {
|
|
622
|
-
let rows_guard = self.staged_writes.rows.lock().map_err(|_| {
|
|
623
|
-
LixError::new(
|
|
624
|
-
"LIX_ERROR_UNKNOWN",
|
|
625
|
-
"failed to acquire transaction staged writes lock",
|
|
626
|
-
)
|
|
627
|
-
})?;
|
|
628
|
-
let adopted_guard = self.staged_writes.adopted_rows.lock().map_err(|_| {
|
|
629
|
-
LixError::new(
|
|
630
|
-
"LIX_ERROR_UNKNOWN",
|
|
631
|
-
"failed to acquire transaction staged adopted writes lock",
|
|
632
|
-
)
|
|
633
|
-
})?;
|
|
634
|
-
|
|
635
|
-
let mut rows = Vec::new();
|
|
636
|
-
let mut hidden_identities = BTreeSet::new();
|
|
637
|
-
for (identity, slot) in &self.slots {
|
|
638
|
-
match *slot {
|
|
639
|
-
RowSlot::State(index) => {
|
|
640
|
-
let Some(row) = rows_guard.get(index).and_then(Option::as_ref) else {
|
|
641
|
-
continue;
|
|
642
|
-
};
|
|
643
|
-
if !staged_row_identity_matches_scan(row, request) {
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
hidden_identities.insert(identity.clone());
|
|
647
|
-
if row.snapshot.is_some() || request.filter.include_tombstones {
|
|
648
|
-
rows.push(MaterializedLiveStateRow::from(row));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
RowSlot::Adopted(index) => {
|
|
652
|
-
let Some(row) = adopted_guard.get(index).and_then(Option::as_ref) else {
|
|
653
|
-
continue;
|
|
654
|
-
};
|
|
655
|
-
if !adopted_row_identity_matches_scan(row, request) {
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
hidden_identities.insert(identity.clone());
|
|
659
|
-
if row.snapshot.is_some() || request.filter.include_tombstones {
|
|
660
|
-
rows.push(MaterializedLiveStateRow::from(row));
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
Ok(StagedScanParts {
|
|
666
|
-
rows,
|
|
667
|
-
hidden_identities,
|
|
668
|
-
})
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/// Returns a staged exact-row answer, if this transaction has one.
|
|
672
|
-
#[cfg(test)]
|
|
673
|
-
pub(crate) fn load_exact(&self, request: &LiveStateRowRequest) -> Option<StagedExactRow> {
|
|
674
|
-
let untracked_identity = PreparedStateRowIdentity::from_exact_request(request, true)?;
|
|
675
|
-
if let Some(row) = self.load_state_slot(&untracked_identity) {
|
|
676
|
-
return Some(if row.snapshot.is_none() {
|
|
677
|
-
StagedExactRow::Tombstone
|
|
678
|
-
} else {
|
|
679
|
-
StagedExactRow::Row(MaterializedLiveStateRow::from(&row))
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
let identity = PreparedStateRowIdentity::from_exact_request(request, false)?;
|
|
684
|
-
if let Some(row) = self.load_state_slot(&identity) {
|
|
685
|
-
return Some(if row.snapshot.is_none() {
|
|
686
|
-
StagedExactRow::Tombstone
|
|
687
|
-
} else {
|
|
688
|
-
StagedExactRow::Row(MaterializedLiveStateRow::from(&row))
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
self.load_adopted_slot(&identity).map(|row| {
|
|
692
|
-
if row.snapshot.is_none() {
|
|
693
|
-
StagedExactRow::Tombstone
|
|
694
|
-
} else {
|
|
695
|
-
StagedExactRow::Row(MaterializedLiveStateRow::from(&row))
|
|
696
|
-
}
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
#[cfg(test)]
|
|
701
|
-
fn load_state_slot(&self, identity: &PreparedStateRowIdentity) -> Option<PreparedStateRow> {
|
|
702
|
-
let Some(RowSlot::State(index)) = self.slots.get(identity).copied() else {
|
|
703
|
-
return None;
|
|
704
|
-
};
|
|
705
|
-
self.staged_writes
|
|
706
|
-
.rows
|
|
707
|
-
.lock()
|
|
708
|
-
.ok()?
|
|
709
|
-
.get(index)?
|
|
710
|
-
.as_ref()
|
|
711
|
-
.cloned()
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
#[cfg(test)]
|
|
715
|
-
fn load_adopted_slot(
|
|
716
|
-
&self,
|
|
717
|
-
identity: &PreparedStateRowIdentity,
|
|
718
|
-
) -> Option<PreparedAdoptedStateRow> {
|
|
719
|
-
let Some(RowSlot::Adopted(index)) = self.slots.get(identity).copied() else {
|
|
720
|
-
return None;
|
|
721
|
-
};
|
|
722
|
-
self.staged_writes
|
|
723
|
-
.adopted_rows
|
|
724
|
-
.lock()
|
|
725
|
-
.ok()?
|
|
726
|
-
.get(index)?
|
|
727
|
-
.as_ref()
|
|
728
|
-
.cloned()
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
#[cfg(test)]
|
|
733
|
-
pub(crate) enum StagedExactRow {
|
|
734
|
-
Row(MaterializedLiveStateRow),
|
|
735
|
-
Tombstone,
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
739
|
-
pub(crate) struct PreparedStateRowIdentity {
|
|
740
|
-
untracked: bool,
|
|
741
|
-
schema_key: String,
|
|
742
|
-
entity_id: crate::entity_identity::EntityIdentity,
|
|
743
|
-
file_id: Option<String>,
|
|
744
|
-
version_id: String,
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
impl PreparedStateRowIdentity {
|
|
748
|
-
fn from_staged_row(row: &PreparedStateRow) -> Self {
|
|
749
|
-
Self {
|
|
750
|
-
untracked: row.untracked,
|
|
751
|
-
schema_key: row.schema_key.clone(),
|
|
752
|
-
entity_id: row.entity_id.clone(),
|
|
753
|
-
file_id: row.file_id.clone(),
|
|
754
|
-
version_id: row.version_id.clone(),
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
#[cfg(test)]
|
|
759
|
-
fn from_exact_request(request: &LiveStateRowRequest, untracked: bool) -> Option<Self> {
|
|
760
|
-
let file_id = match &request.file_id {
|
|
761
|
-
NullableKeyFilter::Null => None,
|
|
762
|
-
NullableKeyFilter::Value(value) => Some(value.clone()),
|
|
763
|
-
// Exact overlay lookup requires a concrete row identity.
|
|
764
|
-
NullableKeyFilter::Any => return None,
|
|
765
|
-
};
|
|
766
|
-
Some(Self {
|
|
767
|
-
untracked,
|
|
768
|
-
schema_key: request.schema_key.clone(),
|
|
769
|
-
entity_id: request.entity_id.clone(),
|
|
770
|
-
file_id,
|
|
771
|
-
version_id: request.version_id.clone(),
|
|
772
|
-
})
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
pub(crate) fn schema_key(&self) -> &str {
|
|
776
|
-
&self.schema_key
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
pub(crate) fn entity_id(&self) -> &crate::entity_identity::EntityIdentity {
|
|
780
|
-
&self.entity_id
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
pub(crate) fn domain(&self) -> Domain {
|
|
784
|
-
Domain::exact_file(
|
|
785
|
-
self.version_id.clone(),
|
|
786
|
-
self.untracked,
|
|
787
|
-
self.file_id.clone(),
|
|
788
|
-
)
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
impl From<&PreparedStateRow> for PreparedStateRowIdentity {
|
|
793
|
-
fn from(row: &PreparedStateRow) -> Self {
|
|
794
|
-
Self::from_staged_row(row)
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
impl From<&PreparedAdoptedStateRow> for PreparedStateRowIdentity {
|
|
799
|
-
fn from(row: &PreparedAdoptedStateRow) -> Self {
|
|
800
|
-
Self {
|
|
801
|
-
untracked: false,
|
|
802
|
-
schema_key: row.schema_key.clone(),
|
|
803
|
-
entity_id: row.entity_id.clone(),
|
|
804
|
-
file_id: row.file_id.clone(),
|
|
805
|
-
version_id: row.version_id.clone(),
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
impl From<&MaterializedLiveStateRow> for PreparedStateRowIdentity {
|
|
811
|
-
fn from(row: &MaterializedLiveStateRow) -> Self {
|
|
812
|
-
Self {
|
|
813
|
-
untracked: row.untracked,
|
|
814
|
-
schema_key: row.schema_key.clone(),
|
|
815
|
-
entity_id: row.entity_id.clone(),
|
|
816
|
-
file_id: row.file_id.clone(),
|
|
817
|
-
version_id: row.version_id.clone(),
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
fn validate_commit_membership_support(row: &PreparedStateRow) -> Result<(), LixError> {
|
|
823
|
-
if row.global && row.version_id != GLOBAL_VERSION_ID {
|
|
824
|
-
return Err(LixError::new(
|
|
825
|
-
"LIX_ERROR_UNKNOWN",
|
|
826
|
-
"engine global staged rows must use the global version id",
|
|
827
|
-
));
|
|
828
|
-
}
|
|
829
|
-
Ok(())
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
fn validate_adopted_commit_membership_support(
|
|
833
|
-
row: &PreparedAdoptedStateRow,
|
|
834
|
-
) -> Result<(), LixError> {
|
|
835
|
-
if row.global && row.version_id != GLOBAL_VERSION_ID {
|
|
836
|
-
return Err(LixError::new(
|
|
837
|
-
"LIX_ERROR_UNKNOWN",
|
|
838
|
-
"engine global adopted rows must use the global version id",
|
|
839
|
-
));
|
|
840
|
-
}
|
|
841
|
-
Ok(())
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
fn reject_duplicate_present_rows_in_batch(rows: &[PreparedStateRow]) -> Result<(), LixError> {
|
|
845
|
-
let mut pending_present_rows = BTreeMap::<PreparedStateRowIdentity, &PreparedStateRow>::new();
|
|
846
|
-
for row in rows {
|
|
847
|
-
let identity = PreparedStateRowIdentity::from(row);
|
|
848
|
-
if row.snapshot.is_none() {
|
|
849
|
-
pending_present_rows.remove(&identity);
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if let Some(previous) = pending_present_rows.insert(identity, row) {
|
|
853
|
-
return Err(duplicate_staged_present_row_error(row, previous));
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
Ok(())
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
fn duplicate_staged_present_row_error(
|
|
860
|
-
row: &PreparedStateRow,
|
|
861
|
-
previous: &PreparedStateRow,
|
|
862
|
-
) -> LixError {
|
|
863
|
-
let message = logical_primary_key_violation_message(row.origin.as_ref())
|
|
864
|
-
.unwrap_or_else(|| {
|
|
865
|
-
format!(
|
|
866
|
-
"primary-key constraint violation on schema '{}': duplicate staged rows for entity_id '{}' in version '{}'",
|
|
867
|
-
row.schema_key,
|
|
868
|
-
previous
|
|
869
|
-
.entity_id
|
|
870
|
-
.as_json_array_text()
|
|
871
|
-
.unwrap_or_else(|_| "<invalid entity_id>".to_string()),
|
|
872
|
-
row.version_id
|
|
873
|
-
)
|
|
874
|
-
});
|
|
875
|
-
LixError::new(LixError::CODE_UNIQUE, message)
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
pub(crate) fn duplicate_insert_identity_message(
|
|
879
|
-
schema_key: &str,
|
|
880
|
-
entity_id: &crate::entity_identity::EntityIdentity,
|
|
881
|
-
version_id: Option<&str>,
|
|
882
|
-
origin: Option<&TransactionWriteOrigin>,
|
|
883
|
-
) -> String {
|
|
884
|
-
if let Some(message) = logical_primary_key_violation_message(origin) {
|
|
885
|
-
return message;
|
|
886
|
-
}
|
|
887
|
-
let entity_id = entity_id
|
|
888
|
-
.as_json_array_text()
|
|
889
|
-
.unwrap_or_else(|_| "<invalid entity_id>".to_string());
|
|
890
|
-
match version_id {
|
|
891
|
-
Some(version_id) => format!(
|
|
892
|
-
"primary-key constraint violation on schema '{schema_key}': INSERT would duplicate entity_id '{entity_id}' in version '{version_id}'"
|
|
893
|
-
),
|
|
894
|
-
None => format!(
|
|
895
|
-
"primary-key constraint violation on schema '{schema_key}': INSERT would duplicate entity_id '{entity_id}'"
|
|
896
|
-
),
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
fn duplicate_insert_identity_error(row: &PreparedStateRow) -> LixError {
|
|
901
|
-
let message = duplicate_insert_identity_message(
|
|
902
|
-
&row.schema_key,
|
|
903
|
-
&row.entity_id,
|
|
904
|
-
Some(&row.version_id),
|
|
905
|
-
row.origin.as_ref(),
|
|
906
|
-
);
|
|
907
|
-
LixError::new(LixError::CODE_UNIQUE, message)
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
fn logical_primary_key_violation_message(
|
|
911
|
-
origin: Option<&TransactionWriteOrigin>,
|
|
912
|
-
) -> Option<String> {
|
|
913
|
-
let origin = origin?;
|
|
914
|
-
if origin.operation != TransactionWriteOperation::Insert {
|
|
915
|
-
return None;
|
|
916
|
-
}
|
|
917
|
-
let primary_key = origin.primary_key.as_ref()?;
|
|
918
|
-
Some(format!(
|
|
919
|
-
"primary-key constraint violation on table '{}': INSERT would duplicate {}",
|
|
920
|
-
origin.surface,
|
|
921
|
-
format_logical_primary_key(primary_key)
|
|
922
|
-
))
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
fn format_logical_primary_key(primary_key: &LogicalPrimaryKey) -> String {
|
|
926
|
-
primary_key
|
|
927
|
-
.columns
|
|
928
|
-
.iter()
|
|
929
|
-
.enumerate()
|
|
930
|
-
.map(|(index, column)| {
|
|
931
|
-
let value = primary_key
|
|
932
|
-
.values
|
|
933
|
-
.get(index)
|
|
934
|
-
.map(String::as_str)
|
|
935
|
-
.unwrap_or("<missing>");
|
|
936
|
-
format!("{column} '{value}'")
|
|
937
|
-
})
|
|
938
|
-
.collect::<Vec<_>>()
|
|
939
|
-
.join(", ")
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
fn conflicting_adopted_identity_error(row: &PreparedStateRow) -> LixError {
|
|
943
|
-
LixError::new(
|
|
944
|
-
LixError::CODE_UNIQUE,
|
|
945
|
-
format!(
|
|
946
|
-
"transaction cannot stage a new row and an adopted projection for schema '{}' entity_id '{}' in version '{}'",
|
|
947
|
-
row.schema_key,
|
|
948
|
-
row.entity_id
|
|
949
|
-
.as_json_array_text()
|
|
950
|
-
.unwrap_or_else(|_| "<invalid entity_id>".to_string()),
|
|
951
|
-
row.version_id
|
|
952
|
-
),
|
|
953
|
-
)
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
fn conflicting_adopted_projection_error(row: &PreparedAdoptedStateRow) -> LixError {
|
|
957
|
-
LixError::new(
|
|
958
|
-
LixError::CODE_UNIQUE,
|
|
959
|
-
format!(
|
|
960
|
-
"transaction cannot stage duplicate adopted projections for schema '{}' entity_id '{}' in version '{}'",
|
|
961
|
-
row.schema_key,
|
|
962
|
-
row.entity_id
|
|
963
|
-
.as_json_array_text()
|
|
964
|
-
.unwrap_or_else(|_| "<invalid entity_id>".to_string()),
|
|
965
|
-
row.version_id
|
|
966
|
-
),
|
|
967
|
-
)
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
fn add_row_to_commit_members(
|
|
971
|
-
members_by_version: &mut BTreeMap<String, StagedCommitMembers>,
|
|
972
|
-
row: &mut PreparedStateRow,
|
|
973
|
-
functions: &mut dyn FunctionProvider,
|
|
974
|
-
) {
|
|
975
|
-
if row.untracked {
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
let change_id = row
|
|
979
|
-
.change_id
|
|
980
|
-
.clone()
|
|
981
|
-
.expect("tracked staged rows must carry change_id for commit membership");
|
|
982
|
-
let members = members_by_version
|
|
983
|
-
.entry(row.version_id.clone())
|
|
984
|
-
.or_insert_with(|| {
|
|
985
|
-
StagedCommitMembers::new(
|
|
986
|
-
functions.uuid_v7(),
|
|
987
|
-
functions.uuid_v7(),
|
|
988
|
-
functions.timestamp(),
|
|
989
|
-
)
|
|
990
|
-
});
|
|
991
|
-
row.commit_id = Some(members.commit_id.clone());
|
|
992
|
-
members.add_change_id(change_id);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
fn add_adopted_row_to_commit_members(
|
|
996
|
-
members_by_version: &mut BTreeMap<String, StagedCommitMembers>,
|
|
997
|
-
row: &mut PreparedAdoptedStateRow,
|
|
998
|
-
functions: &mut dyn FunctionProvider,
|
|
999
|
-
) {
|
|
1000
|
-
let members = members_by_version
|
|
1001
|
-
.entry(row.version_id.clone())
|
|
1002
|
-
.or_insert_with(|| {
|
|
1003
|
-
StagedCommitMembers::new(
|
|
1004
|
-
functions.uuid_v7(),
|
|
1005
|
-
functions.uuid_v7(),
|
|
1006
|
-
functions.timestamp(),
|
|
1007
|
-
)
|
|
1008
|
-
});
|
|
1009
|
-
row.commit_id = members.commit_id.clone();
|
|
1010
|
-
members.add_change_id(row.change_id.clone());
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
fn remove_row_from_commit_members(
|
|
1014
|
-
members_by_version: &mut BTreeMap<String, StagedCommitMembers>,
|
|
1015
|
-
row: &PreparedStateRow,
|
|
1016
|
-
) {
|
|
1017
|
-
if row.untracked {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
let Some(members) = members_by_version.get_mut(&row.version_id) else {
|
|
1021
|
-
return;
|
|
1022
|
-
};
|
|
1023
|
-
let Some(change_id) = row.change_id.as_deref() else {
|
|
1024
|
-
return;
|
|
1025
|
-
};
|
|
1026
|
-
members.remove_change_id(change_id);
|
|
1027
|
-
if members.is_empty() {
|
|
1028
|
-
members_by_version.remove(&row.version_id);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
fn adopted_row_identity_matches_scan(
|
|
1033
|
-
row: &PreparedAdoptedStateRow,
|
|
1034
|
-
request: &LiveStateScanRequest,
|
|
1035
|
-
) -> bool {
|
|
1036
|
-
if !request.filter.schema_keys.is_empty()
|
|
1037
|
-
&& !request.filter.schema_keys.contains(&row.schema_key)
|
|
1038
|
-
{
|
|
1039
|
-
return false;
|
|
1040
|
-
}
|
|
1041
|
-
if !request.filter.entity_ids.is_empty() && !request.filter.entity_ids.contains(&row.entity_id)
|
|
1042
|
-
{
|
|
1043
|
-
return false;
|
|
1044
|
-
}
|
|
1045
|
-
if !request.filter.version_ids.is_empty()
|
|
1046
|
-
&& !request.filter.version_ids.contains(&row.version_id)
|
|
1047
|
-
{
|
|
1048
|
-
return false;
|
|
1049
|
-
}
|
|
1050
|
-
if request.filter.untracked == Some(true) {
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
nullable_key_matches_filters(&row.file_id, &request.filter.file_ids)
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
fn staged_row_identity_matches_scan(
|
|
1057
|
-
row: &PreparedStateRow,
|
|
1058
|
-
request: &LiveStateScanRequest,
|
|
1059
|
-
) -> bool {
|
|
1060
|
-
if !request.filter.schema_keys.is_empty()
|
|
1061
|
-
&& !request.filter.schema_keys.contains(&row.schema_key)
|
|
1062
|
-
{
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
if !request.filter.entity_ids.is_empty() && !request.filter.entity_ids.contains(&row.entity_id)
|
|
1066
|
-
{
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
if !request.filter.version_ids.is_empty()
|
|
1070
|
-
&& !request.filter.version_ids.contains(&row.version_id)
|
|
1071
|
-
{
|
|
1072
|
-
return false;
|
|
1073
|
-
}
|
|
1074
|
-
if request
|
|
1075
|
-
.filter
|
|
1076
|
-
.untracked
|
|
1077
|
-
.is_some_and(|untracked| row.untracked != untracked)
|
|
1078
|
-
{
|
|
1079
|
-
return false;
|
|
1080
|
-
}
|
|
1081
|
-
nullable_key_matches_filters(&row.file_id, &request.filter.file_ids)
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
fn nullable_key_matches_filters(
|
|
1085
|
-
value: &Option<String>,
|
|
1086
|
-
filters: &[NullableKeyFilter<String>],
|
|
1087
|
-
) -> bool {
|
|
1088
|
-
filters.is_empty()
|
|
1089
|
-
|| filters
|
|
1090
|
-
.iter()
|
|
1091
|
-
.any(|filter| nullable_key_matches_filter(value, filter))
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
fn nullable_key_matches_filter(value: &Option<String>, filter: &NullableKeyFilter<String>) -> bool {
|
|
1095
|
-
match filter {
|
|
1096
|
-
NullableKeyFilter::Any => true,
|
|
1097
|
-
NullableKeyFilter::Null => value.is_none(),
|
|
1098
|
-
NullableKeyFilter::Value(expected) => value.as_ref() == Some(expected),
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
#[cfg(test)]
|
|
1103
|
-
mod tests {
|
|
1104
|
-
use super::*;
|
|
1105
|
-
use crate::functions::SharedFunctionProvider;
|
|
1106
|
-
use crate::live_state::{LiveStateFilter, LiveStateRowRequest};
|
|
1107
|
-
|
|
1108
|
-
#[tokio::test]
|
|
1109
|
-
async fn staging_overlay_uses_last_staged_row_for_exact_load() {
|
|
1110
|
-
let staged_writes = test_staged_writes();
|
|
1111
|
-
|
|
1112
|
-
staged_writes
|
|
1113
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1114
|
-
mode: TransactionWriteMode::Replace,
|
|
1115
|
-
rows: vec![state_row("sql2-duplicate-key", "first")],
|
|
1116
|
-
})
|
|
1117
|
-
.expect("initial row should stage");
|
|
1118
|
-
staged_writes
|
|
1119
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1120
|
-
mode: TransactionWriteMode::Replace,
|
|
1121
|
-
rows: vec![state_row("sql2-duplicate-key", "second")],
|
|
1122
|
-
})
|
|
1123
|
-
.expect("staging rows should succeed");
|
|
1124
|
-
|
|
1125
|
-
let overlay = staged_writes
|
|
1126
|
-
.staging_overlay()
|
|
1127
|
-
.expect("overlay should build from staged rows");
|
|
1128
|
-
let row = overlay
|
|
1129
|
-
.load_exact(&LiveStateRowRequest {
|
|
1130
|
-
schema_key: "lix_key_value".to_string(),
|
|
1131
|
-
version_id: "global".to_string(),
|
|
1132
|
-
entity_id: crate::entity_identity::EntityIdentity::single("sql2-duplicate-key"),
|
|
1133
|
-
file_id: NullableKeyFilter::Null,
|
|
1134
|
-
})
|
|
1135
|
-
.expect("staged row should be visible");
|
|
1136
|
-
|
|
1137
|
-
let StagedExactRow::Row(row) = row else {
|
|
1138
|
-
panic!("latest staged row should not be a tombstone");
|
|
1139
|
-
};
|
|
1140
|
-
assert_eq!(
|
|
1141
|
-
row.snapshot_content.as_deref(),
|
|
1142
|
-
Some("{\"key\":\"sql2-duplicate-key\",\"value\":\"second\"}")
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
#[tokio::test]
|
|
1147
|
-
async fn staging_overlay_scan_returns_only_latest_row_per_identity() {
|
|
1148
|
-
let staged_writes = test_staged_writes();
|
|
1149
|
-
|
|
1150
|
-
staged_writes
|
|
1151
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1152
|
-
mode: TransactionWriteMode::Replace,
|
|
1153
|
-
rows: vec![state_row("sql2-duplicate-key", "first")],
|
|
1154
|
-
})
|
|
1155
|
-
.expect("initial row should stage");
|
|
1156
|
-
staged_writes
|
|
1157
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1158
|
-
mode: TransactionWriteMode::Replace,
|
|
1159
|
-
rows: vec![state_row("sql2-duplicate-key", "second")],
|
|
1160
|
-
})
|
|
1161
|
-
.expect("staging rows should succeed");
|
|
1162
|
-
|
|
1163
|
-
let overlay = staged_writes
|
|
1164
|
-
.staging_overlay()
|
|
1165
|
-
.expect("overlay should build from staged rows");
|
|
1166
|
-
let rows = overlay
|
|
1167
|
-
.scan(&scan_request_for_key("sql2-duplicate-key", false))
|
|
1168
|
-
.expect("overlay scan should succeed");
|
|
1169
|
-
|
|
1170
|
-
assert_eq!(rows.len(), 1);
|
|
1171
|
-
assert_eq!(
|
|
1172
|
-
rows[0].snapshot_content.as_deref(),
|
|
1173
|
-
Some("{\"key\":\"sql2-duplicate-key\",\"value\":\"second\"}")
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
#[tokio::test]
|
|
1178
|
-
async fn staging_overlay_delete_hides_prior_staged_insert() {
|
|
1179
|
-
let staged_writes = test_staged_writes();
|
|
1180
|
-
|
|
1181
|
-
staged_writes
|
|
1182
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1183
|
-
mode: TransactionWriteMode::Replace,
|
|
1184
|
-
rows: vec![
|
|
1185
|
-
state_row("sql2-delete-key", "visible"),
|
|
1186
|
-
tombstone_row("sql2-delete-key"),
|
|
1187
|
-
],
|
|
1188
|
-
})
|
|
1189
|
-
.expect("staging rows should succeed");
|
|
1190
|
-
|
|
1191
|
-
let overlay = staged_writes
|
|
1192
|
-
.staging_overlay()
|
|
1193
|
-
.expect("overlay should build from staged rows");
|
|
1194
|
-
let exact = overlay
|
|
1195
|
-
.load_exact(&exact_request_for_key("sql2-delete-key"))
|
|
1196
|
-
.expect("staged tombstone should answer exact load");
|
|
1197
|
-
assert!(matches!(exact, StagedExactRow::Tombstone));
|
|
1198
|
-
assert!(overlay
|
|
1199
|
-
.scan(&scan_request_for_key("sql2-delete-key", false))
|
|
1200
|
-
.expect("overlay scan should succeed")
|
|
1201
|
-
.is_empty());
|
|
1202
|
-
|
|
1203
|
-
let tombstones = overlay
|
|
1204
|
-
.scan(&scan_request_for_key("sql2-delete-key", true))
|
|
1205
|
-
.expect("overlay scan should succeed");
|
|
1206
|
-
assert_eq!(tombstones.len(), 1);
|
|
1207
|
-
assert_eq!(tombstones[0].snapshot_content, None);
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
#[tokio::test]
|
|
1211
|
-
async fn staging_overlay_insert_after_delete_resurrects_row() {
|
|
1212
|
-
let staged_writes = test_staged_writes();
|
|
1213
|
-
|
|
1214
|
-
staged_writes
|
|
1215
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1216
|
-
mode: TransactionWriteMode::Replace,
|
|
1217
|
-
rows: vec![
|
|
1218
|
-
tombstone_row("sql2-resurrect-key"),
|
|
1219
|
-
state_row("sql2-resurrect-key", "visible-again"),
|
|
1220
|
-
],
|
|
1221
|
-
})
|
|
1222
|
-
.expect("staging rows should succeed");
|
|
1223
|
-
|
|
1224
|
-
let overlay = staged_writes
|
|
1225
|
-
.staging_overlay()
|
|
1226
|
-
.expect("overlay should build from staged rows");
|
|
1227
|
-
let exact = overlay
|
|
1228
|
-
.load_exact(&exact_request_for_key("sql2-resurrect-key"))
|
|
1229
|
-
.expect("staged row should answer exact load");
|
|
1230
|
-
|
|
1231
|
-
let StagedExactRow::Row(row) = exact else {
|
|
1232
|
-
panic!("latest staged row should be visible");
|
|
1233
|
-
};
|
|
1234
|
-
assert_eq!(
|
|
1235
|
-
row.snapshot_content.as_deref(),
|
|
1236
|
-
Some("{\"key\":\"sql2-resurrect-key\",\"value\":\"visible-again\"}")
|
|
1237
|
-
);
|
|
1238
|
-
assert_eq!(
|
|
1239
|
-
overlay
|
|
1240
|
-
.scan(&scan_request_for_key("sql2-resurrect-key", false))
|
|
1241
|
-
.expect("overlay scan should succeed")
|
|
1242
|
-
.len(),
|
|
1243
|
-
1
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
#[tokio::test]
|
|
1248
|
-
async fn staged_writes_drain_returns_coalesced_latest_rows() {
|
|
1249
|
-
let staged_writes = test_staged_writes();
|
|
1250
|
-
|
|
1251
|
-
staged_writes
|
|
1252
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1253
|
-
mode: TransactionWriteMode::Replace,
|
|
1254
|
-
rows: vec![
|
|
1255
|
-
state_row("sql2-key-a", "first"),
|
|
1256
|
-
state_row("sql2-key-b", "only"),
|
|
1257
|
-
],
|
|
1258
|
-
})
|
|
1259
|
-
.expect("initial rows should stage");
|
|
1260
|
-
staged_writes
|
|
1261
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1262
|
-
mode: TransactionWriteMode::Replace,
|
|
1263
|
-
rows: vec![state_row("sql2-key-a", "second")],
|
|
1264
|
-
})
|
|
1265
|
-
.expect("staging rows should succeed");
|
|
1266
|
-
|
|
1267
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1268
|
-
|
|
1269
|
-
assert_eq!(drained.state_rows.len(), 2);
|
|
1270
|
-
assert!(drained.state_rows.iter().any(|row| {
|
|
1271
|
-
row.entity_id == crate::entity_identity::EntityIdentity::single("sql2-key-a")
|
|
1272
|
-
&& row
|
|
1273
|
-
.snapshot
|
|
1274
|
-
.as_ref()
|
|
1275
|
-
.map(|snapshot| snapshot.normalized.as_ref())
|
|
1276
|
-
== Some("{\"key\":\"sql2-key-a\",\"value\":\"second\"}")
|
|
1277
|
-
}));
|
|
1278
|
-
assert!(drained.state_rows.iter().any(|row| {
|
|
1279
|
-
row.entity_id == crate::entity_identity::EntityIdentity::single("sql2-key-b")
|
|
1280
|
-
&& row
|
|
1281
|
-
.snapshot
|
|
1282
|
-
.as_ref()
|
|
1283
|
-
.map(|snapshot| snapshot.normalized.as_ref())
|
|
1284
|
-
== Some("{\"key\":\"sql2-key-b\",\"value\":\"only\"}")
|
|
1285
|
-
}));
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
#[tokio::test]
|
|
1289
|
-
async fn staged_writes_drain_preserves_file_data_payloads() {
|
|
1290
|
-
let staged_writes = test_staged_writes();
|
|
1291
|
-
|
|
1292
|
-
staged_writes
|
|
1293
|
-
.stage_write(PreparedTransactionWrite::RowsWithFileData {
|
|
1294
|
-
mode: TransactionWriteMode::Replace,
|
|
1295
|
-
rows: vec![state_row("file-readme", "descriptor")],
|
|
1296
|
-
file_data: vec![TransactionFileData {
|
|
1297
|
-
file_id: "file-readme".to_string(),
|
|
1298
|
-
version_id: "global".to_string(),
|
|
1299
|
-
untracked: true,
|
|
1300
|
-
data: b"hello".to_vec(),
|
|
1301
|
-
}],
|
|
1302
|
-
count: 1,
|
|
1303
|
-
})
|
|
1304
|
-
.expect("staging rows with file data should succeed");
|
|
1305
|
-
|
|
1306
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1307
|
-
|
|
1308
|
-
assert_eq!(drained.state_rows.len(), 1);
|
|
1309
|
-
assert_eq!(drained.file_data_writes.len(), 1);
|
|
1310
|
-
assert_eq!(drained.file_data_writes[0].file_id, "file-readme");
|
|
1311
|
-
assert_eq!(drained.file_data_writes[0].data, b"hello");
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
#[tokio::test]
|
|
1315
|
-
async fn staged_writes_track_commit_members_for_tracked_global_rows() {
|
|
1316
|
-
let staged_writes = test_staged_writes();
|
|
1317
|
-
|
|
1318
|
-
staged_writes
|
|
1319
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1320
|
-
mode: TransactionWriteMode::Replace,
|
|
1321
|
-
rows: vec![state_row("tracked-key", "value").with_tracked()],
|
|
1322
|
-
})
|
|
1323
|
-
.expect("tracked global row should stage");
|
|
1324
|
-
|
|
1325
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1326
|
-
let members = drained
|
|
1327
|
-
.commit_members_by_version
|
|
1328
|
-
.get("global")
|
|
1329
|
-
.expect("global commit members should exist");
|
|
1330
|
-
assert_eq!(
|
|
1331
|
-
members.change_ids.iter().cloned().collect::<Vec<_>>(),
|
|
1332
|
-
vec!["test-change-id".to_string()]
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
#[tokio::test]
|
|
1337
|
-
async fn staged_writes_do_not_track_untracked_rows_as_commit_members() {
|
|
1338
|
-
let staged_writes = test_staged_writes();
|
|
1339
|
-
|
|
1340
|
-
staged_writes
|
|
1341
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1342
|
-
mode: TransactionWriteMode::Replace,
|
|
1343
|
-
rows: vec![state_row("untracked-key", "value")],
|
|
1344
|
-
})
|
|
1345
|
-
.expect("untracked row should stage");
|
|
1346
|
-
|
|
1347
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1348
|
-
assert!(drained.commit_members_by_version.is_empty());
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
#[tokio::test]
|
|
1352
|
-
async fn staged_writes_replace_commit_member_on_tracked_overwrite() {
|
|
1353
|
-
let staged_writes = test_staged_writes();
|
|
1354
|
-
|
|
1355
|
-
staged_writes
|
|
1356
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1357
|
-
mode: TransactionWriteMode::Replace,
|
|
1358
|
-
rows: vec![state_row("overwrite-key", "first")
|
|
1359
|
-
.with_tracked()
|
|
1360
|
-
.with_change_id("change-first")],
|
|
1361
|
-
})
|
|
1362
|
-
.expect("initial tracked row should stage");
|
|
1363
|
-
staged_writes
|
|
1364
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1365
|
-
mode: TransactionWriteMode::Replace,
|
|
1366
|
-
rows: vec![state_row("overwrite-key", "second")
|
|
1367
|
-
.with_tracked()
|
|
1368
|
-
.with_change_id("change-second")],
|
|
1369
|
-
})
|
|
1370
|
-
.expect("tracked overwrite should stage");
|
|
1371
|
-
|
|
1372
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1373
|
-
let members = drained
|
|
1374
|
-
.commit_members_by_version
|
|
1375
|
-
.get("global")
|
|
1376
|
-
.expect("global commit members should exist");
|
|
1377
|
-
assert_eq!(
|
|
1378
|
-
members.change_ids.iter().cloned().collect::<Vec<_>>(),
|
|
1379
|
-
vec!["change-second".to_string()]
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
#[tokio::test]
|
|
1384
|
-
async fn staged_writes_keep_tracked_and_untracked_domains_separate() {
|
|
1385
|
-
let staged_writes = test_staged_writes();
|
|
1386
|
-
|
|
1387
|
-
staged_writes
|
|
1388
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1389
|
-
mode: TransactionWriteMode::Replace,
|
|
1390
|
-
rows: vec![
|
|
1391
|
-
state_row("tracked-to-untracked-key", "tracked")
|
|
1392
|
-
.with_tracked()
|
|
1393
|
-
.with_change_id("change-tracked"),
|
|
1394
|
-
state_row("tracked-to-untracked-key", "untracked")
|
|
1395
|
-
.with_change_id("change-untracked"),
|
|
1396
|
-
],
|
|
1397
|
-
})
|
|
1398
|
-
.expect("untracked overwrite should stage");
|
|
1399
|
-
|
|
1400
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1401
|
-
assert_eq!(drained.state_rows.len(), 2);
|
|
1402
|
-
assert!(drained
|
|
1403
|
-
.state_rows
|
|
1404
|
-
.iter()
|
|
1405
|
-
.any(|row| { row.change_id.as_deref() == Some("change-tracked") && !row.untracked }));
|
|
1406
|
-
assert!(drained
|
|
1407
|
-
.state_rows
|
|
1408
|
-
.iter()
|
|
1409
|
-
.any(|row| { row.change_id.as_deref() == Some("change-untracked") && row.untracked }));
|
|
1410
|
-
let members = drained
|
|
1411
|
-
.commit_members_by_version
|
|
1412
|
-
.get("global")
|
|
1413
|
-
.expect("tracked commit member should remain in tracked domain");
|
|
1414
|
-
assert_eq!(
|
|
1415
|
-
members.change_ids.iter().cloned().collect::<Vec<_>>(),
|
|
1416
|
-
vec!["change-tracked".to_string()]
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
#[tokio::test]
|
|
1421
|
-
async fn staged_writes_reject_duplicate_present_rows_in_one_batch() {
|
|
1422
|
-
let staged_writes = test_staged_writes();
|
|
1423
|
-
|
|
1424
|
-
let error = staged_writes
|
|
1425
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1426
|
-
mode: TransactionWriteMode::Replace,
|
|
1427
|
-
rows: vec![
|
|
1428
|
-
state_row("duplicate-present-key", "first"),
|
|
1429
|
-
state_row("duplicate-present-key", "second"),
|
|
1430
|
-
],
|
|
1431
|
-
})
|
|
1432
|
-
.expect_err("same-batch duplicate present rows should fail");
|
|
1433
|
-
|
|
1434
|
-
assert_eq!(error.code, LixError::CODE_UNIQUE);
|
|
1435
|
-
assert!(
|
|
1436
|
-
error.message.contains("primary-key constraint violation"),
|
|
1437
|
-
"error should explain the duplicate primary key: {error:?}"
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
#[tokio::test]
|
|
1442
|
-
async fn staged_writes_insert_keeps_tracked_and_untracked_rows_as_distinct_identities() {
|
|
1443
|
-
let staged_writes = test_staged_writes();
|
|
1444
|
-
|
|
1445
|
-
staged_writes
|
|
1446
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1447
|
-
mode: TransactionWriteMode::Insert,
|
|
1448
|
-
rows: vec![
|
|
1449
|
-
state_row("shared-domain-key", "tracked").with_tracked(),
|
|
1450
|
-
state_row("shared-domain-key", "untracked"),
|
|
1451
|
-
],
|
|
1452
|
-
})
|
|
1453
|
-
.expect("tracked and untracked rows are distinct domain identities");
|
|
1454
|
-
|
|
1455
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1456
|
-
assert_eq!(drained.state_rows.len(), 2);
|
|
1457
|
-
assert!(drained.state_rows.iter().any(|row| {
|
|
1458
|
-
row.entity_id == crate::entity_identity::EntityIdentity::single("shared-domain-key")
|
|
1459
|
-
&& !row.untracked
|
|
1460
|
-
}));
|
|
1461
|
-
assert!(drained.state_rows.iter().any(|row| {
|
|
1462
|
-
row.entity_id == crate::entity_identity::EntityIdentity::single("shared-domain-key")
|
|
1463
|
-
&& row.untracked
|
|
1464
|
-
}));
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
#[tokio::test]
|
|
1468
|
-
async fn staged_writes_track_active_version_members_separately() {
|
|
1469
|
-
let staged_writes = test_staged_writes();
|
|
1470
|
-
|
|
1471
|
-
staged_writes
|
|
1472
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1473
|
-
mode: TransactionWriteMode::Replace,
|
|
1474
|
-
rows: vec![state_row("active-version-key", "value")
|
|
1475
|
-
.with_tracked()
|
|
1476
|
-
.with_version("version-a")],
|
|
1477
|
-
})
|
|
1478
|
-
.expect("active-version tracked staging should accumulate members");
|
|
1479
|
-
|
|
1480
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1481
|
-
let members = drained
|
|
1482
|
-
.commit_members_by_version
|
|
1483
|
-
.get("version-a")
|
|
1484
|
-
.expect("active-version commit members should exist");
|
|
1485
|
-
assert_eq!(
|
|
1486
|
-
members.change_ids.iter().cloned().collect::<Vec<_>>(),
|
|
1487
|
-
vec!["test-change-id".to_string()]
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
#[tokio::test]
|
|
1492
|
-
async fn staged_writes_reject_global_rows_with_non_global_version_id() {
|
|
1493
|
-
let staged_writes = test_staged_writes();
|
|
1494
|
-
|
|
1495
|
-
let error = staged_writes
|
|
1496
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1497
|
-
mode: TransactionWriteMode::Replace,
|
|
1498
|
-
rows: vec![{
|
|
1499
|
-
let mut row = state_row("invalid-global-key", "value");
|
|
1500
|
-
row.version_id = "version-a".to_string();
|
|
1501
|
-
row
|
|
1502
|
-
}],
|
|
1503
|
-
})
|
|
1504
|
-
.expect_err("global row with non-global version should fail");
|
|
1505
|
-
|
|
1506
|
-
assert!(error
|
|
1507
|
-
.message
|
|
1508
|
-
.contains("global staged rows must use the global version id"));
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
#[tokio::test]
|
|
1512
|
-
async fn staging_overlay_identity_matches_live_state_conflict_key() {
|
|
1513
|
-
let staged_writes = test_staged_writes();
|
|
1514
|
-
|
|
1515
|
-
staged_writes
|
|
1516
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1517
|
-
mode: TransactionWriteMode::Replace,
|
|
1518
|
-
rows: vec![state_row("shared-entity", "base")],
|
|
1519
|
-
})
|
|
1520
|
-
.expect("initial same-identity row should stage");
|
|
1521
|
-
staged_writes
|
|
1522
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1523
|
-
mode: TransactionWriteMode::Replace,
|
|
1524
|
-
rows: vec![
|
|
1525
|
-
state_row("shared-entity", "base"),
|
|
1526
|
-
state_row("shared-entity", "other-version").with_version("version-b"),
|
|
1527
|
-
state_row("shared-entity", "other-schema").with_schema("other_schema"),
|
|
1528
|
-
state_row("shared-entity", "other-file").with_file_id("file-a"),
|
|
1529
|
-
state_row("shared-entity", "tracked").with_tracked(),
|
|
1530
|
-
],
|
|
1531
|
-
})
|
|
1532
|
-
.expect("staging rows should succeed");
|
|
1533
|
-
|
|
1534
|
-
let overlay = staged_writes
|
|
1535
|
-
.staging_overlay()
|
|
1536
|
-
.expect("overlay should build from staged rows");
|
|
1537
|
-
let rows = overlay
|
|
1538
|
-
.scan(&LiveStateScanRequest {
|
|
1539
|
-
filter: LiveStateFilter {
|
|
1540
|
-
entity_ids: vec![crate::entity_identity::EntityIdentity::single(
|
|
1541
|
-
"shared-entity",
|
|
1542
|
-
)],
|
|
1543
|
-
include_tombstones: true,
|
|
1544
|
-
..LiveStateFilter::default()
|
|
1545
|
-
},
|
|
1546
|
-
..LiveStateScanRequest::default()
|
|
1547
|
-
})
|
|
1548
|
-
.expect("overlay scan should succeed");
|
|
1549
|
-
|
|
1550
|
-
assert_eq!(rows.len(), 5);
|
|
1551
|
-
assert_eq!(
|
|
1552
|
-
rows.iter()
|
|
1553
|
-
.filter(|row| row.entity_id
|
|
1554
|
-
== crate::entity_identity::EntityIdentity::single("shared-entity")
|
|
1555
|
-
&& row.version_id == "global"
|
|
1556
|
-
&& row.schema_key == "lix_key_value"
|
|
1557
|
-
&& row.file_id.is_none())
|
|
1558
|
-
.count(),
|
|
1559
|
-
2
|
|
1560
|
-
);
|
|
1561
|
-
assert!(rows.iter().any(|row| {
|
|
1562
|
-
row.snapshot_content.as_deref()
|
|
1563
|
-
== Some("{\"key\":\"shared-entity\",\"value\":\"tracked\"}")
|
|
1564
|
-
}));
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
#[tokio::test]
|
|
1568
|
-
async fn staged_writes_use_injected_function_provider_for_commit_metadata() {
|
|
1569
|
-
let staged_writes = test_staged_writes();
|
|
1570
|
-
|
|
1571
|
-
staged_writes
|
|
1572
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1573
|
-
mode: TransactionWriteMode::Replace,
|
|
1574
|
-
rows: vec![state_row("sql2-functions-key", "value").with_tracked()],
|
|
1575
|
-
})
|
|
1576
|
-
.expect("staging rows should succeed");
|
|
1577
|
-
|
|
1578
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1579
|
-
let members = drained
|
|
1580
|
-
.commit_members_by_version
|
|
1581
|
-
.get("global")
|
|
1582
|
-
.expect("global commit members should exist");
|
|
1583
|
-
assert_eq!(members.commit_id, "test-uuid-1");
|
|
1584
|
-
assert_eq!(members.commit_change_id, "test-uuid-2");
|
|
1585
|
-
assert_eq!(members.created_at, "test-timestamp-1");
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
#[tokio::test]
|
|
1589
|
-
async fn staged_writes_stamp_tracked_rows_with_commit_id_during_staging() {
|
|
1590
|
-
let staged_writes = test_staged_writes();
|
|
1591
|
-
|
|
1592
|
-
staged_writes
|
|
1593
|
-
.stage_write(PreparedTransactionWrite::Rows {
|
|
1594
|
-
mode: TransactionWriteMode::Replace,
|
|
1595
|
-
rows: vec![state_row("tracked-commit-key", "value").with_tracked()],
|
|
1596
|
-
})
|
|
1597
|
-
.expect("tracked row should stage");
|
|
1598
|
-
|
|
1599
|
-
let drained = staged_writes.drain().expect("drain should succeed");
|
|
1600
|
-
assert_eq!(drained.state_rows.len(), 1);
|
|
1601
|
-
assert_eq!(
|
|
1602
|
-
drained.state_rows[0].commit_id.as_deref(),
|
|
1603
|
-
Some("test-uuid-1")
|
|
1604
|
-
);
|
|
1605
|
-
assert_eq!(
|
|
1606
|
-
drained
|
|
1607
|
-
.commit_members_by_version
|
|
1608
|
-
.get("global")
|
|
1609
|
-
.expect("global commit members should exist")
|
|
1610
|
-
.commit_id,
|
|
1611
|
-
"test-uuid-1"
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
fn test_staged_writes() -> Arc<TransactionWriteBuffer> {
|
|
1616
|
-
Arc::new(TransactionWriteBuffer::new(SharedFunctionProvider::new(
|
|
1617
|
-
Box::new(TestFunctionProvider::default()) as Box<dyn FunctionProvider + Send>,
|
|
1618
|
-
)))
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
#[derive(Default)]
|
|
1622
|
-
struct TestFunctionProvider {
|
|
1623
|
-
uuid_count: usize,
|
|
1624
|
-
timestamp_count: usize,
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
impl FunctionProvider for TestFunctionProvider {
|
|
1628
|
-
fn uuid_v7(&mut self) -> String {
|
|
1629
|
-
self.uuid_count += 1;
|
|
1630
|
-
format!("test-uuid-{}", self.uuid_count)
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
fn timestamp(&mut self) -> String {
|
|
1634
|
-
self.timestamp_count += 1;
|
|
1635
|
-
format!("test-timestamp-{}", self.timestamp_count)
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
fn state_row(key: &str, value: &str) -> PreparedStateRow {
|
|
1640
|
-
let snapshot = stage_json_from_value(
|
|
1641
|
-
TransactionJson::from_value_for_test(serde_json::json!({ "key": key, "value": value })),
|
|
1642
|
-
"test staged row snapshot_content",
|
|
1643
|
-
)
|
|
1644
|
-
.expect("test snapshot should prepare");
|
|
1645
|
-
PreparedStateRow {
|
|
1646
|
-
schema_plan_id: SchemaPlanId::for_test(0),
|
|
1647
|
-
facts: crate::transaction::types::PreparedRowFacts::default(),
|
|
1648
|
-
entity_id: crate::entity_identity::EntityIdentity::single(key),
|
|
1649
|
-
schema_key: "lix_key_value".to_string(),
|
|
1650
|
-
file_id: None,
|
|
1651
|
-
snapshot: Some(snapshot),
|
|
1652
|
-
metadata: None,
|
|
1653
|
-
origin: None,
|
|
1654
|
-
created_at: "test-created-at".to_string(),
|
|
1655
|
-
updated_at: "test-updated-at".to_string(),
|
|
1656
|
-
global: true,
|
|
1657
|
-
change_id: None,
|
|
1658
|
-
commit_id: None,
|
|
1659
|
-
untracked: true,
|
|
1660
|
-
version_id: "global".to_string(),
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
fn tombstone_row(key: &str) -> PreparedStateRow {
|
|
1665
|
-
let mut row = state_row(key, "deleted");
|
|
1666
|
-
row.snapshot = None;
|
|
1667
|
-
row
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
fn exact_request_for_key(key: &str) -> LiveStateRowRequest {
|
|
1671
|
-
LiveStateRowRequest {
|
|
1672
|
-
schema_key: "lix_key_value".to_string(),
|
|
1673
|
-
version_id: "global".to_string(),
|
|
1674
|
-
entity_id: crate::entity_identity::EntityIdentity::single(key),
|
|
1675
|
-
file_id: NullableKeyFilter::Null,
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
fn scan_request_for_key(key: &str, include_tombstones: bool) -> LiveStateScanRequest {
|
|
1680
|
-
LiveStateScanRequest {
|
|
1681
|
-
filter: LiveStateFilter {
|
|
1682
|
-
schema_keys: vec!["lix_key_value".to_string()],
|
|
1683
|
-
entity_ids: vec![crate::entity_identity::EntityIdentity::single(key)],
|
|
1684
|
-
version_ids: vec!["global".to_string()],
|
|
1685
|
-
file_ids: vec![NullableKeyFilter::Null],
|
|
1686
|
-
include_tombstones,
|
|
1687
|
-
..LiveStateFilter::default()
|
|
1688
|
-
},
|
|
1689
|
-
..LiveStateScanRequest::default()
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
trait StateRowTestExt {
|
|
1694
|
-
fn with_schema(self, schema_key: &str) -> Self;
|
|
1695
|
-
fn with_file_id(self, file_id: &str) -> Self;
|
|
1696
|
-
fn with_tracked(self) -> Self;
|
|
1697
|
-
fn with_version(self, version_id: &str) -> Self;
|
|
1698
|
-
fn with_change_id(self, change_id: &str) -> Self;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
impl StateRowTestExt for PreparedStateRow {
|
|
1702
|
-
fn with_schema(mut self, schema_key: &str) -> Self {
|
|
1703
|
-
self.schema_key = schema_key.to_string();
|
|
1704
|
-
self
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
fn with_file_id(mut self, file_id: &str) -> Self {
|
|
1708
|
-
self.file_id = Some(file_id.to_string());
|
|
1709
|
-
self
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
fn with_tracked(mut self) -> Self {
|
|
1713
|
-
self.untracked = false;
|
|
1714
|
-
if self.change_id.is_none() {
|
|
1715
|
-
self.change_id = Some("test-change-id".to_string());
|
|
1716
|
-
}
|
|
1717
|
-
self
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
fn with_version(mut self, version_id: &str) -> Self {
|
|
1721
|
-
self.version_id = version_id.to_string();
|
|
1722
|
-
self.global = version_id == GLOBAL_VERSION_ID;
|
|
1723
|
-
self
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
fn with_change_id(mut self, change_id: &str) -> Self {
|
|
1727
|
-
self.change_id = Some(change_id.to_string());
|
|
1728
|
-
self
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
}
|