@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.4

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 (169) hide show
  1. package/SKILL.md +46 -8
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +25 -1
  3. package/dist/engine-wasm/wasm/lix_engine.js +60 -2
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +5 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +10 -3
  9. package/dist/open-lix.js +39 -0
  10. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  11. package/dist-engine-src/src/catalog/context.rs +412 -0
  12. package/dist-engine-src/src/catalog/mod.rs +10 -0
  13. package/dist-engine-src/src/catalog/schema.rs +4 -0
  14. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  15. package/dist-engine-src/src/cel/mod.rs +1 -1
  16. package/dist-engine-src/src/cel/provider.rs +1 -1
  17. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  18. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  19. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  20. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  21. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  22. package/dist-engine-src/src/commit_store/context.rs +944 -0
  23. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  24. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  25. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  26. package/dist-engine-src/src/commit_store/types.rs +215 -0
  27. package/dist-engine-src/src/common/identity.rs +15 -5
  28. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  29. package/dist-engine-src/src/common/metadata.rs +17 -12
  30. package/dist-engine-src/src/common/mod.rs +5 -5
  31. package/dist-engine-src/src/domain.rs +324 -0
  32. package/dist-engine-src/src/engine.rs +29 -43
  33. package/dist-engine-src/src/entity_identity.rs +238 -118
  34. package/dist-engine-src/src/functions/context.rs +17 -52
  35. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  36. package/dist-engine-src/src/functions/mod.rs +1 -1
  37. package/dist-engine-src/src/functions/provider.rs +4 -4
  38. package/dist-engine-src/src/functions/state.rs +39 -66
  39. package/dist-engine-src/src/functions/types.rs +1 -1
  40. package/dist-engine-src/src/init.rs +204 -151
  41. package/dist-engine-src/src/json_store/context.rs +354 -60
  42. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  43. package/dist-engine-src/src/json_store/mod.rs +4 -1
  44. package/dist-engine-src/src/json_store/store.rs +884 -11
  45. package/dist-engine-src/src/json_store/types.rs +166 -1
  46. package/dist-engine-src/src/lib.rs +11 -10
  47. package/dist-engine-src/src/live_state/context.rs +608 -830
  48. package/dist-engine-src/src/live_state/mod.rs +3 -3
  49. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  50. package/dist-engine-src/src/live_state/reader.rs +5 -5
  51. package/dist-engine-src/src/live_state/types.rs +19 -36
  52. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  53. package/dist-engine-src/src/plugin/archive.rs +3 -6
  54. package/dist-engine-src/src/plugin/install.rs +0 -18
  55. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  56. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  57. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  58. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  60. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  61. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  63. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  64. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  65. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  66. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  67. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  68. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  69. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  70. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  71. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  72. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  73. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  74. package/dist-engine-src/src/schema/definition.json +47 -17
  75. package/dist-engine-src/src/schema/definition.rs +202 -96
  76. package/dist-engine-src/src/schema/key.rs +9 -77
  77. package/dist-engine-src/src/schema/mod.rs +4 -4
  78. package/dist-engine-src/src/schema/tests.rs +133 -92
  79. package/dist-engine-src/src/session/context.rs +86 -48
  80. package/dist-engine-src/src/session/create_version.rs +22 -14
  81. package/dist-engine-src/src/session/execute.rs +117 -23
  82. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  83. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  84. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  85. package/dist-engine-src/src/session/merge/version.rs +35 -45
  86. package/dist-engine-src/src/session/mod.rs +9 -7
  87. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  88. package/dist-engine-src/src/session/switch_version.rs +17 -28
  89. package/dist-engine-src/src/session/transaction.rs +76 -0
  90. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  91. package/dist-engine-src/src/sql2/classify.rs +75 -48
  92. package/dist-engine-src/src/sql2/context.rs +22 -18
  93. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  94. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  95. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  96. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  97. package/dist-engine-src/src/sql2/error.rs +24 -5
  98. package/dist-engine-src/src/sql2/execute.rs +426 -272
  99. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  100. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  101. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  102. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  103. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  104. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  105. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  106. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  107. package/dist-engine-src/src/sql2/mod.rs +8 -4
  108. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  109. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  110. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  111. package/dist-engine-src/src/sql2/public_bind/dml.rs +172 -0
  112. package/dist-engine-src/src/sql2/public_bind/mod.rs +26 -0
  113. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  114. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  115. package/dist-engine-src/src/sql2/session.rs +7 -10
  116. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  117. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  118. package/dist-engine-src/src/sql2/udfs/public_call.rs +238 -0
  119. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  120. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  121. package/dist-engine-src/src/storage_bench.rs +1782 -325
  122. package/dist-engine-src/src/test_support.rs +183 -36
  123. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  124. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  125. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  126. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  127. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  128. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  129. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  130. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  131. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  132. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  133. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  134. package/dist-engine-src/src/transaction/commit.rs +695 -435
  135. package/dist-engine-src/src/transaction/context.rs +551 -310
  136. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  137. package/dist-engine-src/src/transaction/mod.rs +2 -0
  138. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  139. package/dist-engine-src/src/transaction/prep.rs +37 -0
  140. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  141. package/dist-engine-src/src/transaction/staging.rs +701 -406
  142. package/dist-engine-src/src/transaction/types.rs +231 -122
  143. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  144. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  145. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  146. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  147. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  148. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  149. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  150. package/dist-engine-src/src/version/context.rs +1 -13
  151. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  152. package/dist-engine-src/src/version/mod.rs +3 -2
  153. package/dist-engine-src/src/version/refs.rs +12 -103
  154. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  155. package/package.json +1 -1
  156. package/dist-engine-src/src/changelog/codec.rs +0 -321
  157. package/dist-engine-src/src/changelog/context.rs +0 -92
  158. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  159. package/dist-engine-src/src/changelog/mod.rs +0 -13
  160. package/dist-engine-src/src/changelog/reader.rs +0 -20
  161. package/dist-engine-src/src/changelog/storage.rs +0 -220
  162. package/dist-engine-src/src/changelog/types.rs +0 -38
  163. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  164. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  165. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  166. package/dist-engine-src/src/schema_registry.rs +0 -294
  167. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  168. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  169. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,13 +1,13 @@
