@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,403 @@
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;
9
+
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)>,
19
+ projection: &TrackedMaterializationProjection,
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>
135
+ where
136
+ S: StorageReader,
137
+ {
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);
217
+ };
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);
271
+ };
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
+ })
305
+ }
306
+
307
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
308
+ pub(crate) struct TrackedMaterializationProjection {
309
+ pub(crate) snapshot_content: bool,
310
+ pub(crate) metadata: bool,
311
+ }
312
+
313
+ impl TrackedMaterializationProjection {
314
+ pub(crate) fn full() -> Self {
315
+ Self {
316
+ snapshot_content: true,
317
+ metadata: true,
318
+ }
319
+ }
320
+
321
+ pub(crate) fn from_columns(columns: &[String]) -> Self {
322
+ if columns.is_empty() {
323
+ return Self::full();
324
+ }
325
+ Self {
326
+ snapshot_content: columns.iter().any(|column| column == "snapshot_content"),
327
+ metadata: columns.iter().any(|column| column == "metadata"),
328
+ }
329
+ }
330
+ }
331
+
332
+ #[cfg(test)]
333
+ mod tests {
334
+ use super::*;
335
+
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
+ }
350
+
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
+ ];
364
+
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
+ }
403
+ }