@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,6 +1,8 @@
1
1
  use crate::entity_identity::EntityIdentity;
2
- use crate::tracked_state::tree_types::TrackedStateTreeScanRequest;
3
- use crate::tracked_state::{TrackedStateFilter, TrackedStateRow, TrackedStateStoreReader};
2
+ use crate::tracked_state::types::TrackedStateTreeScanRequest;
3
+ use crate::tracked_state::{
4
+ MaterializedTrackedStateRow, TrackedStateFilter, TrackedStateStoreReader,
5
+ };
4
6
  use crate::LixError;
5
7
 
6
8
  /// Filter for comparing two tracked-state commit roots.
@@ -24,12 +26,12 @@ pub(crate) struct TrackedStateDiffEntry {
24
26
  ///
25
27
  /// This can be a tombstone. Callers that need user-visible semantics
26
28
  /// should use `visible_before()` instead of inspecting this directly.
27
- pub(crate) before: Option<TrackedStateRow>,
29
+ pub(crate) before: Option<MaterializedTrackedStateRow>,
28
30
  /// Raw row in the right root.
29
31
  ///
30
32
  /// This can be a tombstone. Keeping the raw tombstone is what lets merge
31
33
  /// apply deletes without reloading the source root.
32
- pub(crate) after: Option<TrackedStateRow>,
34
+ pub(crate) after: Option<MaterializedTrackedStateRow>,
33
35
  }
34
36
 
35
37
  /// Root-local tracked-state identity.
@@ -61,19 +63,35 @@ where
61
63
  S: crate::storage::StorageReader,
62
64
  {
63
65
  let scan_request = scan_request_for_diff(request);
64
- let mut entries = Vec::new();
65
- for tree_entry in reader
66
+ let tree_entries = reader
66
67
  .diff_tree_entries_at_commits(left_commit_id, right_commit_id, &scan_request)
67
- .await?
68
- {
69
- let before = match tree_entry.before {
70
- Some((key, value)) => Some(reader.materialize_tree_value(key, value).await?),
71
- None => None,
72
- };
73
- let after = match tree_entry.after {
74
- Some((key, value)) => Some(reader.materialize_tree_value(key, value).await?),
75
- None => None,
76
- };
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)?;
77
95
  let identity = match before.as_ref().or(after.as_ref()) {
78
96
  Some(row) => TrackedStateDiffIdentity::from_row(row)?,
79
97
  None => continue,
@@ -87,6 +105,26 @@ where
87
105
  Ok(TrackedStateDiff { entries })
88
106
  }
89
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
+
90
128
  fn scan_request_for_diff(request: &TrackedStateDiffRequest) -> TrackedStateTreeScanRequest {
91
129
  let mut filter = request.filter.clone();
92
130
  filter.include_tombstones = true;
@@ -94,15 +132,15 @@ fn scan_request_for_diff(request: &TrackedStateDiffRequest) -> TrackedStateTreeS
94
132
  schema_keys: filter.schema_keys,
95
133
  entity_ids: filter.entity_ids,
96
134
  file_ids: filter.file_ids,
97
- include_tombstones: filter.include_tombstones,
135
+ include_tombstones: true,
98
136
  limit: None,
99
137
  }
100
138
  }
101
139
 
