@lix-js/sdk 0.6.0-preview.2 → 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 (165) hide show
  1. package/SKILL.md +4 -5
  2. package/dist/engine-wasm/wasm/lix_engine.js +1 -1
  3. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  4. package/dist/generated/builtin-schemas.d.ts +87 -162
  5. package/dist/generated/builtin-schemas.js +139 -236
  6. package/dist/open-lix.d.ts +1 -1
  7. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  8. package/dist-engine-src/src/catalog/context.rs +412 -0
  9. package/dist-engine-src/src/catalog/mod.rs +10 -0
  10. package/dist-engine-src/src/catalog/schema.rs +4 -0
  11. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  12. package/dist-engine-src/src/cel/mod.rs +1 -1
  13. package/dist-engine-src/src/cel/provider.rs +1 -1
  14. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  15. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  16. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  17. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  18. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  19. package/dist-engine-src/src/commit_store/context.rs +944 -0
  20. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  21. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  22. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  23. package/dist-engine-src/src/commit_store/types.rs +215 -0
  24. package/dist-engine-src/src/common/identity.rs +15 -5
  25. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  26. package/dist-engine-src/src/common/metadata.rs +17 -12
  27. package/dist-engine-src/src/common/mod.rs +5 -5
  28. package/dist-engine-src/src/domain.rs +324 -0
  29. package/dist-engine-src/src/engine.rs +29 -43
  30. package/dist-engine-src/src/entity_identity.rs +238 -118
  31. package/dist-engine-src/src/functions/context.rs +17 -52
  32. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  33. package/dist-engine-src/src/functions/mod.rs +1 -1
  34. package/dist-engine-src/src/functions/provider.rs +4 -4
  35. package/dist-engine-src/src/functions/state.rs +39 -66
  36. package/dist-engine-src/src/functions/types.rs +1 -1
  37. package/dist-engine-src/src/init.rs +204 -151
  38. package/dist-engine-src/src/json_store/context.rs +354 -60
  39. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  40. package/dist-engine-src/src/json_store/mod.rs +4 -1
  41. package/dist-engine-src/src/json_store/store.rs +884 -11
  42. package/dist-engine-src/src/json_store/types.rs +166 -1
  43. package/dist-engine-src/src/lib.rs +10 -9
  44. package/dist-engine-src/src/live_state/context.rs +608 -830
  45. package/dist-engine-src/src/live_state/mod.rs +3 -3
  46. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  47. package/dist-engine-src/src/live_state/reader.rs +5 -5
  48. package/dist-engine-src/src/live_state/types.rs +19 -36
  49. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  50. package/dist-engine-src/src/plugin/archive.rs +3 -6
  51. package/dist-engine-src/src/plugin/install.rs +0 -18
  52. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  53. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  54. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  55. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  56. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  57. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  58. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  60. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  61. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  63. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  64. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  65. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  66. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  67. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  68. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  69. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  70. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  71. package/dist-engine-src/src/schema/definition.json +47 -17
  72. package/dist-engine-src/src/schema/definition.rs +202 -96
  73. package/dist-engine-src/src/schema/key.rs +9 -77
  74. package/dist-engine-src/src/schema/mod.rs +4 -4
  75. package/dist-engine-src/src/schema/tests.rs +133 -92
  76. package/dist-engine-src/src/session/context.rs +40 -42
  77. package/dist-engine-src/src/session/create_version.rs +22 -14
  78. package/dist-engine-src/src/session/execute.rs +45 -14
  79. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  80. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  81. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  82. package/dist-engine-src/src/session/merge/version.rs +35 -45
  83. package/dist-engine-src/src/session/mod.rs +4 -2
  84. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  85. package/dist-engine-src/src/session/switch_version.rs +16 -28
  86. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  87. package/dist-engine-src/src/sql2/classify.rs +61 -26
  88. package/dist-engine-src/src/sql2/context.rs +22 -18
  89. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  90. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  91. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  92. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  93. package/dist-engine-src/src/sql2/error.rs +21 -1
  94. package/dist-engine-src/src/sql2/execute.rs +325 -264
  95. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  96. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  97. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  98. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  99. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  100. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  101. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  102. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  103. package/dist-engine-src/src/sql2/mod.rs +6 -3
  104. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  105. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  106. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  107. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  108. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  109. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  110. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  111. package/dist-engine-src/src/sql2/session.rs +7 -10
  112. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  113. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  114. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  115. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  116. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  117. package/dist-engine-src/src/storage_bench.rs +1782 -325
  118. package/dist-engine-src/src/test_support.rs +183 -36
  119. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  120. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  121. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  122. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  123. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  124. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  125. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  126. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  127. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  128. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  129. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  130. package/dist-engine-src/src/transaction/commit.rs +695 -435
  131. package/dist-engine-src/src/transaction/context.rs +551 -310
  132. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  133. package/dist-engine-src/src/transaction/mod.rs +2 -0
  134. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  135. package/dist-engine-src/src/transaction/prep.rs +37 -0
  136. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  137. package/dist-engine-src/src/transaction/staging.rs +701 -406
  138. package/dist-engine-src/src/transaction/types.rs +231 -122
  139. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  140. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  141. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  142. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  143. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  144. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  145. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  146. package/dist-engine-src/src/version/context.rs +1 -13
  147. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  148. package/dist-engine-src/src/version/mod.rs +3 -2
  149. package/dist-engine-src/src/version/refs.rs +12 -103
  150. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  151. package/package.json +1 -1
  152. package/dist-engine-src/src/changelog/codec.rs +0 -321
  153. package/dist-engine-src/src/changelog/context.rs +0 -92
  154. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  155. package/dist-engine-src/src/changelog/mod.rs +0 -13
  156. package/dist-engine-src/src/changelog/reader.rs +0 -20
  157. package/dist-engine-src/src/changelog/storage.rs +0 -220
  158. package/dist-engine-src/src/changelog/types.rs +0 -38
  159. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  160. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  161. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  162. package/dist-engine-src/src/schema_registry.rs +0 -294
  163. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  164. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  165. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,43 +1,307 @@
