@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.
Files changed (223) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +39 -201
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +20 -50
  16. package/SKILL.md +0 -506
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -821
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -26
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -303
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/kv.rs +0 -358
  31. package/dist-engine-src/src/backend/mod.rs +0 -12
  32. package/dist-engine-src/src/backend/testing.rs +0 -658
  33. package/dist-engine-src/src/backend/types.rs +0 -96
  34. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  35. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  36. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  37. package/dist-engine-src/src/binary_cas/kv.rs +0 -1063
  38. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  39. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  40. package/dist-engine-src/src/catalog/context.rs +0 -412
  41. package/dist-engine-src/src/catalog/mod.rs +0 -10
  42. package/dist-engine-src/src/catalog/schema.rs +0 -4
  43. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  44. package/dist-engine-src/src/cel/context.rs +0 -86
  45. package/dist-engine-src/src/cel/error.rs +0 -19
  46. package/dist-engine-src/src/cel/mod.rs +0 -8
  47. package/dist-engine-src/src/cel/provider.rs +0 -9
  48. package/dist-engine-src/src/cel/runtime.rs +0 -167
  49. package/dist-engine-src/src/cel/value.rs +0 -50
  50. package/dist-engine-src/src/commit_graph/context.rs +0 -901
  51. package/dist-engine-src/src/commit_graph/mod.rs +0 -11
  52. package/dist-engine-src/src/commit_graph/types.rs +0 -109
  53. package/dist-engine-src/src/commit_graph/walker.rs +0 -756
  54. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  55. package/dist-engine-src/src/commit_store/context.rs +0 -944
  56. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  57. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  58. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  59. package/dist-engine-src/src/commit_store/types.rs +0 -215
  60. package/dist-engine-src/src/common/error.rs +0 -313
  61. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  62. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  63. package/dist-engine-src/src/common/identity.rs +0 -145
  64. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  65. package/dist-engine-src/src/common/metadata.rs +0 -40
  66. package/dist-engine-src/src/common/mod.rs +0 -23
  67. package/dist-engine-src/src/common/types.rs +0 -105
  68. package/dist-engine-src/src/common/wire.rs +0 -222
  69. package/dist-engine-src/src/domain.rs +0 -324
  70. package/dist-engine-src/src/engine.rs +0 -225
  71. package/dist-engine-src/src/entity_identity.rs +0 -405
  72. package/dist-engine-src/src/functions/context.rs +0 -292
  73. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  74. package/dist-engine-src/src/functions/mod.rs +0 -18
  75. package/dist-engine-src/src/functions/provider.rs +0 -130
  76. package/dist-engine-src/src/functions/state.rs +0 -336
  77. package/dist-engine-src/src/functions/types.rs +0 -37
  78. package/dist-engine-src/src/init.rs +0 -558
  79. package/dist-engine-src/src/json_store/compression.rs +0 -77
  80. package/dist-engine-src/src/json_store/context.rs +0 -423
  81. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  82. package/dist-engine-src/src/json_store/mod.rs +0 -12
  83. package/dist-engine-src/src/json_store/store.rs +0 -1109
  84. package/dist-engine-src/src/json_store/types.rs +0 -217
  85. package/dist-engine-src/src/lib.rs +0 -62
  86. package/dist-engine-src/src/live_state/context.rs +0 -2019
  87. package/dist-engine-src/src/live_state/mod.rs +0 -15
  88. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  89. package/dist-engine-src/src/live_state/reader.rs +0 -23
  90. package/dist-engine-src/src/live_state/types.rs +0 -222
  91. package/dist-engine-src/src/live_state/visibility.rs +0 -223
  92. package/dist-engine-src/src/plugin/archive.rs +0 -438
  93. package/dist-engine-src/src/plugin/component.rs +0 -183
  94. package/dist-engine-src/src/plugin/install.rs +0 -619
  95. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  96. package/dist-engine-src/src/plugin/materializer.rs +0 -477
  97. package/dist-engine-src/src/plugin/mod.rs +0 -33
  98. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -118
  99. package/dist-engine-src/src/plugin/storage.rs +0 -74
  100. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  101. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  102. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  103. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  104. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  105. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  106. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  107. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  108. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  109. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  110. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  111. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  112. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  113. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  114. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  115. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  116. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  117. package/dist-engine-src/src/schema/builtin/mod.rs +0 -222
  118. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  119. package/dist-engine-src/src/schema/definition.json +0 -187
  120. package/dist-engine-src/src/schema/definition.rs +0 -742
  121. package/dist-engine-src/src/schema/key.rs +0 -138
  122. package/dist-engine-src/src/schema/mod.rs +0 -20
  123. package/dist-engine-src/src/schema/seed.rs +0 -14
  124. package/dist-engine-src/src/schema/tests.rs +0 -780
  125. package/dist-engine-src/src/session/context.rs +0 -404
  126. package/dist-engine-src/src/session/create_version.rs +0 -88
  127. package/dist-engine-src/src/session/execute.rs +0 -541
  128. package/dist-engine-src/src/session/merge/analysis.rs +0 -102
  129. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  130. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  131. package/dist-engine-src/src/session/merge/mod.rs +0 -11
  132. package/dist-engine-src/src/session/merge/stats.rs +0 -65
  133. package/dist-engine-src/src/session/merge/version.rs +0 -427
  134. package/dist-engine-src/src/session/mod.rs +0 -27
  135. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  136. package/dist-engine-src/src/session/switch_version.rs +0 -110
  137. package/dist-engine-src/src/session/transaction.rs +0 -76
  138. package/dist-engine-src/src/sql2/change_provider.rs +0 -331
  139. package/dist-engine-src/src/sql2/classify.rs +0 -174
  140. package/dist-engine-src/src/sql2/context.rs +0 -311
  141. package/dist-engine-src/src/sql2/directory_history_provider.rs +0 -631
  142. package/dist-engine-src/src/sql2/directory_provider.rs +0 -2453
  143. package/dist-engine-src/src/sql2/dml.rs +0 -148
  144. package/dist-engine-src/src/sql2/entity_history_provider.rs +0 -440
  145. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  146. package/dist-engine-src/src/sql2/error.rs +0 -215
  147. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  148. package/dist-engine-src/src/sql2/file_history_provider.rs +0 -910
  149. package/dist-engine-src/src/sql2/file_provider.rs +0 -3679
  150. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1490
  151. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  152. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  153. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  154. package/dist-engine-src/src/sql2/history_provider.rs +0 -412
  155. package/dist-engine-src/src/sql2/history_route.rs +0 -657
  156. package/dist-engine-src/src/sql2/lix_state_provider.rs +0 -2512
  157. package/dist-engine-src/src/sql2/mod.rs +0 -47
  158. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -246
  159. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  160. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  161. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  162. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  163. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  164. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  165. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  166. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  167. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  168. package/dist-engine-src/src/sql2/session.rs +0 -132
  169. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  170. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +0 -53
  171. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  172. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  173. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  174. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  175. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  176. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  177. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  178. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  179. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -89
  180. package/dist-engine-src/src/sql2/udfs/public_call.rs +0 -238
  181. package/dist-engine-src/src/sql2/version_provider.rs +0 -1202
  182. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  183. package/dist-engine-src/src/sql2/write_normalization.rs +0 -345
  184. package/dist-engine-src/src/storage/context.rs +0 -356
  185. package/dist-engine-src/src/storage/mod.rs +0 -14
  186. package/dist-engine-src/src/storage/read_scope.rs +0 -88
  187. package/dist-engine-src/src/storage/types.rs +0 -501
  188. package/dist-engine-src/src/storage_bench.rs +0 -4863
  189. package/dist-engine-src/src/test_support.rs +0 -228
  190. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  191. package/dist-engine-src/src/tracked_state/codec.rs +0 -2085
  192. package/dist-engine-src/src/tracked_state/context.rs +0 -1867
  193. package/dist-engine-src/src/tracked_state/diff.rs +0 -686
  194. package/dist-engine-src/src/tracked_state/materialization.rs +0 -403
  195. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  196. package/dist-engine-src/src/tracked_state/merge.rs +0 -492
  197. package/dist-engine-src/src/tracked_state/mod.rs +0 -32
  198. package/dist-engine-src/src/tracked_state/storage.rs +0 -375
  199. package/dist-engine-src/src/tracked_state/tree.rs +0 -3187
  200. package/dist-engine-src/src/tracked_state/types.rs +0 -231
  201. package/dist-engine-src/src/transaction/commit.rs +0 -1484
  202. package/dist-engine-src/src/transaction/context.rs +0 -1548
  203. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  204. package/dist-engine-src/src/transaction/mod.rs +0 -13
  205. package/dist-engine-src/src/transaction/normalization.rs +0 -890
  206. package/dist-engine-src/src/transaction/prep.rs +0 -37
  207. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -149
  208. package/dist-engine-src/src/transaction/staging.rs +0 -1731
  209. package/dist-engine-src/src/transaction/types.rs +0 -460
  210. package/dist-engine-src/src/transaction/validation.rs +0 -5830
  211. package/dist-engine-src/src/untracked_state/codec.rs +0 -307
  212. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  213. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  214. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  215. package/dist-engine-src/src/untracked_state/storage.rs +0 -396
  216. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  217. package/dist-engine-src/src/version/context.rs +0 -40
  218. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  219. package/dist-engine-src/src/version/mod.rs +0 -13
  220. package/dist-engine-src/src/version/refs.rs +0 -330
  221. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  222. package/dist-engine-src/src/version/types.rs +0 -21
  223. 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
- }