102
140
  fn classify_diff(
103
141
  identity: TrackedStateDiffIdentity,
104
- before: Option<TrackedStateRow>,
105
- after: Option<TrackedStateRow>,
142
+ before: Option<MaterializedTrackedStateRow>,
143
+ after: Option<MaterializedTrackedStateRow>,
106
144
  ) -> Option<TrackedStateDiffEntry> {
107
145
  match (is_live_row(before.as_ref()), is_live_row(after.as_ref())) {
108
146
  (None, None) => None,
@@ -128,18 +166,19 @@ fn classify_diff(
128
166
  }
129
167
  }
130
168
 
131
- fn is_live_row(row: Option<&TrackedStateRow>) -> Option<&TrackedStateRow> {
169
+ fn is_live_row(row: Option<&MaterializedTrackedStateRow>) -> Option<&MaterializedTrackedStateRow> {
132
170
  row.filter(|row| row.snapshot_content.is_some())
133
171
  }
134
172
 
135
- fn tracked_row_payload_eq(left: &TrackedStateRow, right: &TrackedStateRow) -> bool {
136
- left.snapshot_content == right.snapshot_content
137
- && left.metadata == right.metadata
138
- && left.schema_version == right.schema_version
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
139
178
  }
140
179
 
141
180
  impl TrackedStateDiffIdentity {
142
- fn from_row(row: &TrackedStateRow) -> Result<Self, LixError> {
181
+ fn from_row(row: &MaterializedTrackedStateRow) -> Result<Self, LixError> {
143
182
  Ok(Self {
144
183
  schema_key: row.schema_key.clone(),
145
184
  entity_id: row.entity_id.clone(),
@@ -160,14 +199,14 @@ impl TrackedStateDiffEntry {
160
199
  }
161
200
 
162
201
  #[cfg(test)]
163
- pub(crate) fn visible_before(&self) -> Option<&TrackedStateRow> {
202
+ pub(crate) fn visible_before(&self) -> Option<&MaterializedTrackedStateRow> {
164
203
  self.before
165
204
  .as_ref()
166
205
  .filter(|row| row.snapshot_content.is_some())
167
206
  }
168
207
 
169
208
  #[cfg(test)]
170
- pub(crate) fn visible_after(&self) -> Option<&TrackedStateRow> {
209
+ pub(crate) fn visible_after(&self) -> Option<&MaterializedTrackedStateRow> {
171
210
  self.after
172
211
  .as_ref()
173
212
  .filter(|row| row.snapshot_content.is_some())
@@ -180,8 +219,7 @@ mod tests {
180
219
 
181
220
  use super::*;
182
221
  use crate::backend::testing::UnitTestBackend;
183
- use crate::json_store::JsonStoreContext;
184
- use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
222
+ use crate::storage::{StorageContext, StorageWriteTransaction};
185
223
  use crate::tracked_state::TrackedStateContext;
186
224
  use crate::NullableKeyFilter;
187
225
 
@@ -373,6 +411,133 @@ mod tests {
373
411
  );
374
412
  }
375
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
+
376
541
  async fn diff(
377
542
  storage: StorageContext,
378
543
  tracked_state: &TrackedStateContext,
@@ -385,8 +550,8 @@ mod tests {
385
550
  }
386
551
 
387
552
  async fn seed_roots(
388
- left_rows: &[TrackedStateRow],
389
- right_rows: &[TrackedStateRow],
553
+ left_rows: &[MaterializedTrackedStateRow],
554
+ right_rows: &[MaterializedTrackedStateRow],
390
555
  ) -> (StorageContext, TrackedStateContext) {
391
556
  let backend = Arc::new(UnitTestBackend::new());
392
557
  let storage = StorageContext::new(backend.clone());
@@ -405,30 +570,48 @@ mod tests {
405
570
  (storage, tracked_state)
406
571
  }
407
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
+
408
600
  async fn write_root_for_test(
409
601
  tx: &mut dyn StorageWriteTransaction,
410
602
  tracked_state: &TrackedStateContext,
411
603
  commit_id: &str,
412
604
  parent_commit_id: Option<&str>,
413
- rows: &[TrackedStateRow],
605
+ rows: &[MaterializedTrackedStateRow],
414
606
  ) -> Result<(), LixError> {
415
- let mut writes = StorageWriteSet::new();
416
- {
417
- let mut json_writer = JsonStoreContext::new().writer();
418
- tracked_state
419
- .writer()
420
- .stage_root(
421
- tx,
422
- &mut writes,
423
- &mut json_writer,
424
- commit_id,
425
- parent_commit_id,
426
- rows,
427
- )
428
- .await?;
429
- }
430
- writes.apply(tx).await?;
431
- Ok(())
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
432
615
  }
433
616
 
434
617
  fn kinds(diff: &TrackedStateDiff) -> Vec<(String, TrackedStateDiffKind)> {
@@ -436,20 +619,29 @@ mod tests {
436
619
  .iter()
437
620
  .map(|entry| {
438
621
  (
439
- entry.identity.entity_id.as_string().expect("identity"),
622
+ entry
623
+ .identity
624
+ .entity_id
625
+ .as_single_string_owned()
626
+ .expect("identity"),
440
627
  entry.kind,
441
628
  )
442
629
  })
443
630
  .collect()
444
631
  }
445
632
 
446
- fn tombstone(entity_id: &str, file_id: Option<&str>, change_id: &str) -> TrackedStateRow {
633
+ fn tombstone(
634
+ entity_id: &str,
635
+ file_id: Option<&str>,
636
+ change_id: &str,
637
+ ) -> MaterializedTrackedStateRow {
447
638
  let mut row = row(entity_id, file_id, change_id);
448
639
  row.snapshot_content = None;
640
+ row.deleted = true;
449
641
  row
450
642
  }
451
643
 
452
- fn row(entity_id: &str, file_id: Option<&str>, change_id: &str) -> TrackedStateRow {
644
+ fn row(entity_id: &str, file_id: Option<&str>, change_id: &str) -> MaterializedTrackedStateRow {
453
645
  row_with_schema(entity_id, file_id, "test_schema", change_id)
454
646
  }
455
647
 
@@ -458,7 +650,7 @@ mod tests {
458
650
  file_id: Option<&str>,
459
651
  schema_key: &str,
460
652
  change_id: &str,
461
- ) -> TrackedStateRow {
653
+ ) -> MaterializedTrackedStateRow {
462
654
  row_with_schema_and_value(entity_id, file_id, schema_key, change_id, "value")
463
655
  }
464
656
 
@@ -467,7 +659,7 @@ mod tests {
467
659
  file_id: Option<&str>,
468
660
  change_id: &str,
469
661
  value: &str,
470
- ) -> TrackedStateRow {
662
+ ) -> MaterializedTrackedStateRow {
471
663
  row_with_schema_and_value(entity_id, file_id, "test_schema", change_id, value)
472
664
  }
473
665
 
@@ -477,14 +669,14 @@ mod tests {
477
669
  schema_key: &str,
478
670
  change_id: &str,
479
671
  value: &str,
480
- ) -> TrackedStateRow {
481
- TrackedStateRow {
672
+ ) -> MaterializedTrackedStateRow {
673
+ MaterializedTrackedStateRow {
482
674
  entity_id: EntityIdentity::single(entity_id),
483
675
  schema_key: schema_key.to_string(),
484
676
  file_id: file_id.map(str::to_string),
485
677
  snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
486
678
  metadata: None,
487
- schema_version: "1".to_string(),
679
+ deleted: false,
488
680
  created_at: "2026-01-01T00:00:00Z".to_string(),
489
681
  updated_at: "2026-01-01T00:00:00Z".to_string(),
490
682
  change_id: change_id.to_string(),