@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3

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 (205) hide show
  1. package/SKILL.md +304 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -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 +103 -14
  9. package/dist/open-lix.js +3 -0
  10. package/dist/sqlite/index.js +99 -22
  11. package/dist-engine-src/README.md +18 -0
  12. package/dist-engine-src/src/backend/kv.rs +358 -0
  13. package/dist-engine-src/src/backend/mod.rs +12 -0
  14. package/dist-engine-src/src/backend/testing.rs +658 -0
  15. package/dist-engine-src/src/backend/types.rs +96 -0
  16. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  17. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  18. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  19. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  20. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  21. package/dist-engine-src/src/binary_cas/types.rs +121 -0
  22. package/dist-engine-src/src/catalog/context.rs +412 -0
  23. package/dist-engine-src/src/catalog/mod.rs +10 -0
  24. package/dist-engine-src/src/catalog/schema.rs +4 -0
  25. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  26. package/dist-engine-src/src/cel/context.rs +86 -0
  27. package/dist-engine-src/src/cel/error.rs +19 -0
  28. package/dist-engine-src/src/cel/mod.rs +8 -0
  29. package/dist-engine-src/src/cel/provider.rs +9 -0
  30. package/dist-engine-src/src/cel/runtime.rs +167 -0
  31. package/dist-engine-src/src/cel/value.rs +50 -0
  32. package/dist-engine-src/src/commit_graph/context.rs +901 -0
  33. package/dist-engine-src/src/commit_graph/mod.rs +11 -0
  34. package/dist-engine-src/src/commit_graph/types.rs +109 -0
  35. package/dist-engine-src/src/commit_graph/walker.rs +756 -0
  36. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  37. package/dist-engine-src/src/commit_store/context.rs +944 -0
  38. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  39. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  40. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  41. package/dist-engine-src/src/commit_store/types.rs +215 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +145 -0
  46. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  47. package/dist-engine-src/src/common/metadata.rs +40 -0
  48. package/dist-engine-src/src/common/mod.rs +23 -0
  49. package/dist-engine-src/src/common/types.rs +105 -0
  50. package/dist-engine-src/src/common/wire.rs +222 -0
  51. package/dist-engine-src/src/domain.rs +324 -0
  52. package/dist-engine-src/src/engine.rs +225 -0
  53. package/dist-engine-src/src/entity_identity.rs +405 -0
  54. package/dist-engine-src/src/functions/context.rs +292 -0
  55. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  56. package/dist-engine-src/src/functions/mod.rs +18 -0
  57. package/dist-engine-src/src/functions/provider.rs +130 -0
  58. package/dist-engine-src/src/functions/state.rs +336 -0
  59. package/dist-engine-src/src/functions/types.rs +37 -0
  60. package/dist-engine-src/src/init.rs +558 -0
  61. package/dist-engine-src/src/json_store/compression.rs +77 -0
  62. package/dist-engine-src/src/json_store/context.rs +423 -0
  63. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  64. package/dist-engine-src/src/json_store/mod.rs +12 -0
  65. package/dist-engine-src/src/json_store/store.rs +1109 -0
  66. package/dist-engine-src/src/json_store/types.rs +217 -0
  67. package/dist-engine-src/src/lib.rs +62 -0
  68. package/dist-engine-src/src/live_state/context.rs +2019 -0
  69. package/dist-engine-src/src/live_state/mod.rs +15 -0
  70. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  71. package/dist-engine-src/src/live_state/reader.rs +23 -0
  72. package/dist-engine-src/src/live_state/types.rs +222 -0
  73. package/dist-engine-src/src/live_state/visibility.rs +223 -0
  74. package/dist-engine-src/src/plugin/archive.rs +438 -0
  75. package/dist-engine-src/src/plugin/component.rs +183 -0
  76. package/dist-engine-src/src/plugin/install.rs +619 -0
  77. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  78. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  79. package/dist-engine-src/src/plugin/mod.rs +33 -0
  80. package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
  81. package/dist-engine-src/src/plugin/storage.rs +74 -0
  82. package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
  83. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  84. package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
  85. package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
  86. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
  92. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
  93. package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
  94. package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
  100. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  101. package/dist-engine-src/src/schema/definition.json +187 -0
  102. package/dist-engine-src/src/schema/definition.rs +742 -0
  103. package/dist-engine-src/src/schema/key.rs +138 -0
  104. package/dist-engine-src/src/schema/mod.rs +20 -0
  105. package/dist-engine-src/src/schema/seed.rs +14 -0
  106. package/dist-engine-src/src/schema/tests.rs +780 -0
  107. package/dist-engine-src/src/session/context.rs +364 -0
  108. package/dist-engine-src/src/session/create_version.rs +88 -0
  109. package/dist-engine-src/src/session/execute.rs +478 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +427 -0
  116. package/dist-engine-src/src/session/mod.rs +27 -0
  117. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  118. package/dist-engine-src/src/session/switch_version.rs +109 -0
  119. package/dist-engine-src/src/sql2/change_provider.rs +331 -0
  120. package/dist-engine-src/src/sql2/classify.rs +182 -0
  121. package/dist-engine-src/src/sql2/context.rs +311 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
  127. package/dist-engine-src/src/sql2/error.rs +216 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3440 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +56 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +412 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +657 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
  138. package/dist-engine-src/src/sql2/mod.rs +46 -0
  139. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  140. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  141. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  142. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  143. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  144. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  145. package/dist-engine-src/src/sql2/read_only.rs +63 -0
  146. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  147. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  148. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  149. package/dist-engine-src/src/sql2/session.rs +132 -0
  150. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  153. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  154. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  155. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  156. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  157. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  158. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  159. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  160. package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
  161. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  162. package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
  163. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  164. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  165. package/dist-engine-src/src/storage/context.rs +356 -0
  166. package/dist-engine-src/src/storage/mod.rs +14 -0
  167. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  168. package/dist-engine-src/src/storage/types.rs +501 -0
  169. package/dist-engine-src/src/storage_bench.rs +4863 -0
  170. package/dist-engine-src/src/test_support.rs +228 -0
  171. package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
  172. package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
  173. package/dist-engine-src/src/tracked_state/context.rs +1867 -0
  174. package/dist-engine-src/src/tracked_state/diff.rs +686 -0
  175. package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
  176. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  177. package/dist-engine-src/src/tracked_state/merge.rs +492 -0
  178. package/dist-engine-src/src/tracked_state/mod.rs +32 -0
  179. package/dist-engine-src/src/tracked_state/storage.rs +375 -0
  180. package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
  181. package/dist-engine-src/src/tracked_state/types.rs +231 -0
  182. package/dist-engine-src/src/transaction/commit.rs +1484 -0
  183. package/dist-engine-src/src/transaction/context.rs +1548 -0
  184. package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
  185. package/dist-engine-src/src/transaction/mod.rs +13 -0
  186. package/dist-engine-src/src/transaction/normalization.rs +890 -0
  187. package/dist-engine-src/src/transaction/prep.rs +37 -0
  188. package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
  189. package/dist-engine-src/src/transaction/staging.rs +1731 -0
  190. package/dist-engine-src/src/transaction/types.rs +460 -0
  191. package/dist-engine-src/src/transaction/validation.rs +5830 -0
  192. package/dist-engine-src/src/untracked_state/codec.rs +307 -0
  193. package/dist-engine-src/src/untracked_state/context.rs +98 -0
  194. package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
  195. package/dist-engine-src/src/untracked_state/mod.rs +15 -0
  196. package/dist-engine-src/src/untracked_state/storage.rs +396 -0
  197. package/dist-engine-src/src/untracked_state/types.rs +146 -0
  198. package/dist-engine-src/src/version/context.rs +40 -0
  199. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  200. package/dist-engine-src/src/version/mod.rs +13 -0
  201. package/dist-engine-src/src/version/refs.rs +330 -0
  202. package/dist-engine-src/src/version/stage_rows.rs +67 -0
  203. package/dist-engine-src/src/version/types.rs +21 -0
  204. package/dist-engine-src/src/wasm/mod.rs +60 -0
  205. package/package.json +68 -64
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ mod context;
2
+ mod lifecycle;
3
+ mod refs;
4
+ mod stage_rows;
5
+ mod types;
6
+
7
+ pub(crate) use context::VersionContext;
8
+ pub(crate) use lifecycle::{VersionLifecycle, VersionOperation, VersionReferenceRole};
9
+ pub(crate) use stage_rows::{
10
+ version_descriptor_stage_row, version_descriptor_tombstone_row, version_ref_stage_row,
11
+ version_ref_tombstone_row, VERSION_DESCRIPTOR_SCHEMA_KEY, VERSION_REF_SCHEMA_KEY,
12
+ };
13
+ pub(crate) use types::{VersionHead, VersionRefReader};
@@ -0,0 +1,330 @@
1
+ use std::sync::Arc;
2
+
3
+ use tokio::sync::Mutex;
4
+
5
+ use crate::entity_identity::EntityIdentity;
6
+ use crate::storage::{StorageReader, StorageWriteSet};
7
+ use crate::untracked_state::{
8
+ MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateFilter, UntrackedStateRow,
9
+ UntrackedStateRowRequest, UntrackedStateScanRequest,
10
+ };
11
+ use crate::version::VERSION_REF_SCHEMA_KEY;
12
+ use crate::version::{VersionHead, VersionRefReader};
13
+ use crate::GLOBAL_VERSION_ID;
14
+ use crate::{LixError, NullableKeyFilter};
15
+
16
+ /// Typed access to moving version heads stored in untracked state.
17
+ ///
18
+ /// Version refs are one of the inputs used by live_state visibility, so this
19
+ /// context deliberately bypasses live_state and reads the underlying untracked
20
+ /// rows directly. That keeps the dependency acyclic:
21
+ /// untracked_state -> version_ref -> live_state.
22
+ pub(super) struct VersionRefContext {
23
+ untracked_state: Arc<UntrackedStateContext>,
24
+ }
25
+
26
+ impl VersionRefContext {
27
+ pub(super) fn new(untracked_state: Arc<UntrackedStateContext>) -> Self {
28
+ Self { untracked_state }
29
+ }
30
+
31
+ /// Creates a version-ref reader over a caller-provided KV store.
32
+ pub(super) fn reader<S>(&self, store: S) -> VersionRefStoreReader<S>
33
+ where
34
+ S: StorageReader,
35
+ {
36
+ VersionRefStoreReader {
37
+ untracked_state: Arc::clone(&self.untracked_state),
38
+ store: Mutex::new(store),
39
+ }
40
+ }
41
+
42
+ /// Creates a version-ref writer over a transaction-local storage write set.
43
+ pub(super) fn writer<'a>(&self, writes: &'a mut StorageWriteSet) -> VersionRefWriter<'a> {
44
+ VersionRefWriter {
45
+ untracked_state: Arc::clone(&self.untracked_state),
46
+ writes,
47
+ }
48
+ }
49
+ }
50
+
51
+ /// Read side for version heads.
52
+ pub(super) struct VersionRefStoreReader<S>
53
+ where
54
+ S: StorageReader,
55
+ {
56
+ untracked_state: Arc<UntrackedStateContext>,
57
+ store: Mutex<S>,
58
+ }
59
+
60
+ impl<S> VersionRefStoreReader<S>
61
+ where
62
+ S: StorageReader,
63
+ {
64
+ pub(crate) async fn load_head(
65
+ &self,
66
+ version_id: &str,
67
+ ) -> Result<Option<VersionHead>, LixError> {
68
+ let mut store = self.store.lock().await;
69
+ let Some(row) = self
70
+ .untracked_state
71
+ .reader(&mut *store as &mut dyn StorageReader)
72
+ .load_row(&UntrackedStateRowRequest {
73
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
74
+ version_id: GLOBAL_VERSION_ID.to_string(),
75
+ entity_id: EntityIdentity::single(version_id),
76
+ file_id: NullableKeyFilter::Null,
77
+ })
78
+ .await?
79
+ else {
80
+ return Ok(None);
81
+ };
82
+
83
+ decode_version_head(version_id, &row)
84
+ }
85
+
86
+ pub(crate) async fn load_head_commit_id(
87
+ &self,
88
+ version_id: &str,
89
+ ) -> Result<Option<String>, LixError> {
90
+ Ok(self.load_head(version_id).await?.map(|head| head.commit_id))
91
+ }
92
+
93
+ pub(crate) async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
94
+ let mut store = self.store.lock().await;
95
+ let rows = self
96
+ .untracked_state
97
+ .reader(&mut *store as &mut dyn StorageReader)
98
+ .scan_rows(&UntrackedStateScanRequest {
99
+ filter: UntrackedStateFilter {
100
+ schema_keys: vec![VERSION_REF_SCHEMA_KEY.to_string()],
101
+ version_ids: vec![GLOBAL_VERSION_ID.to_string()],
102
+ ..UntrackedStateFilter::default()
103
+ },
104
+ ..UntrackedStateScanRequest::default()
105
+ })
106
+ .await?;
107
+ let mut heads = rows
108
+ .iter()
109
+ .map(|row| {
110
+ let version_id = row.entity_id.as_single_string_owned()?;
111
+ decode_version_head(&version_id, row)
112
+ })
113
+ .collect::<Result<Vec<_>, _>>()?
114
+ .into_iter()
115
+ .flatten()
116
+ .collect::<Vec<_>>();
117
+ heads.sort_by(|left, right| left.version_id.cmp(&right.version_id));
118
+ Ok(heads)
119
+ }
120
+ }
121
+
122
+ #[async_trait::async_trait]
123
+ impl<S> VersionRefReader for VersionRefStoreReader<S>
124
+ where
125
+ S: StorageReader + Send,
126
+ {
127
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, LixError> {
128
+ VersionRefStoreReader::load_head(self, version_id).await
129
+ }
130
+
131
+ async fn load_head_commit_id(&self, version_id: &str) -> Result<Option<String>, LixError> {
132
+ VersionRefStoreReader::load_head_commit_id(self, version_id).await
133
+ }
134
+
135
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
136
+ VersionRefStoreReader::scan_heads(self).await
137
+ }
138
+ }
139
+
140
+ /// Write side for moving version heads.
141
+ pub(super) struct VersionRefWriter<'a> {
142
+ untracked_state: Arc<UntrackedStateContext>,
143
+ writes: &'a mut StorageWriteSet,
144
+ }
145
+
146
+ impl VersionRefWriter<'_> {
147
+ pub(crate) fn stage_rows(&mut self, rows: &[UntrackedStateRow]) -> Result<(), LixError> {
148
+ self.untracked_state
149
+ .writer(self.writes)
150
+ .stage_rows(rows.iter().map(|row| row.as_ref()))
151
+ }
152
+ }
153
+
154
+ fn decode_version_head(
155
+ requested_version_id: &str,
156
+ row: &MaterializedUntrackedStateRow,
157
+ ) -> Result<Option<VersionHead>, LixError> {
158
+ let Some(snapshot_content) = row.snapshot_content.as_deref() else {
159
+ return Ok(None);
160
+ };
161
+ let snapshot =
162
+ serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
163
+ LixError::new(
164
+ "LIX_ERROR_UNKNOWN",
165
+ format!("engine version-ref snapshot parse failed: {error}"),
166
+ )
167
+ })?;
168
+ let commit_id = snapshot
169
+ .get("commit_id")
170
+ .and_then(serde_json::Value::as_str)
171
+ .ok_or_else(|| {
172
+ LixError::new(
173
+ "LIX_ERROR_UNKNOWN",
174
+ format!("version ref for version '{requested_version_id}' is missing commit_id"),
175
+ )
176
+ })?;
177
+ Ok(Some(VersionHead {
178
+ version_id: requested_version_id.to_string(),
179
+ commit_id: commit_id.to_string(),
180
+ }))
181
+ }
182
+
183
+ #[cfg(test)]
184
+ mod tests {
185
+ use std::sync::Arc;
186
+
187
+ use crate::backend::testing::UnitTestBackend;
188
+ use crate::storage::{StorageContext, StorageWriteSet};
189
+ use crate::transaction::prepare_version_ref_row;
190
+ use crate::untracked_state::{UntrackedStateContext, UntrackedStateRowRequest};
191
+
192
+ use super::*;
193
+
194
+ #[tokio::test]
195
+ async fn load_head_returns_none_when_missing() {
196
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
197
+ let version_ref = test_version_ref();
198
+
199
+ let head = version_ref
200
+ .reader(storage)
201
+ .load_head("missing-version")
202
+ .await
203
+ .expect("missing version ref should load cleanly");
204
+
205
+ assert_eq!(head, None);
206
+ }
207
+
208
+ #[tokio::test]
209
+ async fn advance_head_writes_untracked_global_ref() {
210
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
211
+ let version_ref = VersionRefContext::new(Arc::new(UntrackedStateContext::new()));
212
+ let mut transaction = storage
213
+ .begin_write_transaction()
214
+ .await
215
+ .expect("transaction should open");
216
+
217
+ let mut writes = StorageWriteSet::new();
218
+ stage_version_head(
219
+ &version_ref,
220
+ &mut writes,
221
+ "version-a",
222
+ "commit-a",
223
+ "2026-01-01T00:00:00Z",
224
+ )
225
+ .expect("version head should advance");
226
+ writes
227
+ .apply(&mut transaction.as_mut())
228
+ .await
229
+ .expect("version head should apply");
230
+ transaction
231
+ .commit()
232
+ .await
233
+ .expect("transaction should commit");
234
+
235
+ let head = version_ref
236
+ .reader(storage.clone())
237
+ .load_head("version-a")
238
+ .await
239
+ .expect("version head should load")
240
+ .expect("version head should exist");
241
+ assert_eq!(head.version_id, "version-a");
242
+ assert_eq!(head.commit_id, "commit-a");
243
+
244
+ let mut reader = UntrackedStateContext::new().reader(storage);
245
+ let row = reader
246
+ .load_row(&UntrackedStateRowRequest {
247
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
248
+ version_id: GLOBAL_VERSION_ID.to_string(),
249
+ entity_id: crate::entity_identity::EntityIdentity::single("version-a"),
250
+ file_id: NullableKeyFilter::Null,
251
+ })
252
+ .await
253
+ .expect("version-ref row should load")
254
+ .expect("version-ref row should exist");
255
+ assert!(row.global);
256
+ assert_eq!(row.created_at, "2026-01-01T00:00:00Z");
257
+ assert_eq!(row.updated_at, "2026-01-01T00:00:00Z");
258
+ }
259
+
260
+ #[tokio::test]
261
+ async fn scan_heads_returns_sorted_version_heads() {
262
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
263
+ let version_ref = test_version_ref();
264
+ let mut transaction = storage
265
+ .begin_write_transaction()
266
+ .await
267
+ .expect("transaction should open");
268
+
269
+ let mut writes = StorageWriteSet::new();
270
+ stage_version_head(
271
+ &version_ref,
272
+ &mut writes,
273
+ "version-b",
274
+ "commit-b",
275
+ "2026-01-01T00:00:00Z",
276
+ )
277
+ .expect("version-b should advance");
278
+ stage_version_head(
279
+ &version_ref,
280
+ &mut writes,
281
+ "version-a",
282
+ "commit-a",
283
+ "2026-01-01T00:00:00Z",
284
+ )
285
+ .expect("version-a should advance");
286
+ writes
287
+ .apply(&mut transaction.as_mut())
288
+ .await
289
+ .expect("version heads should apply");
290
+ transaction
291
+ .commit()
292
+ .await
293
+ .expect("transaction should commit");
294
+
295
+ let heads = version_ref
296
+ .reader(storage)
297
+ .scan_heads()
298
+ .await
299
+ .expect("heads should scan");
300
+
301
+ assert_eq!(
302
+ heads,
303
+ vec![
304
+ VersionHead {
305
+ version_id: "version-a".to_string(),
306
+ commit_id: "commit-a".to_string(),
307
+ },
308
+ VersionHead {
309
+ version_id: "version-b".to_string(),
310
+ commit_id: "commit-b".to_string(),
311
+ },
312
+ ]
313
+ );
314
+ }
315
+
316
+ fn test_version_ref() -> VersionRefContext {
317
+ VersionRefContext::new(Arc::new(UntrackedStateContext::new()))
318
+ }
319
+
320
+ fn stage_version_head(
321
+ version_ref: &VersionRefContext,
322
+ writes: &mut StorageWriteSet,
323
+ version_id: &str,
324
+ commit_id: &str,
325
+ timestamp: &str,
326
+ ) -> Result<(), LixError> {
327
+ let canonical_row = prepare_version_ref_row(version_id, commit_id, timestamp)?;
328
+ version_ref.writer(writes).stage_rows(&[canonical_row.row])
329
+ }
330
+ }
@@ -0,0 +1,67 @@
1
+ use serde_json::json;
2
+
3
+ use crate::entity_identity::EntityIdentity;
4
+ use crate::transaction::types::{TransactionJson, TransactionWriteRow};
5
+ use crate::GLOBAL_VERSION_ID;
6
+
7
+ pub(crate) const VERSION_DESCRIPTOR_SCHEMA_KEY: &str = "lix_version_descriptor";
8
+ pub(crate) const VERSION_REF_SCHEMA_KEY: &str = "lix_version_ref";
9
+
10
+ pub(crate) fn version_descriptor_stage_row(
11
+ version_id: &str,
12
+ name: &str,
13
+ hidden: bool,
14
+ ) -> TransactionWriteRow {
15
+ TransactionWriteRow {
16
+ entity_id: Some(EntityIdentity::single(version_id)),
17
+ schema_key: VERSION_DESCRIPTOR_SCHEMA_KEY.to_string(),
18
+ file_id: None,
19
+ snapshot: Some(TransactionJson::from_value_unchecked(json!({
20
+ "id": version_id,
21
+ "name": name,
22
+ "hidden": hidden,
23
+ }))),
24
+ metadata: None,
25
+ origin: None,
26
+ created_at: None,
27
+ updated_at: None,
28
+ global: true,
29
+ change_id: None,
30
+ commit_id: None,
31
+ untracked: false,
32
+ version_id: GLOBAL_VERSION_ID.to_string(),
33
+ }
34
+ }
35
+
36
+ pub(crate) fn version_ref_stage_row(version_id: &str, commit_id: &str) -> TransactionWriteRow {
37
+ TransactionWriteRow {
38
+ entity_id: Some(EntityIdentity::single(version_id)),
39
+ schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
40
+ file_id: None,
41
+ snapshot: Some(TransactionJson::from_value_unchecked(json!({
42
+ "id": version_id,
43
+ "commit_id": commit_id,
44
+ }))),
45
+ metadata: None,
46
+ origin: None,
47
+ created_at: None,
48
+ updated_at: None,
49
+ global: true,
50
+ change_id: None,
51
+ commit_id: None,
52
+ untracked: true,
53
+ version_id: GLOBAL_VERSION_ID.to_string(),
54
+ }
55
+ }
56
+
57
+ pub(crate) fn version_descriptor_tombstone_row(version_id: &str) -> TransactionWriteRow {
58
+ let mut row = version_descriptor_stage_row(version_id, "", false);
59
+ row.snapshot = None;
60
+ row
61
+ }
62
+
63
+ pub(crate) fn version_ref_tombstone_row(version_id: &str) -> TransactionWriteRow {
64
+ let mut row = version_ref_stage_row(version_id, "");
65
+ row.snapshot = None;
66
+ row
67
+ }
@@ -0,0 +1,21 @@
1
+ /// Current changelog head for a version.
2
+ #[derive(Debug, Clone, PartialEq, Eq)]
3
+ pub(crate) struct VersionHead {
4
+ pub(crate) version_id: String,
5
+ pub(crate) commit_id: String,
6
+ }
7
+
8
+ /// Typed reader for moving version heads.
9
+ #[async_trait::async_trait]
10
+ pub(crate) trait VersionRefReader: Send + Sync {
11
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, crate::LixError>;
12
+
13
+ async fn load_head_commit_id(
14
+ &self,
15
+ version_id: &str,
16
+ ) -> Result<Option<String>, crate::LixError> {
17
+ Ok(self.load_head(version_id).await?.map(|head| head.commit_id))
18
+ }
19
+
20
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, crate::LixError>;
21
+ }