1
- use crate::json_store::JsonStoreContext;
2
1
  use crate::storage::KvScanRange;
3
2
  use crate::storage::{KvGetGroup, KvGetRequest, KvScanRequest, StorageReader, StorageWriteSet};
4
3
  use crate::untracked_state::{
5
4
  MaterializedUntrackedStateRow, UntrackedMaterializationProjection, UntrackedStateIdentity,
6
- UntrackedStateRow, UntrackedStateRowRequest, UntrackedStateScanRequest,
5
+ UntrackedStateIdentityRef, UntrackedStateRow, UntrackedStateRowRef, UntrackedStateRowRequest,
6
+ UntrackedStateScanRequest,
7
7
  };
8
8
  use crate::{LixError, NullableKeyFilter};
9
9
 
10
- const UNTRACKED_STATE_ROW_NAMESPACE: &str = "untracked_state.row";
10
+ pub(super) const UNTRACKED_STATE_ROW_NAMESPACE: &str = "untracked_state.row";
11
11
 
12
12
  pub(crate) async fn scan_rows(
13
13
  store: &mut impl StorageReader,
@@ -19,12 +19,9 @@ pub(crate) async fn scan_rows(
19
19
  rows.truncate(limit);
20
20
  }
21
21
  let projection = UntrackedMaterializationProjection::from_columns(&request.projection.columns);
22
- let mut json_reader = JsonStoreContext::new().reader(store);
23
22
  let mut materialized = Vec::with_capacity(rows.len());
24
23
  for row in rows {
25
- materialized.push(
26
- crate::untracked_state::materialize_row(&mut json_reader, row, &projection).await?,
27
- );
24
+ materialized.push(crate::untracked_state::materialize_row(row, &projection)?);
28
25
  }
29
26
  Ok(materialized)
30
27
  }
@@ -52,46 +49,98 @@ pub(crate) async fn load_row(
52
49
  return Ok(None);
53
50
  };
54
51
  let row = crate::untracked_state::codec::decode_row(&bytes)?;