1
- use crate::json_store::{JsonRef, JsonStoreReader, JsonStoreWriter};
2
- use crate::storage::{StorageReader, StorageWriteSet};
3
- use crate::tracked_state::tree_types::{TrackedStateKey, TrackedStateValue};
4
- use crate::tracked_state::TrackedStateRow;
5
- use crate::{serialize_row_metadata, validate_row_metadata, LixError, RowMetadata};
6
-
7
- pub(crate) fn canonicalize_materialized_row(
8
- writes: &mut StorageWriteSet,
9
- json_writer: &mut JsonStoreWriter,
10
- row: &TrackedStateRow,
11
- ) -> Result<TrackedStateValue, LixError> {
12
- let snapshot_ref = stage_optional_json(writes, json_writer, row.snapshot_content.as_deref())?;
13
- let metadata_ref = stage_optional_metadata(writes, json_writer, row.metadata.as_ref())?;
14
- Ok(TrackedStateValue::from_row_refs(
15
- row,
16
- snapshot_ref,
17
- metadata_ref,
18
- ))
19
- }
1
+ use crate::entity_identity::EntityIdentity;
2
+ use crate::json_store::JsonRef;
3
+ use crate::json_store::{JsonLoadRequestRef, JsonReadScopeRef, JsonStoreContext};
4
+ use crate::storage::StorageReader;
5
+ use crate::tracked_state::types::{TrackedStateIndexValue, TrackedStateKey};
6
+ use crate::tracked_state::MaterializedTrackedStateRow;
7
+ use crate::LixError;
8
+ use std::collections::BTreeMap;
20
9
 
