@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,686 @@
1
+ use crate::entity_identity::EntityIdentity;
2
+ use crate::tracked_state::types::TrackedStateTreeScanRequest;
3
+ use crate::tracked_state::{
4
+ MaterializedTrackedStateRow, TrackedStateFilter, TrackedStateStoreReader,
5
+ };
6
+ use crate::LixError;
7
+
8
+ /// Filter for comparing two tracked-state commit roots.
9
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
10
+ pub(crate) struct TrackedStateDiffRequest {
11
+ pub(crate) filter: TrackedStateFilter,
12
+ }
13
+
14
+ /// Changed tracked-state rows between two commit roots.
15
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
16
+ pub(crate) struct TrackedStateDiff {
17
+ pub(crate) entries: Vec<TrackedStateDiffEntry>,
18
+ }
19
+
20
+ /// One changed identity between two commit roots.
21
+ #[derive(Debug, Clone, PartialEq, Eq)]
22
+ pub(crate) struct TrackedStateDiffEntry {
23
+ pub(crate) identity: TrackedStateDiffIdentity,
24
+ pub(crate) kind: TrackedStateDiffKind,
25
+ /// Raw row in the left root.
26
+ ///
27
+ /// This can be a tombstone. Callers that need user-visible semantics
28
+ /// should use `visible_before()` instead of inspecting this directly.
29
+ pub(crate) before: Option<MaterializedTrackedStateRow>,
30
+ /// Raw row in the right root.
31
+ ///
32
+ /// This can be a tombstone. Keeping the raw tombstone is what lets merge
33
+ /// apply deletes without reloading the source root.
34
+ pub(crate) after: Option<MaterializedTrackedStateRow>,
35
+ }
36
+
37
+ /// Root-local tracked-state identity.
38
+ ///
39
+ /// Entity identity used by merge/diff logic.
40
+ #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
41
+ pub(crate) struct TrackedStateDiffIdentity {
42
+ pub(crate) schema_key: String,
43
+ pub(crate) entity_id: EntityIdentity,
44
+ pub(crate) file_id: Option<String>,
45
+ }
46
+
47
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48
+ pub(crate) enum TrackedStateDiffKind {
49
+ Added,
50
+ Modified,
51
+ Removed,
52
+ }
53
+
54
+ /// Diffs two tracked-state commit roots.
55
+ ///
56
+ pub(crate) async fn diff_commits<S>(
57
+ reader: &mut TrackedStateStoreReader<S>,
58
+ left_commit_id: &str,
59
+ right_commit_id: &str,
60
+ request: &TrackedStateDiffRequest,
61
+ ) -> Result<TrackedStateDiff, LixError>
62
+ where
63
+ S: crate::storage::StorageReader,
64
+ {
65
+ let scan_request = scan_request_for_diff(request);
66
+ let tree_entries = reader
67
+ .diff_tree_entries_at_commits(left_commit_id, right_commit_id, &scan_request)
68
+ .await?;
69
+ let mut before_entries = Vec::new();
70
+ let mut after_entries = Vec::new();
71
+ let mut pending_entries = Vec::with_capacity(tree_entries.len());
72
+ for tree_entry in tree_entries {
73
+ let before_index = tree_entry.before.map(|entry| {
74
+ let index = before_entries.len();
75
+ before_entries.push(entry);
76
+ index
77
+ });
78
+ let after_index = tree_entry.after.map(|entry| {
79
+ let index = after_entries.len();
80
+ after_entries.push(entry);
81
+ index
82
+ });
83
+ pending_entries.push(PendingDiffEntry {
84
+ before_index,
85
+ after_index,
86
+ });
87
+ }
88
+
89
+ let before_rows = reader.materialize_tree_values(before_entries).await?;
90
+ let after_rows = reader.materialize_tree_values(after_entries).await?;
91
+ let mut entries = Vec::new();
92
+ for pending_entry in pending_entries {
93
+ let before = materialized_row_at(pending_entry.before_index, &before_rows)?;
94
+ let after = materialized_row_at(pending_entry.after_index, &after_rows)?;
95
+ let identity = match before.as_ref().or(after.as_ref()) {
96
+ Some(row) => TrackedStateDiffIdentity::from_row(row)?,
97
+ None => continue,
98
+ };
99
+ let Some(entry) = classify_diff(identity, before, after) else {
100
+ continue;
101
+ };
102
+ entries.push(entry);
103
+ }
104
+
105
+ Ok(TrackedStateDiff { entries })
106
+ }
107
+
108
+ fn materialized_row_at(
109
+ index: Option<usize>,
110
+ rows: &[MaterializedTrackedStateRow],
111
+ ) -> Result<Option<MaterializedTrackedStateRow>, LixError> {
112
+ let Some(index) = index else {
113
+ return Ok(None);
114
+ };
115
+ rows.get(index).cloned().map(Some).ok_or_else(|| {
116
+ LixError::new(
117
+ LixError::CODE_INTERNAL_ERROR,
118
+ "tracked_state diff materialization returned fewer rows than planned",
119
+ )
120
+ })
121
+ }
122
+
123
+ struct PendingDiffEntry {
124
+ before_index: Option<usize>,
125
+ after_index: Option<usize>,
126
+ }
127
+
128
+ fn scan_request_for_diff(request: &TrackedStateDiffRequest) -> TrackedStateTreeScanRequest {
129
+ let mut filter = request.filter.clone();
130
+ filter.include_tombstones = true;
131
+ TrackedStateTreeScanRequest {
132
+ schema_keys: filter.schema_keys,
133
+ entity_ids: filter.entity_ids,
134
+ file_ids: filter.file_ids,
135
+ include_tombstones: true,
136
+ limit: None,
137
+ }
138
+ }
139
+
140
+ fn classify_diff(
141
+ identity: TrackedStateDiffIdentity,
142
+ before: Option<MaterializedTrackedStateRow>,
143
+ after: Option<MaterializedTrackedStateRow>,
144
+ ) -> Option<TrackedStateDiffEntry> {
145
+ match (is_live_row(before.as_ref()), is_live_row(after.as_ref())) {
146
+ (None, None) => None,
147
+ (None, Some(_)) => Some(TrackedStateDiffEntry {
148
+ identity,
149
+ kind: TrackedStateDiffKind::Added,
150
+ before,
151
+ after,
152
+ }),
153
+ (Some(_), None) => Some(TrackedStateDiffEntry {
154
+ identity,
155
+ kind: TrackedStateDiffKind::Removed,
156
+ before,
157
+ after,
158
+ }),
159
+ (Some(before), Some(after)) if tracked_row_payload_eq(before, after) => None,
160
+ (Some(_), Some(_)) => Some(TrackedStateDiffEntry {
161
+ identity,
162
+ kind: TrackedStateDiffKind::Modified,
163
+ before,
164
+ after,
165
+ }),
166
+ }
167
+ }
168
+
169
+ fn is_live_row(row: Option<&MaterializedTrackedStateRow>) -> Option<&MaterializedTrackedStateRow> {
170
+ row.filter(|row| row.snapshot_content.is_some())
171
+ }
172
+
173
+ fn tracked_row_payload_eq(
174
+ left: &MaterializedTrackedStateRow,
175
+ right: &MaterializedTrackedStateRow,
176
+ ) -> bool {
177
+ left.snapshot_content == right.snapshot_content && left.metadata == right.metadata
178
+ }
179
+
180
+ impl TrackedStateDiffIdentity {
181
+ fn from_row(row: &MaterializedTrackedStateRow) -> Result<Self, LixError> {
182
+ Ok(Self {
183
+ schema_key: row.schema_key.clone(),
184
+ entity_id: row.entity_id.clone(),
185
+ file_id: row.file_id.clone(),
186
+ })
187
+ }
188
+ }
189
+
190
+ impl TrackedStateDiffEntry {
191
+ #[cfg(test)]
192
+ pub(crate) fn before_is_live(&self) -> bool {
193
+ self.visible_before().is_some()
194
+ }
195
+
196
+ #[cfg(test)]
197
+ pub(crate) fn after_is_live(&self) -> bool {
198
+ self.visible_after().is_some()
199
+ }
200
+
201
+ #[cfg(test)]
202
+ pub(crate) fn visible_before(&self) -> Option<&MaterializedTrackedStateRow> {
203
+ self.before
204
+ .as_ref()
205
+ .filter(|row| row.snapshot_content.is_some())
206
+ }
207
+
208
+ #[cfg(test)]
209
+ pub(crate) fn visible_after(&self) -> Option<&MaterializedTrackedStateRow> {
210
+ self.after
211
+ .as_ref()
212
+ .filter(|row| row.snapshot_content.is_some())
213
+ }
214
+ }
215
+
216
+ #[cfg(test)]
217
+ mod tests {
218
+ use std::sync::Arc;
219
+
220
+ use super::*;
221
+ use crate::backend::testing::UnitTestBackend;
222
+ use crate::storage::{StorageContext, StorageWriteTransaction};
223
+ use crate::tracked_state::TrackedStateContext;
224
+ use crate::NullableKeyFilter;
225
+
226
+ #[tokio::test]
227
+ async fn diff_commits_reports_added_rows() {
228
+ let (storage, tracked_state) = seed_roots(&[], &[row("entity-a", None, "after")]).await;
229
+
230
+ let diff = diff(storage.clone(), &tracked_state).await;
231
+
232
+ assert_eq!(
233
+ kinds(&diff),
234
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
235
+ );
236
+ assert!(diff.entries[0].before.is_none());
237
+ assert_eq!(
238
+ diff.entries[0]
239
+ .after
240
+ .as_ref()
241
+ .map(|row| row.change_id.as_str()),
242
+ Some("after")
243
+ );
244
+ assert!(!diff.entries[0].before_is_live());
245
+ assert!(diff.entries[0].after_is_live());
246
+ }
247
+
248
+ #[tokio::test]
249
+ async fn diff_commits_reports_removed_rows_when_right_side_is_absent() {
250
+ let (storage, tracked_state) = seed_roots(&[row("entity-a", None, "before")], &[]).await;
251
+
252
+ let diff = diff(storage.clone(), &tracked_state).await;
253
+
254
+ assert_eq!(
255
+ kinds(&diff),
256
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
257
+ );
258
+ assert_eq!(
259
+ diff.entries[0]
260
+ .before
261
+ .as_ref()
262
+ .map(|row| row.change_id.as_str()),
263
+ Some("before")
264
+ );
265
+ assert!(diff.entries[0].after.is_none());
266
+ assert!(diff.entries[0].before_is_live());
267
+ assert!(!diff.entries[0].after_is_live());
268
+ }
269
+
270
+ #[tokio::test]
271
+ async fn diff_commits_reports_removed_rows_when_right_side_is_tombstone() {
272
+ let (storage, tracked_state) = seed_roots(
273
+ &[row("entity-a", None, "before")],
274
+ &[tombstone("entity-a", None, "delete")],
275
+ )
276
+ .await;
277
+
278
+ let diff = diff(storage.clone(), &tracked_state).await;
279
+
280
+ assert_eq!(
281
+ kinds(&diff),
282
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
283
+ );
284
+ let entry = &diff.entries[0];
285
+ assert_eq!(
286
+ entry.after.as_ref().map(|row| row.change_id.as_str()),
287
+ Some("delete")
288
+ );
289
+ assert!(
290
+ entry
291
+ .after
292
+ .as_ref()
293
+ .is_some_and(|row| row.snapshot_content.is_none()),
294
+ "removed diff should preserve the right-side tombstone for merge"
295
+ );
296
+ assert!(entry.before_is_live());
297
+ assert!(!entry.after_is_live());
298
+ }
299
+
300
+ #[tokio::test]
301
+ async fn diff_commits_reports_added_rows_when_left_side_is_tombstone() {
302
+ let (storage, tracked_state) = seed_roots(
303
+ &[tombstone("entity-a", None, "delete")],
304
+ &[row("entity-a", None, "after")],
305
+ )
306
+ .await;
307
+
308
+ let diff = diff(storage.clone(), &tracked_state).await;
309
+
310
+ assert_eq!(
311
+ kinds(&diff),
312
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Added)]
313
+ );
314
+ let entry = &diff.entries[0];
315
+ assert_eq!(
316
+ entry.before.as_ref().map(|row| row.change_id.as_str()),
317
+ Some("delete")
318
+ );
319
+ assert!(
320
+ entry
321
+ .before
322
+ .as_ref()
323
+ .is_some_and(|row| row.snapshot_content.is_none()),
324
+ "added diff should preserve the left-side tombstone for merge"
325
+ );
326
+ assert!(!entry.before_is_live());
327
+ assert!(entry.after_is_live());
328
+ }
329
+
330
+ #[tokio::test]
331
+ async fn diff_commits_reports_modified_rows_for_changed_payload() {
332
+ let (storage, tracked_state) = seed_roots(
333
+ &[row_with_value("entity-a", None, "before", "one")],
334
+ &[row_with_value("entity-a", None, "after", "two")],
335
+ )
336
+ .await;
337
+
338
+ let diff = diff(storage.clone(), &tracked_state).await;
339
+
340
+ assert_eq!(
341
+ kinds(&diff),
342
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
343
+ );
344
+ assert!(diff.entries[0].before_is_live());
345
+ assert!(diff.entries[0].after_is_live());
346
+ }
347
+
348
+ #[tokio::test]
349
+ async fn diff_commits_omits_unchanged_rows_even_when_metadata_differs_only_by_commit() {
350
+ let (storage, tracked_state) = seed_roots(
351
+ &[row_with_value("entity-a", None, "before", "same")],
352
+ &[row_with_value("entity-a", None, "after", "same")],
353
+ )
354
+ .await;
355
+
356
+ let diff = diff(storage.clone(), &tracked_state).await;
357
+
358
+ assert!(diff.entries.is_empty());
359
+ }
360
+
361
+ #[tokio::test]
362
+ async fn diff_commits_distinguishes_same_entity_with_different_file_id() {
363
+ let (storage, tracked_state) = seed_roots(
364
+ &[row("entity-a", Some("file-a"), "before-a")],
365
+ &[
366
+ row("entity-a", Some("file-a"), "before-a"),
367
+ row("entity-a", Some("file-b"), "after-b"),
368
+ ],
369
+ )
370
+ .await;
371
+
372
+ let diff = diff(storage.clone(), &tracked_state).await;
373
+
374
+ assert_eq!(diff.entries.len(), 1);
375
+ assert_eq!(diff.entries[0].identity.file_id.as_deref(), Some("file-b"));
376
+ assert_eq!(diff.entries[0].kind, TrackedStateDiffKind::Added);
377
+ }
378
+
379
+ #[tokio::test]
380
+ async fn diff_commits_filters_by_schema_entity_and_file_id() {
381
+ let (storage, tracked_state) = seed_roots(
382
+ &[],
383
+ &[
384
+ row_with_schema("entity-a", Some("file-a"), "schema-a", "change-a"),
385
+ row_with_schema("entity-b", Some("file-b"), "schema-b", "change-b"),
386
+ ],
387
+ )
388
+ .await;
389
+ let mut reader = tracked_state.reader(storage.clone());
390
+ let diff = reader
391
+ .diff_commits(
392
+ "left",
393
+ "right",
394
+ &TrackedStateDiffRequest {
395
+ filter: TrackedStateFilter {
396
+ schema_keys: vec!["schema-b".to_string()],
397
+ entity_ids: vec![crate::entity_identity::EntityIdentity::single(
398
+ "entity-b",
399
+ )],
400
+ file_ids: vec![NullableKeyFilter::Value("file-b".to_string())],
401
+ ..Default::default()
402
+ },
403
+ },
404
+ )
405
+ .await
406
+ .expect("diff should load");
407
+
408
+ assert_eq!(
409
+ kinds(&diff),
410
+ vec![("entity-b".to_string(), TrackedStateDiffKind::Added)]
411
+ );
412
+ }
413
+
414
+ #[tokio::test]
415
+ async fn diff_commits_between_delta_parent_and_child_reports_suffix_rows() {
416
+ let backend = Arc::new(UnitTestBackend::new());
417
+ let storage = StorageContext::new(backend.clone());
418
+ let tracked_state = TrackedStateContext::new();
419
+ let mut tx = storage
420
+ .begin_write_transaction()
421
+ .await
422
+ .expect("transaction should open");
423
+ write_root_for_test(
424
+ tx.as_mut(),
425
+ &tracked_state,
426
+ "parent",
427
+ None,
428
+ &[
429
+ row_with_value("entity-a", None, "parent-a", "before"),
430
+ row_with_value("entity-b", None, "parent-b", "same"),
431
+ ],
432
+ )
433
+ .await
434
+ .expect("parent should write");
435
+ write_root_for_test(
436
+ tx.as_mut(),
437
+ &tracked_state,
438
+ "child",
439
+ Some("parent"),
440
+ &[row_with_value("entity-a", None, "child-a", "after")],
441
+ )
442
+ .await
443
+ .expect("child should write");
444
+ tx.commit().await.expect("transaction should commit");
445
+
446
+ let diff = tracked_state
447
+ .reader(storage)
448
+ .diff_commits("parent", "child", &TrackedStateDiffRequest::default())
449
+ .await
450
+ .expect("diff should load");
451
+
452
+ assert_eq!(
453
+ kinds(&diff),
454
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
455
+ );
456
+ assert_eq!(
457
+ diff.entries[0]
458
+ .before
459
+ .as_ref()
460
+ .and_then(|row| row.snapshot_content.as_deref()),
461
+ Some("{\"value\":\"before\"}")
462
+ );
463
+ assert_eq!(
464
+ diff.entries[0]
465
+ .after
466
+ .as_ref()
467
+ .and_then(|row| row.snapshot_content.as_deref()),
468
+ Some("{\"value\":\"after\"}")
469
+ );
470
+ }
471
+
472
+ #[tokio::test]
473
+ async fn diff_commits_between_delta_child_and_parent_reports_reverse_suffix_rows() {
474
+ let (storage, tracked_state) = seed_parent_child_delta(
475
+ &[
476
+ row_with_value("entity-a", None, "parent-a", "before"),
477
+ row_with_value("entity-b", None, "parent-b", "same"),
478
+ ],
479
+ &[row_with_value("entity-a", None, "child-a", "after")],
480
+ )
481
+ .await;
482
+
483
+ let diff = tracked_state
484
+ .reader(storage)
485
+ .diff_commits("child", "parent", &TrackedStateDiffRequest::default())
486
+ .await
487
+ .expect("diff should load");
488
+
489
+ assert_eq!(
490
+ kinds(&diff),
491
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Modified)]
492
+ );
493
+ assert_eq!(
494
+ diff.entries[0]
495
+ .before
496
+ .as_ref()
497
+ .and_then(|row| row.snapshot_content.as_deref()),
498
+ Some("{\"value\":\"after\"}")
499
+ );
500
+ assert_eq!(
501
+ diff.entries[0]
502
+ .after
503
+ .as_ref()
504
+ .and_then(|row| row.snapshot_content.as_deref()),
505
+ Some("{\"value\":\"before\"}")
506
+ );
507
+ }
508
+
509
+ #[tokio::test]
510
+ async fn diff_commits_between_delta_parent_and_child_preserves_suffix_tombstones() {
511
+ let (storage, tracked_state) = seed_parent_child_delta(
512
+ &[
513
+ row_with_value("entity-a", None, "parent-a", "before"),
514
+ row_with_value("entity-b", None, "parent-b", "same"),
515
+ ],
516
+ &[tombstone("entity-a", None, "child-delete")],
517
+ )
518
+ .await;
519
+
520
+ let diff = tracked_state
521
+ .reader(storage)
522
+ .diff_commits("parent", "child", &TrackedStateDiffRequest::default())
523
+ .await
524
+ .expect("diff should load");
525
+
526
+ assert_eq!(
527
+ kinds(&diff),
528
+ vec![("entity-a".to_string(), TrackedStateDiffKind::Removed)]
529
+ );
530
+ assert!(diff.entries[0].before_is_live());
531
+ assert!(!diff.entries[0].after_is_live());
532
+ assert_eq!(
533
+ diff.entries[0]
534
+ .after
535
+ .as_ref()
536
+ .map(|row| row.change_id.as_str()),
537
+ Some("child-delete")
538
+ );
539
+ }
540
+
541
+ async fn diff(
542
+ storage: StorageContext,
543
+ tracked_state: &TrackedStateContext,
544
+ ) -> TrackedStateDiff {
545
+ tracked_state
546
+ .reader(storage)
547
+ .diff_commits("left", "right", &TrackedStateDiffRequest::default())
548
+ .await
549
+ .expect("diff should load")
550
+ }
551
+
552
+ async fn seed_roots(
553
+ left_rows: &[MaterializedTrackedStateRow],
554
+ right_rows: &[MaterializedTrackedStateRow],
555
+ ) -> (StorageContext, TrackedStateContext) {
556
+ let backend = Arc::new(UnitTestBackend::new());
557
+ let storage = StorageContext::new(backend.clone());
558
+ let tracked_state = TrackedStateContext::new();
559
+ let mut tx = storage
560
+ .begin_write_transaction()
561
+ .await
562
+ .expect("transaction should open");
563
+ write_root_for_test(tx.as_mut(), &tracked_state, "left", None, left_rows)
564
+ .await
565
+ .expect("left root should write");
566
+ write_root_for_test(tx.as_mut(), &tracked_state, "right", None, right_rows)
567
+ .await
568
+ .expect("right root should write");
569
+ tx.commit().await.expect("transaction should commit");
570
+ (storage, tracked_state)
571
+ }
572
+
573
+ async fn seed_parent_child_delta(
574
+ parent_rows: &[MaterializedTrackedStateRow],
575
+ child_rows: &[MaterializedTrackedStateRow],
576
+ ) -> (StorageContext, TrackedStateContext) {
577
+ let backend = Arc::new(UnitTestBackend::new());
578
+ let storage = StorageContext::new(backend.clone());
579
+ let tracked_state = TrackedStateContext::new();
580
+ let mut tx = storage
581
+ .begin_write_transaction()
582
+ .await
583
+ .expect("transaction should open");
584
+ write_root_for_test(tx.as_mut(), &tracked_state, "parent", None, parent_rows)
585
+ .await
586
+ .expect("parent should write");
587
+ write_root_for_test(
588
+ tx.as_mut(),
589
+ &tracked_state,
590
+ "child",
591
+ Some("parent"),
592
+ child_rows,
593
+ )
594
+ .await
595
+ .expect("child should write");
596
+ tx.commit().await.expect("transaction should commit");
597
+ (storage, tracked_state)
598
+ }
599
+
600
+ async fn write_root_for_test(
601
+ tx: &mut dyn StorageWriteTransaction,
602
+ tracked_state: &TrackedStateContext,
603
+ commit_id: &str,
604
+ parent_commit_id: Option<&str>,
605
+ rows: &[MaterializedTrackedStateRow],
606
+ ) -> Result<(), LixError> {
607
+ crate::test_support::stage_tracked_root_from_materialized(
608
+ tx,
609
+ tracked_state,
610
+ commit_id,
611
+ parent_commit_id,
612
+ rows,
613
+ )
614
+ .await
615
+ }
616
+
617
+ fn kinds(diff: &TrackedStateDiff) -> Vec<(String, TrackedStateDiffKind)> {
618
+ diff.entries
619
+ .iter()
620
+ .map(|entry| {
621
+ (
622
+ entry
623
+ .identity
624
+ .entity_id
625
+ .as_single_string_owned()
626
+ .expect("identity"),
627
+ entry.kind,
628
+ )
629
+ })
630
+ .collect()
631
+ }
632
+
633
+ fn tombstone(
634
+ entity_id: &str,
635
+ file_id: Option<&str>,
636
+ change_id: &str,
637
+ ) -> MaterializedTrackedStateRow {
638
+ let mut row = row(entity_id, file_id, change_id);
639
+ row.snapshot_content = None;
640
+ row.deleted = true;
641
+ row
642
+ }
643
+
644
+ fn row(entity_id: &str, file_id: Option<&str>, change_id: &str) -> MaterializedTrackedStateRow {
645
+ row_with_schema(entity_id, file_id, "test_schema", change_id)
646
+ }
647
+
648
+ fn row_with_schema(
649
+ entity_id: &str,
650
+ file_id: Option<&str>,
651
+ schema_key: &str,
652
+ change_id: &str,
653
+ ) -> MaterializedTrackedStateRow {
654
+ row_with_schema_and_value(entity_id, file_id, schema_key, change_id, "value")
655
+ }
656
+
657
+ fn row_with_value(
658
+ entity_id: &str,
659
+ file_id: Option<&str>,
660
+ change_id: &str,
661
+ value: &str,
662
+ ) -> MaterializedTrackedStateRow {
663
+ row_with_schema_and_value(entity_id, file_id, "test_schema", change_id, value)
664
+ }
665
+
666
+ fn row_with_schema_and_value(
667
+ entity_id: &str,
668
+ file_id: Option<&str>,
669
+ schema_key: &str,
670
+ change_id: &str,
671
+ value: &str,
672
+ ) -> MaterializedTrackedStateRow {
673
+ MaterializedTrackedStateRow {
674
+ entity_id: EntityIdentity::single(entity_id),
675
+ schema_key: schema_key.to_string(),
676
+ file_id: file_id.map(str::to_string),
677
+ snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
678
+ metadata: None,
679
+ deleted: false,
680
+ created_at: "2026-01-01T00:00:00Z".to_string(),
681
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
682
+ change_id: change_id.to_string(),
683
+ commit_id: change_id.replace("change", "commit"),
684
+ }
685
+ }
686
+ }