55
- let mut json_reader = JsonStoreContext::new().reader(store);
56
- crate::untracked_state::materialize_row(
57
- &mut json_reader,
58
- row,
59
- &UntrackedMaterializationProjection::full(),
60
- )
61
- .await
62
- .map(Some)
52
+ crate::untracked_state::materialize_row(row, &UntrackedMaterializationProjection::full())
53
+ .map(Some)
54
+ }
55
+
56
+ pub(super) async fn existing_identities<'a>(
57
+ store: &mut (impl StorageReader + ?Sized),
58
+ identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
59
+ ) -> Result<Vec<UntrackedStateIdentity>, LixError> {
60
+ let mut candidates = identities
61
+ .into_iter()
62
+ .map(|identity| {
63
+ let owned = UntrackedStateIdentity {
64
+ version_id: identity.version_id.to_string(),
65
+ schema_key: identity.schema_key.to_string(),
66
+ entity_id: identity.entity_id.clone(),
67
+ file_id: identity.file_id.map(str::to_string),
68
+ };
69
+ let key = encode_untracked_state_row_key_ref(owned.as_ref());
70
+ (key, owned)
71
+ })
72
+ .collect::<Vec<_>>();
73
+ candidates.sort_by(|(left, _), (right, _)| left.cmp(right));
74
+ candidates.dedup_by(|(left, _), (right, _)| left == right);
75
+ if candidates.is_empty() {
76
+ return Ok(Vec::new());
77
+ }
78
+ let keys = candidates
79
+ .iter()
80
+ .map(|(key, _)| key.clone())
81
+ .collect::<Vec<_>>();
82
+
83
+ let result = store
84
+ .exists_many(KvGetRequest {
85
+ groups: vec![KvGetGroup {
86
+ namespace: UNTRACKED_STATE_ROW_NAMESPACE.to_string(),
87
+ keys,
88
+ }],
89
+ })
90
+ .await?;
91
+ let group = result.groups.into_iter().next().ok_or_else(|| {
92
+ LixError::new(
93
+ LixError::CODE_INTERNAL_ERROR,
94
+ "untracked identity existence probe returned no result group",
95
+ )
96
+ })?;
97
+ if group.exists.len() != candidates.len() {
98
+ return Err(LixError::new(
99
+ LixError::CODE_INTERNAL_ERROR,
100
+ format!(
101
+ "untracked identity existence probe returned {} results for {} requested keys",
102
+ group.exists.len(),
103
+ candidates.len()
104
+ ),
105
+ ));
106
+ }
107
+
108
+ Ok(candidates
109
+ .into_iter()
110
+ .zip(group.exists)
111
+ .filter_map(|((_, identity), exists)| exists.then_some(identity))
112
+ .collect())
63
113
  }
64
114
 