21
- pub(crate) async fn materialize_value<S>(
22
- json_reader: &mut JsonStoreReader<S>,
23
- key: TrackedStateKey,
24
- value: TrackedStateValue,
10
+ /// Materializes tracked-state index entries.
11
+ ///
12
+ /// The durable tracked_state value is authoritative for scalar projection
13
+ /// fields and stores the JSON refs needed for payload projections. Snapshot and
14
+ /// metadata bytes are hydrated from grouped json_store loads only when the
15
+ /// requested projection needs them.
16
+ pub(crate) async fn materialize_index_entries<S>(
17
+ store: &mut S,
18
+ entries: Vec<(TrackedStateKey, TrackedStateIndexValue)>,
25
19
  projection: &TrackedMaterializationProjection,
26
- ) -> Result<TrackedStateRow, LixError>
20
+ ) -> Result<Vec<MaterializedTrackedStateRow>, LixError>
21
+ where
22
+ S: StorageReader,
23
+ {
24
+ if !projection.snapshot_content && !projection.metadata {
25
+ return Ok(entries
26
+ .into_iter()
27
+ .map(materialize_entry_without_json)
28
+ .collect());
29
+ }
30
+
31
+ let json_slots_per_row =
32
+ usize::from(projection.snapshot_content) + usize::from(projection.metadata);
33
+ let json_ref_capacity = entries.len().saturating_mul(json_slots_per_row);
34
+ let mut row_plans = Vec::with_capacity(entries.len());
35
+ let mut json_refs = Vec::with_capacity(json_ref_capacity);
36
+ let mut json_ref_localities = Vec::with_capacity(json_ref_capacity);
37
+ for (key, value) in entries {
38
+ let row_index = row_plans.len();
39
+ let snapshot_ref_index = projected_json_ref_index(
40
+ projection.snapshot_content,
41
+ value.snapshot_ref,
42
+ row_index,
43
+ value.change_locator.source_pack_id,
44
+ &mut json_refs,
45
+ &mut json_ref_localities,
46
+ );
47
+ let metadata_ref_index = projected_json_ref_index(
48
+ projection.metadata,
49
+ value.metadata_ref,
50
+ row_index,
51
+ value.change_locator.source_pack_id,
52
+ &mut json_refs,
53
+ &mut json_ref_localities,
54
+ );
55
+ row_plans.push(MaterializedTrackedStateRowPlan {
56
+ entity_id: key.entity_id,
57
+ schema_key: key.schema_key,
58
+ file_id: key.file_id,
59
+ deleted: value.deleted,
60
+ created_at: value.created_at,
61
+ updated_at: value.updated_at,
62
+ change_id: value.change_locator.change_id,
63
+ commit_id: value.change_locator.source_commit_id,
64
+ snapshot_ref_index,
65
+ metadata_ref_index,
66
+ });
67
+ }
68
+
69
+ let mut json_values =
70
+ load_projection_json_values(store, &json_refs, &json_ref_localities, &row_plans).await?;
71
+ row_plans
72
+ .into_iter()
73
+ .map(|plan| materialize_row_plan(plan, &json_refs, &mut json_values))
74
+ .collect()
75
+ }
76
+
77
+ fn materialize_entry_without_json(
78
+ (key, value): (TrackedStateKey, TrackedStateIndexValue),
79
+ ) -> MaterializedTrackedStateRow {
80
+ MaterializedTrackedStateRow {
81
+ entity_id: key.entity_id,
82
+ schema_key: key.schema_key,
83
+ file_id: key.file_id,
84
+ snapshot_content: None,
85
+ metadata: None,
86
+ deleted: value.deleted,
87
+ created_at: value.created_at,
88
+ updated_at: value.updated_at,
89
+ change_id: value.change_locator.change_id,
90
+ commit_id: value.change_locator.source_commit_id,
91
+ }
92
+ }
93
+
94
+ struct MaterializedTrackedStateRowPlan {
95
+ entity_id: EntityIdentity,
96
+ schema_key: String,
97
+ file_id: Option<String>,
98
+ deleted: bool,
99
+ created_at: String,
100
+ updated_at: String,
101
+ change_id: String,
102
+ commit_id: String,
103
+ snapshot_ref_index: Option<usize>,
104
+ metadata_ref_index: Option<usize>,
105
+ }
106
+
107
+ fn projected_json_ref_index(
108
+ include: bool,
109
+ json_ref: Option<JsonRef>,
110
+ row_index: usize,
111
+ pack_id: u32,
112
+ json_refs: &mut Vec<JsonRef>,
113
+ json_ref_localities: &mut Vec<JsonRefLocality>,
114
+ ) -> Option<usize> {
115
+ if !include {
116
+ return None;
117
+ }
118
+ let index = json_refs.len();
119
+ json_refs.push(json_ref?);
120
+ json_ref_localities.push(JsonRefLocality { row_index, pack_id });
121
+ Some(index)
122
+ }
123
+
124
+ struct JsonRefLocality {
125
+ row_index: usize,
126
+ pack_id: u32,
127
+ }
128
+
129
+ async fn load_projection_json_values<S>(
130
+ store: &mut S,
131
+ json_refs: &[JsonRef],
132
+ json_ref_localities: &[JsonRefLocality],
133
+ row_plans: &[MaterializedTrackedStateRowPlan],
134
+ ) -> Result<Vec<Option<Vec<u8>>>, LixError>
27
135
  where
28
136
  S: StorageReader,
29
137
  {
30
- let snapshot_content = if projection.snapshot_content {
31
- load_optional_json(json_reader, value.snapshot_ref.as_ref(), "snapshot_ref").await?
32
- } else {
33
- None
138
+ if json_refs.len() != json_ref_localities.len() {
139
+ return Err(LixError::new(
140
+ LixError::CODE_INTERNAL_ERROR,
141
+ "tracked_state materialization JSON refs and locality indexes diverged",
142
+ ));
143
+ }
144
+
145
+ let json_store = JsonStoreContext::new();
146
+ if let Some((commit_id, pack_id)) = single_projection_pack(json_ref_localities, row_plans)? {
147
+ let pack_ids = [pack_id];
148
+ return json_store
149
+ .load_bytes_many(
150
+ store,
151
+ JsonLoadRequestRef {
152
+ refs: json_refs,
153
+ scope: JsonReadScopeRef::CommitPacks {
154
+ commit_id,
155
+ pack_ids: &pack_ids,
156
+ },
157
+ },
158
+ )
159
+ .await
160
+ .map(|batch| batch.into_values());
161
+ }
162
+
163
+ let mut json_values = vec![None; json_refs.len()];
164
+ let mut refs_by_pack = BTreeMap::<(&str, u32), Vec<(usize, JsonRef)>>::new();
165
+ for (index, json_ref) in json_refs.iter().copied().enumerate() {
166
+ let locality = json_ref_localities.get(index).ok_or_else(|| {
167
+ LixError::new(
168
+ LixError::CODE_INTERNAL_ERROR,
169
+ "tracked_state materialization lost JSON locality index",
170
+ )
171
+ })?;
172
+ let row_plan = row_plans.get(locality.row_index).ok_or_else(|| {
173
+ LixError::new(
174
+ LixError::CODE_INTERNAL_ERROR,
175
+ "tracked_state materialization lost JSON row locality index",
176
+ )
177
+ })?;
178
+ refs_by_pack
179
+ .entry((row_plan.commit_id.as_str(), locality.pack_id))
180
+ .or_default()
181
+ .push((index, json_ref));
182
+ }
183
+
184
+ for ((commit_id, pack_id), refs) in refs_by_pack {
185
+ let indexes = refs.iter().map(|(index, _)| *index).collect::<Vec<_>>();
186
+ let refs = refs
187
+ .into_iter()
188
+ .map(|(_, json_ref)| json_ref)
189
+ .collect::<Vec<_>>();
190
+ let pack_ids = [pack_id];
191
+ let values = json_store
192
+ .load_bytes_many(
193
+ store,
194
+ JsonLoadRequestRef {
195
+ refs: &refs,
196
+ scope: JsonReadScopeRef::CommitPacks {
197
+ commit_id: &commit_id,
198
+ pack_ids: &pack_ids,
199
+ },
200
+ },
201
+ )
202
+ .await?
203
+ .into_values();
204
+ for (index, value) in indexes.into_iter().zip(values) {
205
+ json_values[index] = value;
206
+ }
207
+ }
208
+ Ok(json_values)
209
+ }
210
+
211
+ fn single_projection_pack<'a>(
212
+ json_ref_localities: &[JsonRefLocality],
213
+ row_plans: &'a [MaterializedTrackedStateRowPlan],
214
+ ) -> Result<Option<(&'a str, u32)>, LixError> {
215
+ let Some(first_locality) = json_ref_localities.first() else {
216
+ return Ok(None);
34
217
  };
35
- let metadata = if projection.metadata {
36
- load_optional_metadata(json_reader, value.metadata_ref.as_ref()).await?
37
- } else {
38
- None
218
+ let first_plan = row_plans.get(first_locality.row_index).ok_or_else(|| {
219
+ LixError::new(
220
+ LixError::CODE_INTERNAL_ERROR,
221
+ "tracked_state materialization lost JSON row locality index",
222
+ )
223
+ })?;
224
+ let commit_id = first_plan.commit_id.as_str();
225
+ let pack_id = first_locality.pack_id;
226
+
227
+ for locality in &json_ref_localities[1..] {
228
+ let row_plan = row_plans.get(locality.row_index).ok_or_else(|| {
229
+ LixError::new(
230
+ LixError::CODE_INTERNAL_ERROR,
231
+ "tracked_state materialization lost JSON row locality index",
232
+ )
233
+ })?;
234
+ if row_plan.commit_id != commit_id || locality.pack_id != pack_id {
235
+ return Ok(None);
236
+ }
237
+ }
238
+ Ok(Some((commit_id, pack_id)))
239
+ }
240
+
241
+ fn materialize_row_plan(
242
+ plan: MaterializedTrackedStateRowPlan,
243
+ json_refs: &[JsonRef],
244
+ json_values: &mut [Option<Vec<u8>>],
245
+ ) -> Result<MaterializedTrackedStateRow, LixError> {
246
+ Ok(MaterializedTrackedStateRow {
247
+ entity_id: plan.entity_id,
248
+ schema_key: plan.schema_key,
249
+ file_id: plan.file_id,
250
+ snapshot_content: materialized_json_string(
251
+ plan.snapshot_ref_index,
252
+ json_refs,
253
+ json_values,
254
+ )?,
255
+ metadata: materialized_json_string(plan.metadata_ref_index, json_refs, json_values)?,
256
+ deleted: plan.deleted,
257
+ created_at: plan.created_at,
258
+ updated_at: plan.updated_at,
259
+ change_id: plan.change_id,
260
+ commit_id: plan.commit_id,
261
+ })
262
+ }
263
+
264
+ fn materialized_json_string(
265
+ index: Option<usize>,
266
+ json_refs: &[JsonRef],
267
+ json_values: &mut [Option<Vec<u8>>],
268
+ ) -> Result<Option<String>, LixError> {
269
+ let Some(index) = index else {
270
+ return Ok(None);
39
271
  };
40
- Ok(value.into_materialized_row(key, snapshot_content, metadata))
272
+ let json_ref = json_refs.get(index).ok_or_else(|| {
273
+ LixError::new(
274
+ LixError::CODE_INTERNAL_ERROR,
275
+ "tracked_state materialization lost JSON ref index",
276
+ )
277
+ })?;
278
+ // Each row plan owns its projected JSON slots. If this path starts
279
+ // deduplicating refs, duplicate consumers must clone intentionally.
280
+ let bytes = json_values
281
+ .get_mut(index)
282
+ .ok_or_else(|| {
283
+ LixError::new(
284
+ LixError::CODE_INTERNAL_ERROR,
285
+ "tracked_state materialization lost JSON value index",
286
+ )
287
+ })?
288
+ .take()
289
+ .ok_or_else(|| {
290
+ LixError::new(
291
+ LixError::CODE_INTERNAL_ERROR,
292
+ format!(
293
+ "tracked_state materialization missing JSON payload '{}'",
294
+ json_ref.to_hex()
295
+ ),
296
+ )
297
+ })?;
298
+ String::from_utf8(bytes).map(Some).map_err(|error| {
299
+ let utf8_error = error.utf8_error();
300
+ LixError::new(
301
+ LixError::CODE_INTERNAL_ERROR,
302
+ format!("tracked_state materialized JSON payload is not UTF-8: {utf8_error}"),
303
+ )
304
+ })
41
305
  }
42
306
 
43
307
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -65,77 +329,75 @@ impl TrackedMaterializationProjection {
65
329
  }
66
330
  }