65
- pub(crate) fn stage_rows(
66
- writes: &mut StorageWriteSet,
67
- rows: &[UntrackedStateRow],
68
- ) -> Result<(), LixError> {
115
+ pub(crate) fn stage_rows<'a, I>(writes: &mut StorageWriteSet, rows: I) -> Result<(), LixError>
116
+ where
117
+ I: IntoIterator<Item = UntrackedStateRowRef<'a>>,
118
+ {
69
119
  for row in rows {
70
- let identity = UntrackedStateIdentity::from_row(row);
71
- if row.snapshot_ref.is_none() {
120
+ if row.snapshot_content.is_none() {
72
121
  writes.delete(
73
122
  UNTRACKED_STATE_ROW_NAMESPACE,
74
- encode_untracked_state_row_key(&identity),
123
+ encode_untracked_state_row_key_ref(row.into()),
75
124
  );
76
125
  } else {
77
126
  writes.put(
78
127
  UNTRACKED_STATE_ROW_NAMESPACE,
79
- encode_untracked_state_row_key(&identity),
80
- crate::untracked_state::codec::encode_row(row)?,
128
+ encode_untracked_state_row_key_ref(row.into()),
129
+ crate::untracked_state::codec::encode_row_ref(row)?,
81
130
  );
82
131
  }
83
132
  }
84
133
  Ok(())
85
134
  }
86
135
 
87
- pub(crate) fn stage_delete_rows(
88
- writes: &mut StorageWriteSet,
89
- identities: &[UntrackedStateIdentity],
90
- ) {
136
+ pub(crate) fn stage_delete_rows<'a, I>(writes: &mut StorageWriteSet, identities: I)
137
+ where
138
+ I: IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
139
+ {
91
140
  for identity in identities {
92
141
  writes.delete(
93
142
  UNTRACKED_STATE_ROW_NAMESPACE,
94
- encode_untracked_state_row_key(identity),
143
+ encode_untracked_state_row_key_ref(identity),
95
144
  );
96
145
  }
97
146
  }
@@ -146,17 +195,21 @@ fn identity_from_request(request: &UntrackedStateRowRequest) -> Option<Untracked
146
195
  }
147
196
 
148
197
  fn encode_untracked_state_row_key(identity: &UntrackedStateIdentity) -> Vec<u8> {
198
+ encode_untracked_state_row_key_ref(identity.as_ref())
199
+ }
200
+
201
+ pub(super) fn encode_untracked_state_row_key_ref(
202
+ identity: UntrackedStateIdentityRef<'_>,
203
+ ) -> Vec<u8> {
149
204
  let mut out = Vec::new();
150
- push_component(&mut out, &identity.version_id);
151
- push_component(&mut out, &identity.schema_key);
152
- push_component(
153
- &mut out,
154
- &identity
155
- .entity_id
156
- .as_string()
157
- .expect("untracked-state identity should project"),
158
- );
159
- match &identity.file_id {
205
+ push_component(&mut out, identity.version_id);
206
+ push_component(&mut out, identity.schema_key);
207
+ let entity_id = identity
208
+ .entity_id
209
+ .as_json_array_text()
210
+ .expect("untracked-state identity should project");
211
+ push_component(&mut out, &entity_id);
212
+ match identity.file_id {
160
213
  Some(file_id) => {
161
214
  out.push(1);
162
215
  push_component(&mut out, file_id);
@@ -179,7 +232,7 @@ mod tests {
179
232
  use super::*;
180
233
  use crate::backend::testing::UnitTestBackend;
181
234
  use crate::storage::{StorageContext, StorageWriteTransaction};
182
- use crate::untracked_state::{canonicalize_materialized_row, UntrackedStateContext};
235
+ use crate::untracked_state::UntrackedStateContext;
183
236
 
184
237
  async fn write_materialized_rows_to_store(
185
238
  context: &UntrackedStateContext,
@@ -187,16 +240,14 @@ mod tests {
187
240
  rows: &[MaterializedUntrackedStateRow],
188
241
  ) {
189
242
  let mut writes = StorageWriteSet::new();
190
- let canonical_rows = {
191
- let mut json_writer = JsonStoreContext::new().writer();
192
- rows.iter()
193
- .map(|row| canonicalize_materialized_row(&mut writes, &mut json_writer, row))
194
- .collect::<Result<Vec<_>, _>>()
195
- .expect("rows should canonicalize")
196
- };
243
+ let canonical_rows = rows
244
+ .iter()
245
+ .map(|row| crate::test_support::untracked_state_row_from_materialized(&mut writes, row))
246
+ .collect::<Result<Vec<_>, _>>()
247
+ .expect("rows should canonicalize");
197
248
  context
198
249
  .writer(&mut writes)
199
- .stage_rows(&canonical_rows)
250
+ .stage_rows(canonical_rows.iter().map(|row| row.as_ref()))
200
251
  .expect("rows should write");
201
252
  writes.apply(store).await.expect("rows should apply");
202
253
  }
@@ -290,22 +341,19 @@ mod tests {
290
341
  entity_id: row.entity_id.clone(),
291
342
  file_id: row.file_id.clone(),
292
343
  };
293
-
294
344
  let mut transaction = storage
295
345
  .begin_write_transaction()
296
346
  .await
297
347
  .expect("transaction should open");
298
348
  let mut writes = StorageWriteSet::new();
299
- let canonical_row = {
300
- let mut json_writer = JsonStoreContext::new().writer();
301
- canonicalize_materialized_row(&mut writes, &mut json_writer, &row)
302
- .expect("row should canonicalize")
303
- };
349
+ let canonical_row =
350
+ crate::test_support::untracked_state_row_from_materialized(&mut writes, &row)
351
+ .expect("row should canonicalize");
304
352
  let mut writer = context.writer(&mut writes);
305
353
  writer
306
- .stage_rows(&[canonical_row])
354
+ .stage_rows(std::iter::once(canonical_row.as_ref()))
307
355
  .expect("write should succeed");
308
- writer.stage_delete_rows(&[identity]);
356
+ writer.stage_delete_rows(std::iter::once(identity.as_ref()));
309
357
  writes
310
358
  .apply(&mut transaction.as_mut())
311
359
  .await
@@ -338,7 +386,7 @@ mod tests {
338
386
  file_id: None,
339
387
  snapshot_content: Some(format!("{{\"key\":\"{}\",\"value\":\"value\"}}", entity_id)),
340
388
  metadata: None,
341
- schema_version: "1".to_string(),
389
+ deleted: false,
342
390
  created_at: "2026-01-01T00:00:00Z".to_string(),
343
391
  updated_at: "2026-01-01T00:00:00Z".to_string(),
344
392
  global: version_id == "global",
@@ -1,25 +1,56 @@
1
1
  use crate::entity_identity::EntityIdentity;
2
- use crate::json_store::JsonRef;
3
- use crate::{NullableKeyFilter, RowMetadata};
2
+ use crate::NullableKeyFilter;
4
3
 
5
4
  /// Durable local row excluded from changelog and commit membership.
6
5
  ///
7
6
  /// This is the canonical physical shape: identity/header fields are stored
8
- /// directly, while JSON payloads live in json_store and are referenced by hash.
7
+ /// directly, and mutable JSON payloads are stored inline in the sidecar row.
9
8
  #[derive(Debug, Clone, PartialEq, Eq)]
10
9
  pub(crate) struct UntrackedStateRow {
11
10
  pub(crate) entity_id: EntityIdentity,
12
11
  pub(crate) schema_key: String,
13
12
  pub(crate) file_id: Option<String>,
14
- pub(crate) snapshot_ref: Option<JsonRef>,
15
- pub(crate) metadata_ref: Option<JsonRef>,
16
- pub(crate) schema_version: String,
13
+ pub(crate) snapshot_content: Option<String>,
14
+ pub(crate) metadata: Option<String>,
17
15
  pub(crate) created_at: String,
18
16
  pub(crate) updated_at: String,
19
17
  pub(crate) global: bool,
20
18
  pub(crate) version_id: String,
21
19
  }
22
20
 
21
+ impl UntrackedStateRow {
22
+ pub(crate) fn as_ref(&self) -> UntrackedStateRowRef<'_> {
23
+ UntrackedStateRowRef {
24
+ entity_id: &self.entity_id,
25
+ schema_key: &self.schema_key,
26
+ file_id: self.file_id.as_deref(),
27
+ snapshot_content: self.snapshot_content.as_deref(),
28
+ metadata: self.metadata.as_deref(),
29
+ created_at: &self.created_at,
30
+ updated_at: &self.updated_at,
31
+ global: self.global,
32
+ version_id: &self.version_id,
33
+ }
34
+ }
35
+ }
36
+
37
+ /// Zero-copy view of untracked-state write row.
38
+ ///
39
+ /// Untracked state owns this storage-facing write shape. Callers adapt into it
40
+ /// without making untracked_state depend on transaction or live-state types.
41
+ #[derive(Debug, Clone, Copy)]
42
+ pub(crate) struct UntrackedStateRowRef<'a> {
43
+ pub(crate) entity_id: &'a EntityIdentity,
44
+ pub(crate) schema_key: &'a str,
45
+ pub(crate) file_id: Option<&'a str>,
46
+ pub(crate) snapshot_content: Option<&'a str>,
47
+ pub(crate) metadata: Option<&'a str>,
48
+ pub(crate) created_at: &'a str,
49
+ pub(crate) updated_at: &'a str,
50
+ pub(crate) global: bool,
51
+ pub(crate) version_id: &'a str,
52
+ }
53
+
23
54
  /// Hydrated boundary shape for callers that still work with JSON payloads.
24
55
  #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25
56
  pub(crate) struct MaterializedUntrackedStateRow {
@@ -27,8 +58,8 @@ pub(crate) struct MaterializedUntrackedStateRow {
27
58
  pub(crate) schema_key: String,
28
59
  pub(crate) file_id: Option<String>,
29
60
  pub(crate) snapshot_content: Option<String>,
30
- pub(crate) metadata: Option<RowMetadata>,
31
- pub(crate) schema_version: String,
61
+ pub(crate) metadata: Option<String>,
62
+ pub(crate) deleted: bool,
32
63
  pub(crate) created_at: String,
33
64
  pub(crate) updated_at: String,
34
65
  pub(crate) global: bool,
@@ -44,13 +75,32 @@ pub(crate) struct UntrackedStateIdentity {
44
75
  pub(crate) file_id: Option<String>,
45
76
  }
46
77
 
78
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
79
+ pub(crate) struct UntrackedStateIdentityRef<'a> {
80
+ pub(crate) version_id: &'a str,
81
+ pub(crate) schema_key: &'a str,
82
+ pub(crate) entity_id: &'a EntityIdentity,
83
+ pub(crate) file_id: Option<&'a str>,
84
+ }
85
+
47
86
  impl UntrackedStateIdentity {
48
- pub(crate) fn from_row(row: &UntrackedStateRow) -> Self {
87
+ pub(crate) fn as_ref(&self) -> UntrackedStateIdentityRef<'_> {
88
+ UntrackedStateIdentityRef {
89
+ version_id: &self.version_id,
90
+ schema_key: &self.schema_key,
91
+ entity_id: &self.entity_id,
92
+ file_id: self.file_id.as_deref(),
93
+ }
94
+ }
95
+ }
96
+
97
+ impl<'a> From<UntrackedStateRowRef<'a>> for UntrackedStateIdentityRef<'a> {
98
+ fn from(row: UntrackedStateRowRef<'a>) -> Self {
49
99
  Self {
50
- version_id: row.version_id.clone(),
51
- schema_key: row.schema_key.clone(),
52
- entity_id: row.entity_id.clone(),
53
- file_id: row.file_id.clone(),
100
+ version_id: row.version_id,
101
+ schema_key: row.schema_key,
102
+ entity_id: row.entity_id,
103
+ file_id: row.file_id,
54
104
  }
55
105
  }
56
106
  }
@@ -1,10 +1,9 @@
1
1
  use std::sync::Arc;
2
2
 
3
- use crate::json_store::JsonStoreWriter;
4
3
  use crate::storage::{StorageReader, StorageWriteSet};
5
4
  use crate::untracked_state::{UntrackedStateContext, UntrackedStateRow};
6
5
 
7
- use super::refs::{canonical_version_ref_row, VersionRefContext};
6
+ use super::refs::VersionRefContext;
8
7
  use super::VersionRefReader;
9
8
 
10
9
  /// Aggregate entrypoint for version-domain services.
@@ -31,17 +30,6 @@ impl VersionContext {
31
30
  self.refs.reader(store)
32
31
  }
33
32
 
34
- pub(crate) fn canonical_ref_row(
35
- &self,
36
- writes: &mut StorageWriteSet,
37
- json_writer: &mut JsonStoreWriter,
38
- version_id: &str,
39
- commit_id: &str,
40
- timestamp: &str,
41
- ) -> Result<UntrackedStateRow, crate::LixError> {
42
- canonical_version_ref_row(writes, json_writer, version_id, commit_id, timestamp)
43
- }
44
-
45
33
  pub(crate) fn stage_canonical_ref_rows(
46
34
  &self,
47
35
  writes: &mut StorageWriteSet,
@@ -0,0 +1,221 @@
1
+ use crate::commit_graph::{CommitGraphCommit, CommitGraphReader};
2
+ use crate::common::validate_non_empty_identity_value;
3
+ use crate::LixError;
4
+
5
+ use super::{VersionHead, VersionRefReader};
6
+
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ pub(crate) enum VersionOperation {
9
+ CreateVersion,
10
+ SwitchVersion,
11
+ MergeVersion,
12
+ MergeVersionPreview,
13
+ LoadWorkspaceSelector,
14
+ }
15
+
16
+ impl VersionOperation {
17
+ pub(crate) fn label(self) -> &'static str {
18
+ match self {
19
+ Self::CreateVersion => "create_version",
20
+ Self::SwitchVersion => "switch_version",
21
+ Self::MergeVersion => "merge_version",
22
+ Self::MergeVersionPreview => "merge_version_preview",
23
+ Self::LoadWorkspaceSelector => "load_workspace_version_id",
24
+ }
25
+ }
26
+ }
27
+
28
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
+ pub(crate) enum VersionReferenceRole {
30
+ Source,
31
+ Target,
32
+ WorkspaceSelector,
33
+ CommitSource,
34
+ }
35
+
36
+ impl VersionReferenceRole {
37
+ pub(crate) fn label(self) -> &'static str {
38
+ match self {
39
+ Self::Source => "source",
40
+ Self::Target => "target",
41
+ Self::WorkspaceSelector => "workspace_selector",
42
+ Self::CommitSource => "commit_source",
43
+ }
44
+ }
45
+ }
46
+
47
+ /// Shared domain service for resolving public version references.
48
+ ///
49
+ /// Built-in version schemas describe row shape. This service owns semantic
50
+ /// ref validation: non-empty ids, global sentinel handling, and missing refs.
51
+ pub(crate) struct VersionLifecycle<'a> {
52
+ refs: &'a dyn VersionRefReader,
53
+ }
54
+
55
+ impl<'a> VersionLifecycle<'a> {
56
+ pub(crate) fn new(refs: &'a dyn VersionRefReader) -> Self {
57
+ Self { refs }
58
+ }
59
+
60
+ pub(crate) fn require_non_empty_id(
61
+ version_id: &str,
62
+ operation: VersionOperation,
63
+ role: VersionReferenceRole,
64
+ ) -> Result<(), LixError> {
65
+ require_non_empty_public_id("version_id", version_id, operation, role)
66
+ }
67
+
68
+ pub(crate) async fn require_existing_commit(
69
+ commit_graph: &mut dyn CommitGraphReader,
70
+ commit_id: &str,
71
+ operation: VersionOperation,
72
+ role: VersionReferenceRole,
73
+ ) -> Result<CommitGraphCommit, LixError> {
74
+ require_non_empty_public_id("commit_id", commit_id, operation, role)?;
75
+ commit_graph
76
+ .load_commit(commit_id)
77
+ .await?
78
+ .ok_or_else(|| LixError::version_not_found(commit_id, operation.label(), role.label()))
79
+ }
80
+
81
+ pub(crate) async fn require_existing_ref(
82
+ &self,
83
+ version_id: &str,
84
+ operation: VersionOperation,
85
+ role: VersionReferenceRole,
86
+ ) -> Result<VersionHead, LixError> {
87
+ Self::require_non_empty_id(version_id, operation, role)?;
88
+ self.require_existing_stored_ref(version_id, operation, role)
89
+ .await
90
+ }
91
+
92
+ pub(crate) async fn require_existing_commit_id(
93
+ &self,
94
+ version_id: &str,
95
+ operation: VersionOperation,
96
+ role: VersionReferenceRole,
97
+ ) -> Result<String, LixError> {
98
+ Ok(self
99
+ .require_existing_ref(version_id, operation, role)
100
+ .await?
101
+ .commit_id)
102
+ }
103
+
104
+ async fn require_existing_stored_ref(
105
+ &self,
106
+ version_id: &str,
107
+ operation: VersionOperation,
108
+ role: VersionReferenceRole,
109
+ ) -> Result<VersionHead, LixError> {
110
+ self.refs
111
+ .load_head(version_id)
112
+ .await?
113
+ .ok_or_else(|| LixError::version_not_found(version_id, operation.label(), role.label()))
114
+ }
115
+ }
116
+
117
+ fn require_non_empty_public_id(
118
+ label: &str,
119
+ value: &str,
120
+ operation: VersionOperation,
121
+ role: VersionReferenceRole,
122
+ ) -> Result<(), LixError> {
123
+ validate_non_empty_identity_value(label, value)
124
+ .map(|_| ())
125
+ .map_err(|_| {
126
+ LixError::new(
127
+ LixError::CODE_INVALID_PARAM,
128
+ format!(
129
+ "{} {} {label} must be non-empty",
130
+ operation.label(),
131
+ role.label()
132
+ ),
133
+ )
134
+ })
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use async_trait::async_trait;
140
+
141
+ use super::*;
142
+
143
+ #[tokio::test]
144
+ async fn require_existing_ref_returns_head() {
145
+ let reader = RowsVersionRefReader::new(vec![VersionHead {
146
+ version_id: "version-a".to_string(),
147
+ commit_id: "commit-a".to_string(),
148
+ }]);
149
+ let lifecycle = VersionLifecycle::new(&reader);
150
+
151
+ let head = lifecycle
152
+ .require_existing_ref(
153
+ "version-a",
154
+ VersionOperation::SwitchVersion,
155
+ VersionReferenceRole::Target,
156
+ )
157
+ .await
158
+ .expect("version should resolve");
159
+
160
+ assert_eq!(head.commit_id, "commit-a");
161
+ }
162
+
163
+ #[tokio::test]
164
+ async fn require_existing_ref_rejects_empty_id_as_invalid_param() {
165
+ let reader = RowsVersionRefReader::new(Vec::new());
166
+ let lifecycle = VersionLifecycle::new(&reader);
167
+
168
+ let error = lifecycle
169
+ .require_existing_ref(
170
+ "",
171
+ VersionOperation::SwitchVersion,
172
+ VersionReferenceRole::Target,
173
+ )
174
+ .await
175
+ .expect_err("empty version id should be rejected before lookup");
176
+
177
+ assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
178
+ }
179
+
180
+ #[tokio::test]
181
+ async fn require_existing_ref_reports_missing_version() {
182
+ let reader = RowsVersionRefReader::new(Vec::new());
183
+ let lifecycle = VersionLifecycle::new(&reader);
184
+
185
+ let error = lifecycle
186
+ .require_existing_ref(
187
+ "missing",
188
+ VersionOperation::SwitchVersion,
189
+ VersionReferenceRole::Target,
190
+ )
191
+ .await
192
+ .expect_err("missing version should be rejected");
193
+
194
+ assert_eq!(error.code, LixError::CODE_VERSION_NOT_FOUND);
195
+ }
196
+
197
+ struct RowsVersionRefReader {
198
+ heads: Vec<VersionHead>,
199
+ }
200
+
201
+ impl RowsVersionRefReader {
202
+ fn new(heads: Vec<VersionHead>) -> Self {
203
+ Self { heads }
204
+ }
205
+ }
206
+
207
+ #[async_trait]
208
+ impl VersionRefReader for RowsVersionRefReader {
209
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, LixError> {
210
+ Ok(self
211
+ .heads
212
+ .iter()
213
+ .find(|head| head.version_id == version_id)
214
+ .cloned())
215
+ }
216
+
217
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
218
+ Ok(self.heads.clone())
219
+ }
220
+ }
221
+ }
@@ -1,12 +1,13 @@
1
1
  mod context;
2
+ mod lifecycle;
2
3
  mod refs;
3
4
  mod stage_rows;
4
5
  mod types;
5
6
 
6
7
  pub(crate) use context::VersionContext;
8
+ pub(crate) use lifecycle::{VersionLifecycle, VersionOperation, VersionReferenceRole};
7
9
  pub(crate) use stage_rows::{
8
10
  version_descriptor_stage_row, version_descriptor_tombstone_row, version_ref_stage_row,
9
- version_ref_tombstone_row, VERSION_DESCRIPTOR_SCHEMA_KEY, VERSION_DESCRIPTOR_SCHEMA_VERSION,
10
- VERSION_REF_SCHEMA_KEY, VERSION_REF_SCHEMA_VERSION,
11
+ version_ref_tombstone_row, VERSION_DESCRIPTOR_SCHEMA_KEY, VERSION_REF_SCHEMA_KEY,
11
12
  };
12
13
  pub(crate) use types::{VersionHead, VersionRefReader};