67
331
 
68
- fn stage_optional_json(
69
- writes: &mut StorageWriteSet,
70
- json_writer: &mut JsonStoreWriter,
71
- value: Option<&str>,
72
- ) -> Result<Option<JsonRef>, LixError> {
73
- let Some(value) = value else {
74
- return Ok(None);
75
- };
76
- json_writer.stage_bytes(writes, value.as_bytes()).map(Some)
77
- }
332
+ #[cfg(test)]
333
+ mod tests {
334
+ use super::*;
78
335
 
79
- fn stage_optional_metadata(
80
- writes: &mut StorageWriteSet,
81
- json_writer: &mut JsonStoreWriter,
82
- value: Option<&RowMetadata>,
83
- ) -> Result<Option<JsonRef>, LixError> {
84
- let Some(value) = value else {
85
- return Ok(None);
86
- };
87
- let serialized = serialize_row_metadata(value);
88
- json_writer
89
- .stage_bytes(writes, serialized.as_bytes())
90
- .map(Some)
91
- }
336
+ fn row_plan(commit_id: &str) -> MaterializedTrackedStateRowPlan {
337
+ MaterializedTrackedStateRowPlan {
338
+ entity_id: EntityIdentity::single("entity"),
339
+ schema_key: "schema".to_string(),
340
+ file_id: None,
341
+ deleted: false,
342
+ created_at: "2024-01-01T00:00:00.000Z".to_string(),
343
+ updated_at: "2024-01-01T00:00:00.000Z".to_string(),
344
+ change_id: "change".to_string(),
345
+ commit_id: commit_id.to_string(),
346
+ snapshot_ref_index: None,
347
+ metadata_ref_index: None,
348
+ }
349
+ }
92
350
 
93
- async fn load_optional_metadata<S>(
94
- json_reader: &mut JsonStoreReader<S>,
95
- json_ref: Option<&JsonRef>,
96
- ) -> Result<Option<RowMetadata>, LixError>
97
- where
98
- S: StorageReader,
99
- {
100
- let Some(json) = load_optional_json(json_reader, json_ref, "metadata_ref").await? else {
101
- return Ok(None);
102
- };
103
- let metadata = serde_json::from_str::<RowMetadata>(&json).map_err(|error| {
104
- LixError::new(
105
- "LIX_ERROR_INVALID_JSON",
106
- format!("tracked_state metadata_ref is invalid JSON: {error}"),
107
- )
108
- })?;
109
- validate_row_metadata(metadata, "tracked_state metadata_ref").map(Some)
110
- }
351
+ #[test]
352
+ fn single_projection_pack_accepts_duplicate_slots_from_same_pack() {
353
+ let row_plans = vec![row_plan("commit-a")];
354
+ let localities = vec![
355
+ JsonRefLocality {
356
+ row_index: 0,
357
+ pack_id: 7,
358
+ },
359
+ JsonRefLocality {
360
+ row_index: 0,
361
+ pack_id: 7,
362
+ },
363
+ ];
111
364
 
112
- async fn load_optional_json<S>(
113
- json_reader: &mut JsonStoreReader<S>,
114
- json_ref: Option<&JsonRef>,
115
- field: &str,
116
- ) -> Result<Option<String>, LixError>
117
- where
118
- S: StorageReader,
119
- {
120
- let Some(json_ref) = json_ref else {
121
- return Ok(None);
122
- };
123
- let bytes = json_reader.load_bytes(json_ref).await?.ok_or_else(|| {
124
- LixError::new(
125
- "LIX_ERROR_UNKNOWN",
126
- format!(
127
- "tracked_state {field} '{}' is missing from json_store",
128
- json_ref.to_hex()
129
- ),
130
- )
131
- })?;
132
- String::from_utf8(bytes).map(Some).map_err(|error| {
133
- LixError::new(
134
- "LIX_ERROR_UNKNOWN",
135
- format!(
136
- "tracked_state {field} '{}' is not valid UTF-8 JSON bytes: {error}",
137
- json_ref.to_hex()
138
- ),
139
- )
140
- })
365
+ assert_eq!(
366
+ single_projection_pack(&localities, &row_plans).expect("pack detection should succeed"),
367
+ Some(("commit-a", 7))
368
+ );
369
+ }
370
+
371
+ #[test]
372
+ fn single_projection_pack_rejects_mixed_packs() {
373
+ let row_plans = vec![row_plan("commit-a")];
374
+ let localities = vec![
375
+ JsonRefLocality {
376
+ row_index: 0,
377
+ pack_id: 7,
378
+ },
379
+ JsonRefLocality {
380
+ row_index: 0,
381
+ pack_id: 8,
382
+ },
383
+ ];
384
+
385
+ assert_eq!(
386
+ single_projection_pack(&localities, &row_plans).expect("pack detection should succeed"),
387
+ None
388
+ );
389
+ }
390
+
391
+ #[test]
392
+ fn materialized_json_string_consumes_owned_payload_bytes() {
393
+ let json = br#"{"value":1}"#.to_vec();
394
+ let json_ref = JsonRef::for_content(&json);
395
+ let mut json_values = vec![Some(json)];
396
+
397
+ let materialized = materialized_json_string(Some(0), &[json_ref], &mut json_values)
398
+ .expect("json should materialize");
399
+
400
+ assert_eq!(materialized, Some(r#"{"value":1}"#.to_string()));
401
+ assert!(json_values[0].is_none());
402
+ }
141
403
  }