@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5

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 (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -1,49 +1,24 @@
1
- use std::collections::HashMap;
2
-
3
1
  use xxhash_rust::xxh3::xxh3_64_with_seed;
4
2
 
5
- use crate::commit_store::ChangeLocator;
6
- use crate::entity_identity::EntityIdentity;
3
+ use crate::entity_pk::EntityPk;
7
4
  use crate::json_store::JsonRef;
8
5
  use crate::tracked_state::types::{
9
- TrackedStateDeltaEntry, TrackedStateDeltaRef, TrackedStateIndexValue,
10
- TrackedStateIndexValueRef, TrackedStateKey, TrackedStateKeyRef, TRACKED_STATE_HASH_BYTES,
6
+ TrackedStateIndexValue, TrackedStateIndexValueRef, TrackedStateKey, TrackedStateKeyRef,
7
+ TRACKED_STATE_HASH_BYTES,
11
8
  };
12
9
  use crate::LixError;
13
10
 
14
- const NODE_VERSION: u8 = 2;
15
- const VALUE_VERSION: u8 = 7;
11
+ const NODE_BRANCH: u8 = 2;
12
+ const VALUE_BRANCH: u8 = 8;
16
13
  const VALUE_DELETED_FLAG: u8 = 0b1000_0000;
17
- const VALUE_VERSION_MASK: u8 = 0b0111_1111;
18
- const DELTA_PACK_VERSION: u8 = 7;
19
- const DELTA_LOCATOR_SAME_COMMIT: u8 = 0;
20
- const DELTA_LOCATOR_FULL: u8 = 1;
21
- const DELTA_JSON_REFS_INLINE: u8 = 0;
22
- const DELTA_JSON_REFS_MIXED_PACK_INDEX: u8 = 1;
23
- const DELTA_JSON_REF_NONE: u8 = 0;
24
- const DELTA_JSON_REF_PACK_INDEX: u8 = 1;
25
- const DELTA_JSON_REF_INLINE: u8 = 2;
26
- const DELTA_CHANGE_ID_FULL: u8 = 0;
27
- const DELTA_CHANGE_ID_COMMIT_SUFFIX: u8 = 1;
14
+ const VALUE_BRANCH_MASK: u8 = 0b0111_1111;
28
15
  const TIMESTAMP_UPDATED_SAME: u8 = 0;
29
16
  const TIMESTAMP_UPDATED_DISTINCT: u8 = 1;
30
17
  const NODE_KIND_LEAF: u8 = 1;
31
18
  const NODE_KIND_INTERNAL: u8 = 2;
32
19
  const WEIBULL_K: i32 = 4;
33
- const ENTITY_IDENTITY_END: u8 = 0;
34
- const ENTITY_IDENTITY_STRING: u8 = 1;
35
-
36
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
37
- struct DeltaKeyPrefixRef<'a> {
38
- schema_key: &'a str,
39
- file_id: Option<&'a str>,
40
- }
41
-
42
- #[derive(Debug, Clone, PartialEq, Eq)]
43
- struct DeltaKeyPrefix {
44
- schema_key: String,
45
- file_id: Option<String>,
46
- }
20
+ const ENTITY_PKENTITY_END: u8 = 0;
21
+ const ENTITY_PKENTITY_STRING: u8 = 1;
47
22
 
48
23
  #[derive(Debug, Clone, PartialEq, Eq)]
49
24
  pub(crate) struct EncodedLeafEntry {
@@ -185,7 +160,7 @@ pub(crate) fn encode_key(key: &TrackedStateKey) -> Vec<u8> {
185
160
  encode_key_ref(TrackedStateKeyRef {
186
161
  schema_key: &key.schema_key,
187
162
  file_id: key.file_id.as_deref(),
188
- entity_id: &key.entity_id,
163
+ entity_pk: &key.entity_pk,
189
164
  })
190
165
  }
191
166
 
@@ -204,7 +179,7 @@ fn append_key_ref(out: &mut Vec<u8>, key: TrackedStateKeyRef<'_>) {
204
179
  }
205
180
  None => out.push(0),
206
181
  }
207
- push_entity_identity(out, key.entity_id);
182
+ push_entity_pk(out, key.entity_pk);
208
183
  }
209
184
 
210
185
  pub(crate) fn encode_schema_key_prefix(schema_key: &str) -> Vec<u8> {
@@ -238,7 +213,7 @@ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
238
213
  ))
239
214
  }
240
215
  };
241
- let entity_id = read_entity_identity(bytes, &mut cursor)?;
216
+ let entity_pk = read_entity_pk(bytes, &mut cursor)?;
242
217
  if cursor != bytes.len() {
243
218
  return Err(LixError::new(
244
219
  "LIX_ERROR_UNKNOWN",
@@ -248,14 +223,14 @@ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
248
223
  Ok(TrackedStateKey {
249
224
  schema_key,
250
225
  file_id,
251
- entity_id,
226
+ entity_pk,
252
227
  })
253
228
  }
254
229
 
255
230
  /// Decodes a key after the caller has already proven the schema/file prefix.
256
231
  ///
257
232
  /// This is for scan paths that have matched an encoded prefix range and only
258
- /// need to materialize the entity suffix plus the known projection fields.
233
+ /// need to materialize the entity suffix plus the selected columns.
259
234
  pub(crate) fn decode_key_with_trusted_prefix(
260
235
  bytes: &[u8],
261
236
  schema_key: &str,
@@ -263,7 +238,7 @@ pub(crate) fn decode_key_with_trusted_prefix(
263
238
  prefix_len: usize,
264
239
  ) -> Result<TrackedStateKey, LixError> {
265
240
  let mut cursor = prefix_len;
266
- let entity_id = read_entity_identity(bytes, &mut cursor)?;
241
+ let entity_pk = read_entity_pk(bytes, &mut cursor)?;
267
242
  if cursor != bytes.len() {
268
243
  return Err(LixError::new(
269
244
  "LIX_ERROR_UNKNOWN",
@@ -273,14 +248,15 @@ pub(crate) fn decode_key_with_trusted_prefix(
273
248
  Ok(TrackedStateKey {
274
249
  schema_key: schema_key.to_string(),
275
250
  file_id: file_id.map(str::to_string),
276
- entity_id,
251
+ entity_pk,
277
252
  })
278
253
  }
279
254
 
280
255
  #[cfg(test)]
281
256
  pub(crate) fn encode_value(value: &TrackedStateIndexValue) -> Vec<u8> {
282
257
  encode_value_ref(TrackedStateIndexValueRef {
283
- change_locator: value.change_locator.as_ref(),
258
+ change_id: &value.change_id,
259
+ commit_id: &value.commit_id,
284
260
  deleted: value.deleted,
285
261
  snapshot_ref: value.snapshot_ref.as_ref(),
286
262
  metadata_ref: value.metadata_ref.as_ref(),
@@ -296,11 +272,9 @@ pub(crate) fn encode_value_ref(value: TrackedStateIndexValueRef<'_>) -> Vec<u8>
296
272
  }
297
273
 
298
274
  fn append_value_ref(out: &mut Vec<u8>, value: TrackedStateIndexValueRef<'_>) {
299
- out.push(VALUE_VERSION | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
300
- push_sized_bytes(out, value.change_locator.source_commit_id.as_bytes());
301
- out.extend_from_slice(&value.change_locator.source_pack_id.to_be_bytes());
302
- out.extend_from_slice(&value.change_locator.source_ordinal.to_be_bytes());
303
- push_sized_bytes(out, value.change_locator.change_id.as_bytes());
275
+ out.push(VALUE_BRANCH | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
276
+ push_sized_bytes(out, value.change_id.as_bytes());
277
+ push_sized_bytes(out, value.commit_id.as_bytes());
304
278
  push_timestamp_pair(out, value.created_at, value.updated_at);
305
279
  push_optional_json_ref(out, value.snapshot_ref);
306
280
  push_optional_json_ref(out, value.metadata_ref);
@@ -308,10 +282,8 @@ fn append_value_ref(out: &mut Vec<u8>, value: TrackedStateIndexValueRef<'_>) {
308
282
 
309
283
  #[cfg(test)]
310
284
  pub(crate) fn encoded_value_len(value: &TrackedStateIndexValue) -> usize {
311
- 1 + sized_bytes_len(value.change_locator.source_commit_id.as_bytes())
312
- + 4
313
- + 4
314
- + sized_bytes_len(value.change_locator.change_id.as_bytes())
285
+ 1 + sized_bytes_len(value.change_id.as_bytes())
286
+ + sized_bytes_len(value.commit_id.as_bytes())
315
287
  + timestamp_pair_len(&value.created_at, &value.updated_at)
316
288
  + optional_json_ref_len(value.snapshot_ref.as_ref())
317
289
  + optional_json_ref_len(value.metadata_ref.as_ref())
@@ -338,12 +310,12 @@ pub(crate) fn decode_visible_value(
338
310
  }
339
311
 
340
312
  fn decode_value_header(value_header: u8) -> Result<bool, LixError> {
341
- let version = value_header & VALUE_VERSION_MASK;
313
+ let branch = value_header & VALUE_BRANCH_MASK;
342
314
  let deleted = value_header & VALUE_DELETED_FLAG != 0;
343
- if version != VALUE_VERSION {
315
+ if branch != VALUE_BRANCH {
344
316
  return Err(LixError::new(
345
317
  "LIX_ERROR_UNKNOWN",
346
- format!("unsupported tracked-state tree value version {version}"),
318
+ format!("unsupported tracked-state tree value branch {branch}"),
347
319
  ));
348
320
  }
349
321
  Ok(deleted)
@@ -354,22 +326,8 @@ fn decode_value_after_header(
354
326
  mut cursor: usize,
355
327
  deleted: bool,
356
328
  ) -> Result<TrackedStateIndexValue, LixError> {
357
- let source_commit_id = read_sized_string(bytes, &mut cursor, "source_commit_id")?;
358
- let source_pack_id =
359
- u32::try_from(read_u32(bytes, &mut cursor, "source_pack_id")?).map_err(|_| {
360
- LixError::new(
361
- LixError::CODE_INTERNAL_ERROR,
362
- "tracked-state source_pack_id exceeds u32",
363
- )
364
- })?;
365
- let source_ordinal =
366
- u32::try_from(read_u32(bytes, &mut cursor, "source_ordinal")?).map_err(|_| {
367
- LixError::new(
368
- LixError::CODE_INTERNAL_ERROR,
369
- "tracked-state source_ordinal exceeds u32",
370
- )
371
- })?;
372
329
  let change_id = read_sized_string(bytes, &mut cursor, "change_id")?;
330
+ let commit_id = read_sized_string(bytes, &mut cursor, "commit_id")?;
373
331
  let (created_at, updated_at) = read_timestamp_pair(bytes, &mut cursor)?;
374
332
  let snapshot_ref = read_optional_json_ref(bytes, &mut cursor, "snapshot_ref")?;
375
333
  let metadata_ref = read_optional_json_ref(bytes, &mut cursor, "metadata_ref")?;
@@ -380,387 +338,8 @@ fn decode_value_after_header(
380
338
  ));
381
339
  }
382
340
  Ok(TrackedStateIndexValue {
383
- change_locator: ChangeLocator {
384
- source_commit_id,
385
- source_pack_id,
386
- source_ordinal,
387
- change_id,
388
- },
389
- deleted,
390
- snapshot_ref,
391
- metadata_ref,
392
- created_at,
393
- updated_at,
394
- })
395
- }
396
-
397
- pub(crate) fn encode_delta_pack_refs(
398
- commit_id: &str,
399
- deltas: &[TrackedStateDeltaRef<'_>],
400
- ) -> Result<Vec<u8>, LixError> {
401
- encode_delta_pack_refs_with_json_pack_indexes(commit_id, deltas, None)
402
- }
403
-
404
- pub(crate) fn encode_delta_pack_refs_with_json_pack_indexes(
405
- commit_id: &str,
406
- deltas: &[TrackedStateDeltaRef<'_>],
407
- json_pack_indexes: Option<&HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>>,
408
- ) -> Result<Vec<u8>, LixError> {
409
- let json_pack_indexes = json_pack_indexes.filter(|indexes| !indexes.is_empty());
410
- let mut out = Vec::new();
411
- out.extend_from_slice(b"LXTD");
412
- out.push(DELTA_PACK_VERSION);
413
- push_var_sized_bytes(&mut out, commit_id.as_bytes(), "delta pack commit_id")?;
414
- let (key_prefixes, delta_prefix_indexes) = delta_key_prefixes(deltas);
415
- push_var_u32(&mut out, key_prefixes.len(), "delta key prefix count")?;
416
- for prefix in &key_prefixes {
417
- append_delta_key_prefix_ref(&mut out, *prefix)?;
418
- }
419
- push_var_u32(&mut out, deltas.len(), "delta pack entry count")?;
420
- out.push(if json_pack_indexes.is_some() {
421
- DELTA_JSON_REFS_MIXED_PACK_INDEX
422
- } else {
423
- DELTA_JSON_REFS_INLINE
424
- });
425
- for (delta, prefix_index) in deltas.iter().zip(delta_prefix_indexes) {
426
- append_delta_key_ref(
427
- &mut out,
428
- &key_prefixes,
429
- prefix_index,
430
- TrackedStateKeyRef {
431
- schema_key: delta.change.schema_key,
432
- file_id: delta.change.file_id,
433
- entity_id: delta.change.entity_id,
434
- },
435
- )?;
436
- append_delta_value_ref(
437
- &mut out,
438
- commit_id,
439
- json_pack_indexes,
440
- TrackedStateIndexValueRef {
441
- change_locator: delta.locator,
442
- deleted: delta.change.snapshot_ref.is_none(),
443
- snapshot_ref: delta.change.snapshot_ref,
444
- metadata_ref: delta.change.metadata_ref,
445
- created_at: delta.created_at,
446
- updated_at: delta.updated_at,
447
- },
448
- )?;
449
- }
450
- Ok(out)
451
- }
452
-
453
- fn delta_key_prefixes<'a>(
454
- deltas: &'a [TrackedStateDeltaRef<'a>],
455
- ) -> (Vec<DeltaKeyPrefixRef<'a>>, Vec<usize>) {
456
- let mut prefixes = Vec::new();
457
- let mut delta_prefix_indexes = Vec::with_capacity(deltas.len());
458
- for delta in deltas {
459
- let prefix = DeltaKeyPrefixRef {
460
- schema_key: delta.change.schema_key,
461
- file_id: delta.change.file_id,
462
- };
463
- let prefix_index = match prefixes.iter().position(|candidate| *candidate == prefix) {
464
- Some(prefix_index) => prefix_index,
465
- None => {
466
- let prefix_index = prefixes.len();
467
- prefixes.push(prefix);
468
- prefix_index
469
- }
470
- };
471
- delta_prefix_indexes.push(prefix_index);
472
- }
473
- (prefixes, delta_prefix_indexes)
474
- }
475
-
476
- fn append_delta_key_prefix_ref(
477
- out: &mut Vec<u8>,
478
- prefix: DeltaKeyPrefixRef<'_>,
479
- ) -> Result<(), LixError> {
480
- push_var_sized_bytes(
481
- out,
482
- prefix.schema_key.as_bytes(),
483
- "delta key prefix schema_key",
484
- )?;
485
- match prefix.file_id {
486
- Some(file_id) => {
487
- out.push(1);
488
- push_var_sized_bytes(out, file_id.as_bytes(), "delta key prefix file_id")?;
489
- }
490
- None => out.push(0),
491
- }
492
- Ok(())
493
- }
494
-
495
- fn decode_delta_key_prefix(bytes: &[u8], cursor: &mut usize) -> Result<DeltaKeyPrefix, LixError> {
496
- let schema_key = read_var_sized_string(bytes, cursor, "delta key prefix schema_key")?;
497
- let file_id = match read_u8(bytes, cursor, "delta key prefix file_id presence")? {
498
- 0 => None,
499
- 1 => Some(read_var_sized_string(
500
- bytes,
501
- cursor,
502
- "delta key prefix file_id",
503
- )?),
504
- other => {
505
- return Err(LixError::new(
506
- "LIX_ERROR_UNKNOWN",
507
- format!("tracked-state delta key prefix has invalid file_id presence byte {other}"),
508
- ))
509
- }
510
- };
511
- Ok(DeltaKeyPrefix {
512
- schema_key,
513
- file_id,
514
- })
515
- }
516
-
517
- fn append_delta_key_ref(
518
- out: &mut Vec<u8>,
519
- prefixes: &[DeltaKeyPrefixRef<'_>],
520
- prefix_index: usize,
521
- key: TrackedStateKeyRef<'_>,
522
- ) -> Result<(), LixError> {
523
- let prefix = DeltaKeyPrefixRef {
524
- schema_key: key.schema_key,
525
- file_id: key.file_id,
526
- };
527
- debug_assert_eq!(prefixes.get(prefix_index), Some(&prefix));
528
- push_var_u32(out, prefix_index, "delta key prefix index")?;
529
- push_var_entity_identity(out, key.entity_id)?;
530
- Ok(())
531
- }
532
-
533
- fn append_delta_value_ref(
534
- out: &mut Vec<u8>,
535
- pack_commit_id: &str,
536
- json_pack_indexes: Option<&HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>>,
537
- value: TrackedStateIndexValueRef<'_>,
538
- ) -> Result<(), LixError> {
539
- out.push(VALUE_VERSION | if value.deleted { VALUE_DELETED_FLAG } else { 0 });
540
- if value.change_locator.source_commit_id == pack_commit_id {
541
- out.push(DELTA_LOCATOR_SAME_COMMIT);
542
- } else {
543
- out.push(DELTA_LOCATOR_FULL);
544
- push_var_sized_bytes(
545
- out,
546
- value.change_locator.source_commit_id.as_bytes(),
547
- "source_commit_id",
548
- )?;
549
- }
550
- push_var_u32(
551
- out,
552
- value.change_locator.source_pack_id as usize,
553
- "source_pack_id",
554
- )?;
555
- push_var_u32(
556
- out,
557
- value.change_locator.source_ordinal as usize,
558
- "source_ordinal",
559
- )?;
560
- push_var_delta_change_id(
561
- out,
562
- value.change_locator.source_commit_id,
563
- value.change_locator.change_id,
564
- )?;
565
- push_var_timestamp_pair(out, value.created_at, value.updated_at)?;
566
- match json_pack_indexes {
567
- Some(indexes) => {
568
- push_mixed_optional_json_ref(out, indexes, value.snapshot_ref)?;
569
- push_mixed_optional_json_ref(out, indexes, value.metadata_ref)?;
570
- }
571
- None => {
572
- push_optional_json_ref(out, value.snapshot_ref);
573
- push_optional_json_ref(out, value.metadata_ref);
574
- }
575
- }
576
- Ok(())
577
- }
578
-
579
- pub(crate) fn decode_delta_pack(
580
- bytes: &[u8],
581
- pack_json_refs: Option<&[JsonRef]>,
582
- ) -> Result<(String, Vec<TrackedStateDeltaEntry>), LixError> {
583
- let mut cursor = 0usize;
584
- let magic = bytes.get(0..4).ok_or_else(|| {
585
- LixError::new(
586
- "LIX_ERROR_UNKNOWN",
587
- "tracked-state delta pack is truncated before magic",
588
- )
589
- })?;
590
- if magic != b"LXTD" {
591
- return Err(LixError::new(
592
- "LIX_ERROR_UNKNOWN",
593
- "tracked-state delta pack has invalid magic",
594
- ));
595
- }
596
- cursor += 4;
597
- let version = read_u8(bytes, &mut cursor, "delta pack version")?;
598
- if version != DELTA_PACK_VERSION {
599
- return Err(LixError::new(
600
- "LIX_ERROR_UNKNOWN",
601
- format!("unsupported tracked-state delta pack version {version}"),
602
- ));
603
- }
604
- let commit_id = read_var_sized_string(bytes, &mut cursor, "delta pack commit_id")?;
605
- let prefix_count = read_var_u32(bytes, &mut cursor, "delta key prefix count")?;
606
- let mut key_prefixes = Vec::new();
607
- for _ in 0..prefix_count {
608
- key_prefixes.push(decode_delta_key_prefix(bytes, &mut cursor)?);
609
- }
610
- let count = read_var_u32(bytes, &mut cursor, "delta pack entry count")?;
611
- let json_ref_mode = decode_delta_json_ref_mode(bytes, &mut cursor, pack_json_refs)?;
612
- let mut entries = Vec::new();
613
- for _ in 0..count {
614
- let key = decode_delta_key(bytes, &mut cursor, &key_prefixes)?;
615
- let value = decode_delta_value(bytes, &mut cursor, &commit_id, &json_ref_mode)?;
616
- entries.push(TrackedStateDeltaEntry { key, value });
617
- }
618
- if cursor != bytes.len() {
619
- return Err(LixError::new(
620
- "LIX_ERROR_UNKNOWN",
621
- "tracked-state delta pack decode found trailing bytes",
622
- ));
623
- }
624
- Ok((commit_id, entries))
625
- }
626
-
627
- pub(crate) fn delta_pack_uses_json_pack_indexes(bytes: &[u8]) -> Result<bool, LixError> {
628
- let mut cursor = 0usize;
629
- let magic = bytes.get(0..4).ok_or_else(|| {
630
- LixError::new(
631
- "LIX_ERROR_UNKNOWN",
632
- "tracked-state delta pack is truncated before magic",
633
- )
634
- })?;
635
- if magic != b"LXTD" {
636
- return Err(LixError::new(
637
- "LIX_ERROR_UNKNOWN",
638
- "tracked-state delta pack has invalid magic",
639
- ));
640
- }
641
- cursor += 4;
642
- let version = read_u8(bytes, &mut cursor, "delta pack version")?;
643
- if version != DELTA_PACK_VERSION {
644
- return Err(LixError::new(
645
- "LIX_ERROR_UNKNOWN",
646
- format!("unsupported tracked-state delta pack version {version}"),
647
- ));
648
- }
649
- let _commit_id = read_var_sized_string(bytes, &mut cursor, "delta pack commit_id")?;
650
- let prefix_count = read_var_u32(bytes, &mut cursor, "delta key prefix count")?;
651
- for _ in 0..prefix_count {
652
- let _ = decode_delta_key_prefix(bytes, &mut cursor)?;
653
- }
654
- let _count = read_var_u32(bytes, &mut cursor, "delta pack entry count")?;
655
- match read_u8(bytes, &mut cursor, "delta JSON ref mode")? {
656
- DELTA_JSON_REFS_INLINE => Ok(false),
657
- DELTA_JSON_REFS_MIXED_PACK_INDEX => Ok(true),
658
- other => Err(LixError::new(
659
- "LIX_ERROR_UNKNOWN",
660
- format!("tracked-state delta pack has invalid JSON ref mode {other}"),
661
- )),
662
- }
663
- }
664
-
665
- fn decode_delta_key(
666
- bytes: &[u8],
667
- cursor: &mut usize,
668
- prefixes: &[DeltaKeyPrefix],
669
- ) -> Result<TrackedStateKey, LixError> {
670
- let prefix_index = read_var_u32(bytes, cursor, "delta key prefix index")?;
671
- let prefix = prefixes.get(prefix_index).ok_or_else(|| {
672
- LixError::new(
673
- "LIX_ERROR_UNKNOWN",
674
- format!("tracked-state delta key prefix index {prefix_index} is out of bounds"),
675
- )
676
- })?;
677
- let entity_id = read_var_entity_identity(bytes, cursor)?;
678
- Ok(TrackedStateKey {
679
- schema_key: prefix.schema_key.clone(),
680
- file_id: prefix.file_id.clone(),
681
- entity_id,
682
- })
683
- }
684
-
685
- enum DeltaJsonRefDecodeMode<'a> {
686
- Inline,
687
- MixedPackIndex(&'a [JsonRef]),
688
- }
689
-
690
- fn decode_delta_json_ref_mode<'a>(
691
- bytes: &[u8],
692
- cursor: &mut usize,
693
- pack_json_refs: Option<&'a [JsonRef]>,
694
- ) -> Result<DeltaJsonRefDecodeMode<'a>, LixError> {
695
- match read_u8(bytes, cursor, "delta JSON ref mode")? {
696
- DELTA_JSON_REFS_INLINE => Ok(DeltaJsonRefDecodeMode::Inline),
697
- DELTA_JSON_REFS_MIXED_PACK_INDEX => {
698
- let refs = pack_json_refs.ok_or_else(|| {
699
- LixError::new(
700
- LixError::CODE_INTERNAL_ERROR,
701
- "tracked-state delta pack needs JSON pack refs but none were provided",
702
- )
703
- })?;
704
- Ok(DeltaJsonRefDecodeMode::MixedPackIndex(refs))
705
- }
706
- other => Err(LixError::new(
707
- "LIX_ERROR_UNKNOWN",
708
- format!("tracked-state delta pack has invalid JSON ref mode {other}"),
709
- )),
710
- }
711
- }
712
-
713
- fn decode_delta_value(
714
- bytes: &[u8],
715
- cursor: &mut usize,
716
- pack_commit_id: &str,
717
- json_ref_mode: &DeltaJsonRefDecodeMode<'_>,
718
- ) -> Result<TrackedStateIndexValue, LixError> {
719
- let value_header = read_u8(bytes, cursor, "delta value header")?;
720
- let deleted = decode_value_header(value_header)?;
721
- let source_commit_id = match read_u8(bytes, cursor, "delta locator tag")? {
722
- DELTA_LOCATOR_SAME_COMMIT => pack_commit_id.to_string(),
723
- DELTA_LOCATOR_FULL => read_var_sized_string(bytes, cursor, "source_commit_id")?,
724
- other => {
725
- return Err(LixError::new(
726
- "LIX_ERROR_UNKNOWN",
727
- format!("tracked-state delta value has invalid locator tag {other}"),
728
- ))
729
- }
730
- };
731
- let source_pack_id =
732
- u32::try_from(read_var_u32(bytes, cursor, "source_pack_id")?).map_err(|_| {
733
- LixError::new(
734
- LixError::CODE_INTERNAL_ERROR,
735
- "tracked-state source_pack_id exceeds u32",
736
- )
737
- })?;
738
- let source_ordinal =
739
- u32::try_from(read_var_u32(bytes, cursor, "source_ordinal")?).map_err(|_| {
740
- LixError::new(
741
- LixError::CODE_INTERNAL_ERROR,
742
- "tracked-state source_ordinal exceeds u32",
743
- )
744
- })?;
745
- let change_id = read_var_delta_change_id(bytes, cursor, &source_commit_id)?;
746
- let (created_at, updated_at) = read_var_timestamp_pair(bytes, cursor)?;
747
- let (snapshot_ref, metadata_ref) = match json_ref_mode {
748
- DeltaJsonRefDecodeMode::Inline => (
749
- read_optional_json_ref(bytes, cursor, "snapshot_ref")?,
750
- read_optional_json_ref(bytes, cursor, "metadata_ref")?,
751
- ),
752
- DeltaJsonRefDecodeMode::MixedPackIndex(refs) => (
753
- read_mixed_optional_json_ref(bytes, cursor, refs, "snapshot_ref")?,
754
- read_mixed_optional_json_ref(bytes, cursor, refs, "metadata_ref")?,
755
- ),
756
- };
757
- Ok(TrackedStateIndexValue {
758
- change_locator: ChangeLocator {
759
- source_commit_id,
760
- source_pack_id,
761
- source_ordinal,
762
- change_id,
763
- },
341
+ change_id,
342
+ commit_id,
764
343
  deleted,
765
344
  snapshot_ref,
766
345
  metadata_ref,
@@ -785,7 +364,7 @@ pub(crate) fn encode_leaf_node(entries: &[EncodedLeafEntry]) -> Vec<u8> {
785
364
  pub(crate) fn encode_leaf_node_refs(entries: &[EncodedLeafEntryRef<'_>]) -> Vec<u8> {
786
365
  let mut out = Vec::new();
787
366
  out.push(NODE_KIND_LEAF);
788
- out.push(NODE_VERSION);
367
+ out.push(NODE_BRANCH);
789
368
  push_u32(&mut out, entries.len());
790
369
 
791
370
  let mut offsets = Vec::with_capacity(entries.len().saturating_add(1));
@@ -814,7 +393,7 @@ pub(crate) fn encode_internal_node(children: &[ChildSummary]) -> Vec<u8> {
814
393
  pub(crate) fn encode_internal_node_refs(children: &[ChildSummaryRef<'_>]) -> Vec<u8> {
815
394
  let mut out = Vec::new();
816
395
  out.push(NODE_KIND_INTERNAL);
817
- out.push(NODE_VERSION);
396
+ out.push(NODE_BRANCH);
818
397
  push_u32(&mut out, children.len());
819
398
  for child in children {
820
399
  push_sized_bytes(&mut out, child.first_key);
@@ -850,11 +429,11 @@ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
850
429
  pub(crate) fn decode_node_ref(bytes: &[u8]) -> Result<DecodedNodeRef<'_>, LixError> {
851
430
  let mut cursor = 0usize;
852
431
  let kind = read_u8(bytes, &mut cursor, "node kind")?;
853
- let version = read_u8(bytes, &mut cursor, "node version")?;
854
- if version != NODE_VERSION {
432
+ let branch = read_u8(bytes, &mut cursor, "node branch")?;
433
+ if branch != NODE_BRANCH {
855
434
  return Err(LixError::new(
856
435
  "LIX_ERROR_UNKNOWN",
857
- format!("unsupported tracked-state tree node version {version}"),
436
+ format!("unsupported tracked-state tree node branch {branch}"),
858
437
  ));
859
438
  }
860
439
  let count = read_u32(bytes, &mut cursor, "entry count")?;
@@ -864,6 +443,13 @@ pub(crate) fn decode_node_ref(bytes: &[u8]) -> Result<DecodedNodeRef<'_>, LixErr
864
443
  DecodedNodeRef::Leaf(leaf)
865
444
  }
866
445
  NODE_KIND_INTERNAL => {
446
+ ensure_counted_records_fit_remaining(
447
+ bytes,
448
+ cursor,
449
+ count,
450
+ internal_child_min_len(),
451
+ "internal children",
452
+ )?;
867
453
  let mut children = Vec::with_capacity(count);
868
454
  for _ in 0..count {
869
455
  let first_key = read_sized_bytes(bytes, &mut cursor, "internal first_key")?;
@@ -900,7 +486,14 @@ fn decode_leaf_node_ref_after_count<'a>(
900
486
  cursor: &mut usize,
901
487
  count: usize,
902
488
  ) -> Result<DecodedLeafNodeRef<'a>, LixError> {
903
- let mut offsets = Vec::with_capacity(count.saturating_add(1));
489
+ let offset_count = count.checked_add(1).ok_or_else(|| {
490
+ LixError::new(
491
+ "LIX_ERROR_UNKNOWN",
492
+ "tracked-state leaf offset count overflows",
493
+ )
494
+ })?;
495
+ ensure_counted_records_fit_remaining(bytes, *cursor, offset_count, 4, "leaf offsets")?;
496
+ let mut offsets = Vec::with_capacity(offset_count);
904
497
  for _ in 0..=count {
905
498
  offsets.push(read_u32(bytes, cursor, "leaf entry offset")?);
906
499
  }
@@ -939,6 +532,38 @@ fn decode_leaf_node_ref_after_count<'a>(
939
532
  })
940
533
  }
941
534
 
535
+ fn ensure_counted_records_fit_remaining(
536
+ bytes: &[u8],
537
+ cursor: usize,
538
+ count: usize,
539
+ record_min_len: usize,
540
+ field_name: &str,
541
+ ) -> Result<(), LixError> {
542
+ let required = count.checked_mul(record_min_len).ok_or_else(|| {
543
+ LixError::new(
544
+ "LIX_ERROR_UNKNOWN",
545
+ format!("tracked-state tree field '{field_name}' byte count overflows"),
546
+ )
547
+ })?;
548
+ let remaining = bytes.len().checked_sub(cursor).ok_or_else(|| {
549
+ LixError::new(
550
+ "LIX_ERROR_UNKNOWN",
551
+ format!("tracked-state tree field '{field_name}' starts past node end"),
552
+ )
553
+ })?;
554
+ if required > remaining {
555
+ return Err(LixError::new(
556
+ "LIX_ERROR_UNKNOWN",
557
+ format!("tracked-state tree field '{field_name}' exceeds remaining node bytes"),
558
+ ));
559
+ }
560
+ Ok(())
561
+ }
562
+
563
+ fn internal_child_min_len() -> usize {
564
+ 4 + 4 + TRACKED_STATE_HASH_BYTES + 8
565
+ }
566
+
942
567
  pub(crate) fn child_summary_from_node(
943
568
  node_bytes: Vec<u8>,
944
569
  first_key: Vec<u8>,
@@ -998,35 +623,37 @@ fn level_salt(level: usize) -> u64 {
998
623
  value ^ (value >> 31)
999
624
  }
1000
625
 
1001
- fn push_entity_identity(out: &mut Vec<u8>, identity: &EntityIdentity) {
626
+ fn push_entity_pk(out: &mut Vec<u8>, identity: &EntityPk) {
1002
627
  assert!(
1003
628
  !identity.parts.is_empty(),
1004
- "tracked-state key entity identity must contain at least one part"
629
+ "tracked-state key entity primary key must contain at least one part"
1005
630
  );
1006
631
  for part in &identity.parts {
1007
- out.push(ENTITY_IDENTITY_STRING);
632
+ out.push(ENTITY_PKENTITY_STRING);
1008
633
  push_sized_bytes(out, part.as_bytes());
1009
634
  }
1010
- out.push(ENTITY_IDENTITY_END);
635
+ out.push(ENTITY_PKENTITY_END);
1011
636
  }
1012
637
 
1013
- fn read_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdentity, LixError> {
638
+ fn read_entity_pk(bytes: &[u8], cursor: &mut usize) -> Result<EntityPk, LixError> {
1014
639
  let mut parts = Vec::new();
1015
640
  loop {
1016
- let tag = read_u8(bytes, cursor, "entity identity part tag")?;
641
+ let tag = read_u8(bytes, cursor, "entity primary key part tag")?;
1017
642
  match tag {
1018
- ENTITY_IDENTITY_END => break,
1019
- ENTITY_IDENTITY_STRING => {
643
+ ENTITY_PKENTITY_END => break,
644
+ ENTITY_PKENTITY_STRING => {
1020
645
  parts.push(read_sized_string(
1021
646
  bytes,
1022
647
  cursor,
1023
- "entity identity string part",
648
+ "entity primary key string part",
1024
649
  )?);
1025
650
  }
1026
651
  other => {
1027
652
  return Err(LixError::new(
1028
653
  "LIX_ERROR_UNKNOWN",
1029
- format!("tracked-state tree key has invalid entity identity part tag {other}"),
654
+ format!(
655
+ "tracked-state tree key has invalid entity primary key part tag {other}"
656
+ ),
1030
657
  ))
1031
658
  }
1032
659
  }
@@ -1034,10 +661,10 @@ fn read_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdenti
1034
661
  if parts.is_empty() {
1035
662
  return Err(LixError::new(
1036
663
  "LIX_ERROR_UNKNOWN",
1037
- "tracked-state tree key entity identity must contain at least one part",
664
+ "tracked-state tree key entity primary key must contain at least one part",
1038
665
  ));
1039
666
  }
1040
- Ok(EntityIdentity { parts })
667
+ Ok(EntityPk { parts })
1041
668
  }
1042
669
 
1043
670
  fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
@@ -1045,49 +672,6 @@ fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
1045
672
  out.extend_from_slice(bytes);
1046
673
  }
1047
674
 
1048
- fn push_var_u32(out: &mut Vec<u8>, value: usize, field_name: &str) -> Result<(), LixError> {
1049
- let (encoded, len) = var_u32_bytes(value, field_name)?;
1050
- out.extend_from_slice(&encoded[..len]);
1051
- Ok(())
1052
- }
1053
-
1054
- fn var_u32_bytes(value: usize, field_name: &str) -> Result<([u8; 5], usize), LixError> {
1055
- let mut value = u32::try_from(value).map_err(|_| {
1056
- LixError::new(
1057
- LixError::CODE_INTERNAL_ERROR,
1058
- format!("tracked-state delta pack field '{field_name}' exceeds u32"),
1059
- )
1060
- })?;
1061
- let mut encoded = [0_u8; 5];
1062
- let mut len = 0usize;
1063
- while value >= 0x80 {
1064
- encoded[len] = (value as u8 & 0x7f) | 0x80;
1065
- len += 1;
1066
- value >>= 7;
1067
- }
1068
- encoded[len] = value as u8;
1069
- len += 1;
1070
- Ok((encoded, len))
1071
- }
1072
-
1073
- fn push_var_sized_bytes(out: &mut Vec<u8>, bytes: &[u8], field_name: &str) -> Result<(), LixError> {
1074
- push_var_u32(out, bytes.len(), field_name)?;
1075
- out.extend_from_slice(bytes);
1076
- Ok(())
1077
- }
1078
-
1079
- fn push_var_entity_identity(out: &mut Vec<u8>, identity: &EntityIdentity) -> Result<(), LixError> {
1080
- assert!(
1081
- !identity.parts.is_empty(),
1082
- "tracked-state delta key entity identity must contain at least one part"
1083
- );
1084
- push_var_u32(out, identity.parts.len(), "entity identity part count")?;
1085
- for part in &identity.parts {
1086
- push_var_sized_bytes(out, part.as_bytes(), "entity identity string part")?;
1087
- }
1088
- Ok(())
1089
- }
1090
-
1091
675
  fn push_optional_json_ref(out: &mut Vec<u8>, json_ref: Option<&JsonRef>) {
1092
676
  match json_ref {
1093
677
  Some(json_ref) => {
@@ -1098,56 +682,6 @@ fn push_optional_json_ref(out: &mut Vec<u8>, json_ref: Option<&JsonRef>) {
1098
682
  }
1099
683
  }
1100
684
 
1101
- fn push_mixed_optional_json_ref(
1102
- out: &mut Vec<u8>,
1103
- indexes: &HashMap<[u8; TRACKED_STATE_HASH_BYTES], usize>,
1104
- json_ref: Option<&JsonRef>,
1105
- ) -> Result<(), LixError> {
1106
- let Some(json_ref) = json_ref else {
1107
- out.push(DELTA_JSON_REF_NONE);
1108
- return Ok(());
1109
- };
1110
- if let Some(index) = indexes.get(json_ref.as_hash_array()).copied() {
1111
- out.push(DELTA_JSON_REF_PACK_INDEX);
1112
- push_var_u32(out, index, "json ref pack index")
1113
- } else {
1114
- out.push(DELTA_JSON_REF_INLINE);
1115
- out.extend_from_slice(json_ref.as_hash_bytes());
1116
- Ok(())
1117
- }
1118
- }
1119
-
1120
- fn push_var_delta_change_id(
1121
- out: &mut Vec<u8>,
1122
- source_commit_id: &str,
1123
- change_id: &str,
1124
- ) -> Result<(), LixError> {
1125
- if let Some(suffix) = change_id.strip_prefix(source_commit_id) {
1126
- out.push(DELTA_CHANGE_ID_COMMIT_SUFFIX);
1127
- push_var_sized_bytes(out, suffix.as_bytes(), "change_id")
1128
- } else {
1129
- out.push(DELTA_CHANGE_ID_FULL);
1130
- push_var_sized_bytes(out, change_id.as_bytes(), "change_id")
1131
- }
1132
- }
1133
-
1134
- fn read_var_delta_change_id(
1135
- bytes: &[u8],
1136
- cursor: &mut usize,
1137
- source_commit_id: &str,
1138
- ) -> Result<String, LixError> {
1139
- let tag = read_u8(bytes, cursor, "delta change_id tag")?;
1140
- let value = read_var_sized_string(bytes, cursor, "change_id")?;
1141
- match tag {
1142
- DELTA_CHANGE_ID_FULL => Ok(value),
1143
- DELTA_CHANGE_ID_COMMIT_SUFFIX => Ok(format!("{source_commit_id}{value}")),
1144
- other => Err(LixError::new(
1145
- "LIX_ERROR_UNKNOWN",
1146
- format!("tracked-state delta value has invalid change_id tag {other}"),
1147
- )),
1148
- }
1149
- }
1150
-
1151
685
  #[cfg(test)]
1152
686
  fn optional_json_ref_len(json_ref: Option<&JsonRef>) -> usize {
1153
687
  1 + json_ref.map_or(0, |_| TRACKED_STATE_HASH_BYTES)
@@ -1163,21 +697,6 @@ fn push_timestamp_pair(out: &mut Vec<u8>, created_at: &str, updated_at: &str) {
1163
697
  }
1164
698
  }
1165
699
 
1166
- fn push_var_timestamp_pair(
1167
- out: &mut Vec<u8>,
1168
- created_at: &str,
1169
- updated_at: &str,
1170
- ) -> Result<(), LixError> {
1171
- push_var_sized_bytes(out, created_at.as_bytes(), "created_at")?;
1172
- if updated_at == created_at {
1173
- out.push(TIMESTAMP_UPDATED_SAME);
1174
- } else {
1175
- out.push(TIMESTAMP_UPDATED_DISTINCT);
1176
- push_var_sized_bytes(out, updated_at.as_bytes(), "updated_at")?;
1177
- }
1178
- Ok(())
1179
- }
1180
-
1181
700
  #[cfg(test)]
1182
701
  fn timestamp_pair_len(created_at: &str, updated_at: &str) -> usize {
1183
702
  sized_bytes_len(created_at.as_bytes())
@@ -1204,21 +723,6 @@ fn read_timestamp_pair(bytes: &[u8], cursor: &mut usize) -> Result<(String, Stri
1204
723
  Ok((created_at, updated_at))
1205
724
  }
1206
725
 
1207
- fn read_var_timestamp_pair(bytes: &[u8], cursor: &mut usize) -> Result<(String, String), LixError> {
1208
- let created_at = read_var_sized_string(bytes, cursor, "created_at")?;
1209
- let updated_at = match read_u8(bytes, cursor, "updated_at tag")? {
1210
- TIMESTAMP_UPDATED_SAME => created_at.clone(),
1211
- TIMESTAMP_UPDATED_DISTINCT => read_var_sized_string(bytes, cursor, "updated_at")?,
1212
- other => {
1213
- return Err(LixError::new(
1214
- "LIX_ERROR_UNKNOWN",
1215
- format!("tracked-state timestamp pair has invalid updated_at tag {other}"),
1216
- ))
1217
- }
1218
- };
1219
- Ok((created_at, updated_at))
1220
- }
1221
-
1222
726
  fn push_u32(out: &mut Vec<u8>, value: usize) {
1223
727
  out.extend_from_slice(&(value as u32).to_be_bytes());
1224
728
  }
@@ -1266,60 +770,6 @@ fn read_sized_slice<'a>(
1266
770
  Ok(slice)
1267
771
  }
1268
772
 
1269
- fn read_var_sized_string(
1270
- bytes: &[u8],
1271
- cursor: &mut usize,
1272
- field_name: &str,
1273
- ) -> Result<String, LixError> {
1274
- String::from_utf8(read_var_sized_slice(bytes, cursor, field_name)?.to_vec()).map_err(|error| {
1275
- LixError::new(
1276
- "LIX_ERROR_UNKNOWN",
1277
- format!("tracked-state delta pack field '{field_name}' is invalid UTF-8: {error}"),
1278
- )
1279
- })
1280
- }
1281
-
1282
- fn read_var_sized_slice<'a>(
1283
- bytes: &'a [u8],
1284
- cursor: &mut usize,
1285
- field_name: &str,
1286
- ) -> Result<&'a [u8], LixError> {
1287
- let len = read_var_u32(bytes, cursor, field_name)?;
1288
- let end = cursor.checked_add(len).ok_or_else(|| {
1289
- LixError::new(
1290
- "LIX_ERROR_UNKNOWN",
1291
- format!("tracked-state delta pack field '{field_name}' length overflow"),
1292
- )
1293
- })?;
1294
- let slice = bytes.get(*cursor..end).ok_or_else(|| {
1295
- LixError::new(
1296
- "LIX_ERROR_UNKNOWN",
1297
- format!("tracked-state delta pack field '{field_name}' is truncated"),
1298
- )
1299
- })?;
1300
- *cursor = end;
1301
- Ok(slice)
1302
- }
1303
-
1304
- fn read_var_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdentity, LixError> {
1305
- let count = read_var_u32(bytes, cursor, "entity identity part count")?;
1306
- let mut parts = Vec::new();
1307
- for _ in 0..count {
1308
- parts.push(read_var_sized_string(
1309
- bytes,
1310
- cursor,
1311
- "entity identity string part",
1312
- )?);
1313
- }
1314
- if parts.is_empty() {
1315
- return Err(LixError::new(
1316
- "LIX_ERROR_UNKNOWN",
1317
- "tracked-state delta key entity identity must contain at least one part",
1318
- ));
1319
- }
1320
- Ok(EntityIdentity { parts })
1321
- }
1322
-
1323
773
  fn read_fixed_hash(
1324
774
  bytes: &[u8],
1325
775
  cursor: &mut usize,
@@ -1355,34 +805,6 @@ fn read_optional_json_ref(
1355
805
  }
1356
806
  }
1357
807
 
1358
- fn read_mixed_optional_json_ref(
1359
- bytes: &[u8],
1360
- cursor: &mut usize,
1361
- refs: &[JsonRef],
1362
- field_name: &str,
1363
- ) -> Result<Option<JsonRef>, LixError> {
1364
- match read_u8(bytes, cursor, field_name)? {
1365
- DELTA_JSON_REF_NONE => Ok(None),
1366
- DELTA_JSON_REF_PACK_INDEX => {
1367
- let index = read_var_u32(bytes, cursor, field_name)?;
1368
- refs.get(index).copied().map(Some).ok_or_else(|| {
1369
- LixError::new(
1370
- "LIX_ERROR_UNKNOWN",
1371
- format!("tracked-state delta JSON ref index {index} is out of bounds"),
1372
- )
1373
- })
1374
- }
1375
- DELTA_JSON_REF_INLINE => {
1376
- let hash = read_fixed_hash(bytes, cursor, field_name)?;
1377
- Ok(Some(JsonRef::from_hash_bytes(hash)))
1378
- }
1379
- other => Err(LixError::new(
1380
- "LIX_ERROR_UNKNOWN",
1381
- format!("tracked-state tree field '{field_name}' has invalid JSON ref tag {other}"),
1382
- )),
1383
- }
1384
- }
1385
-
1386
808
  fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, LixError> {
1387
809
  let value = *bytes.get(*cursor).ok_or_else(|| {
1388
810
  LixError::new(
@@ -1394,35 +816,6 @@ fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, Lix
1394
816
  Ok(value)
1395
817
  }
1396
818
 
1397
- fn read_var_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
1398
- let mut value = 0u32;
1399
- let mut shift = 0u32;
1400
- for byte_index in 0..5 {
1401
- let byte = read_u8(bytes, cursor, field_name)?;
1402
- if shift == 28 && (byte & 0x80 != 0 || byte & 0x70 != 0) {
1403
- return Err(LixError::new(
1404
- "LIX_ERROR_UNKNOWN",
1405
- format!("tracked-state delta pack field '{field_name}' varint exceeds u32"),
1406
- ));
1407
- }
1408
- if byte_index > 0 && byte & 0x80 == 0 && byte == 0 {
1409
- return Err(LixError::new(
1410
- "LIX_ERROR_UNKNOWN",
1411
- format!("tracked-state delta pack field '{field_name}' has non-canonical varint"),
1412
- ));
1413
- }
1414
- value |= ((byte & 0x7f) as u32) << shift;
1415
- if byte & 0x80 == 0 {
1416
- return Ok(value as usize);
1417
- }
1418
- shift += 7;
1419
- }
1420
- Err(LixError::new(
1421
- "LIX_ERROR_UNKNOWN",
1422
- format!("tracked-state delta pack field '{field_name}' varint exceeds u32"),
1423
- ))
1424
- }
1425
-
1426
819
  fn read_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
1427
820
  let end = *cursor + 4;
1428
821
  let slice = bytes.get(*cursor..end).ok_or_else(|| {
@@ -1455,17 +848,29 @@ fn read_u64(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u64, L
1455
848
  mod tests {
1456
849
  use super::*;
1457
850
 
851
+ fn test_value(commit_id: &str, change_id: &str) -> TrackedStateIndexValue {
852
+ TrackedStateIndexValue {
853
+ change_id: change_id.to_string(),
854
+ commit_id: commit_id.to_string(),
855
+ deleted: false,
856
+ snapshot_ref: None,
857
+ metadata_ref: None,
858
+ created_at: "2026-01-01T00:00:00Z".to_string(),
859
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
860
+ }
861
+ }
862
+
1458
863
  #[test]
1459
864
  fn key_codec_distinguishes_null_and_value_file_id() {
1460
865
  let null_key = encode_key(&TrackedStateKey {
1461
866
  schema_key: "schema".to_string(),
1462
867
  file_id: None,
1463
- entity_id: EntityIdentity::single("entity"),
868
+ entity_pk: EntityPk::single("entity"),
1464
869
  });
1465
870
  let file_key = encode_key(&TrackedStateKey {
1466
871
  schema_key: "schema".to_string(),
1467
872
  file_id: Some("file".to_string()),
1468
- entity_id: EntityIdentity::single("entity"),
873
+ entity_pk: EntityPk::single("entity"),
1469
874
  });
1470
875
 
1471
876
  assert_ne!(null_key, file_key);
@@ -1474,7 +879,7 @@ mod tests {
1474
879
  TrackedStateKey {
1475
880
  schema_key: "schema".to_string(),
1476
881
  file_id: None,
1477
- entity_id: EntityIdentity::single("entity"),
882
+ entity_pk: EntityPk::single("entity"),
1478
883
  }
1479
884
  );
1480
885
  assert_eq!(
@@ -1482,7 +887,7 @@ mod tests {
1482
887
  TrackedStateKey {
1483
888
  schema_key: "schema".to_string(),
1484
889
  file_id: Some("file".to_string()),
1485
- entity_id: EntityIdentity::single("entity"),
890
+ entity_pk: EntityPk::single("entity"),
1486
891
  }
1487
892
  );
1488
893
  }
@@ -1492,7 +897,7 @@ mod tests {
1492
897
  let key = TrackedStateKey {
1493
898
  schema_key: "schema".to_string(),
1494
899
  file_id: None,
1495
- entity_id: EntityIdentity {
900
+ entity_pk: EntityPk {
1496
901
  parts: vec![
1497
902
  "namespace".to_string(),
1498
903
  "true".to_string(),
@@ -1511,7 +916,7 @@ mod tests {
1511
916
  let key = TrackedStateKey {
1512
917
  schema_key: "schema".to_string(),
1513
918
  file_id: Some("file".to_string()),
1514
- entity_id: EntityIdentity {
919
+ entity_pk: EntityPk {
1515
920
  parts: vec!["namespace".to_string(), "id".to_string()],
1516
921
  },
1517
922
  };
@@ -1530,7 +935,7 @@ mod tests {
1530
935
  let mut encoded = encode_key(&TrackedStateKey {
1531
936
  schema_key: "schema".to_string(),
1532
937
  file_id: None,
1533
- entity_id: EntityIdentity {
938
+ entity_pk: EntityPk {
1534
939
  parts: vec!["true".to_string()],
1535
940
  },
1536
941
  });
@@ -1542,7 +947,7 @@ mod tests {
1542
947
  let error = decode_key(&encoded).expect_err("non-string identity tag should reject");
1543
948
  assert!(error
1544
949
  .to_string()
1545
- .contains("invalid entity identity part tag 2"));
950
+ .contains("invalid entity primary key part tag 2"));
1546
951
  }
1547
952
 
1548
953
  #[test]
@@ -1550,14 +955,14 @@ mod tests {
1550
955
  let prefix = encode_key(&TrackedStateKey {
1551
956
  schema_key: "schema".to_string(),
1552
957
  file_id: None,
1553
- entity_id: EntityIdentity {
958
+ entity_pk: EntityPk {
1554
959
  parts: vec!["a".to_string()],
1555
960
  },
1556
961
  });
1557
962
  let extended = encode_key(&TrackedStateKey {
1558
963
  schema_key: "schema".to_string(),
1559
964
  file_id: None,
1560
- entity_id: EntityIdentity {
965
+ entity_pk: EntityPk {
1561
966
  parts: vec!["a".to_string(), "b".to_string()],
1562
967
  },
1563
968
  });
@@ -1566,14 +971,10 @@ mod tests {
1566
971
  }
1567
972
 
1568
973
  #[test]
1569
- fn value_codec_roundtrips_locator_value() {
974
+ fn value_codec_roundtrips_change_ref_value() {
1570
975
  let value = TrackedStateIndexValue {
1571
- change_locator: ChangeLocator {
1572
- source_commit_id: "commit".to_string(),
1573
- source_pack_id: 7,
1574
- source_ordinal: 11,
1575
- change_id: "change".to_string(),
1576
- },
976
+ change_id: "change".to_string(),
977
+ commit_id: "commit".to_string(),
1577
978
  deleted: false,
1578
979
  snapshot_ref: Some(JsonRef::from_hash_bytes([1; 32])),
1579
980
  metadata_ref: Some(JsonRef::from_hash_bytes([2; 32])),
@@ -1586,14 +987,10 @@ mod tests {
1586
987
  }
1587
988
 
1588
989
  #[test]
1589
- fn value_codec_roundtrips_second_locator_value() {
990
+ fn value_codec_roundtrips_second_change_ref_value() {
1590
991
  let value = TrackedStateIndexValue {
1591
- change_locator: ChangeLocator {
1592
- source_commit_id: "other-commit".to_string(),
1593
- source_pack_id: 0,
1594
- source_ordinal: 1,
1595
- change_id: "other-change".to_string(),
1596
- },
992
+ change_id: "other-change".to_string(),
993
+ commit_id: "other-commit".to_string(),
1597
994
  deleted: true,
1598
995
  snapshot_ref: None,
1599
996
  metadata_ref: None,
@@ -1607,19 +1004,8 @@ mod tests {
1607
1004
 
1608
1005
  #[test]
1609
1006
  fn value_codec_compacts_matching_timestamps() {
1610
- let mut compact = TrackedStateIndexValue {
1611
- change_locator: ChangeLocator {
1612
- source_commit_id: "commit".to_string(),
1613
- source_pack_id: 0,
1614
- source_ordinal: 1,
1615
- change_id: "change".to_string(),
1616
- },
1617
- deleted: false,
1618
- snapshot_ref: None,
1619
- metadata_ref: None,
1620
- created_at: "2026-01-01T00:00:00Z".to_string(),
1621
- updated_at: "2026-01-01T00:00:00Z".to_string(),
1622
- };
1007
+ let mut compact = test_value("commit", "change");
1008
+ compact.updated_at = compact.created_at.clone();
1623
1009
  let compact_len = encode_value(&compact).len();
1624
1010
  assert_eq!(
1625
1011
  decode_value(&encode_value(&compact)).expect("value"),
@@ -1636,316 +1022,12 @@ mod tests {
1636
1022
  );
1637
1023
  }
1638
1024
 
1639
- #[test]
1640
- fn delta_pack_ref_encoder_roundtrips_entries() {
1641
- let entity_id = EntityIdentity {
1642
- parts: vec!["entity-a".to_string()],
1643
- };
1644
- let snapshot_ref = JsonRef::from_hash_bytes([1; 32]);
1645
- let metadata_ref = JsonRef::from_hash_bytes([2; 32]);
1646
- let live_change = crate::commit_store::ChangeRef {
1647
- id: "commit-a:change-live",
1648
- entity_id: &entity_id,
1649
- schema_key: "schema",
1650
- file_id: Some("file-a"),
1651
- snapshot_ref: Some(&snapshot_ref),
1652
- metadata_ref: Some(&metadata_ref),
1653
- created_at: "2026-01-01T00:00:00Z",
1654
- };
1655
- let tombstone_change = crate::commit_store::ChangeRef {
1656
- id: "change-deleted",
1657
- entity_id: &entity_id,
1658
- schema_key: "schema",
1659
- file_id: None,
1660
- snapshot_ref: None,
1661
- metadata_ref: None,
1662
- created_at: "2026-01-01T00:00:00Z",
1663
- };
1664
- let live_locator = crate::commit_store::ChangeLocatorRef {
1665
- source_commit_id: "commit-a",
1666
- source_pack_id: 3,
1667
- source_ordinal: 5,
1668
- change_id: "commit-a:change-live",
1669
- };
1670
- let tombstone_locator = crate::commit_store::ChangeLocatorRef {
1671
- source_commit_id: "source-commit",
1672
- source_pack_id: 3,
1673
- source_ordinal: 6,
1674
- change_id: "commit-a:borrowed",
1675
- };
1676
- let encoded = encode_delta_pack_refs(
1677
- "commit-a",
1678
- &[
1679
- TrackedStateDeltaRef {
1680
- change: live_change,
1681
- locator: live_locator,
1682
- created_at: "2026-01-01T00:00:00Z",
1683
- updated_at: "2026-01-02T00:00:00Z",
1684
- },
1685
- TrackedStateDeltaRef {
1686
- change: tombstone_change,
1687
- locator: tombstone_locator,
1688
- created_at: "2026-01-03T00:00:00Z",
1689
- updated_at: "2026-01-04T00:00:00Z",
1690
- },
1691
- ],
1692
- )
1693
- .expect("delta pack should encode");
1694
-
1695
- let mut cursor = 5usize;
1696
- assert_eq!(
1697
- read_var_sized_string(&encoded, &mut cursor, "delta pack commit_id")
1698
- .expect("commit id should decode"),
1699
- "commit-a"
1700
- );
1701
- assert_eq!(
1702
- read_var_u32(&encoded, &mut cursor, "delta key prefix count")
1703
- .expect("prefix count should decode"),
1704
- 2
1705
- );
1706
-
1707
- let (decoded_commit_id, decoded) =
1708
- decode_delta_pack(&encoded, None).expect("delta pack should decode");
1709
-
1710
- assert_eq!(decoded_commit_id, "commit-a");
1711
- assert_eq!(
1712
- decoded,
1713
- vec![
1714
- TrackedStateDeltaEntry {
1715
- key: TrackedStateKey {
1716
- schema_key: "schema".to_string(),
1717
- file_id: Some("file-a".to_string()),
1718
- entity_id: entity_id.clone(),
1719
- },
1720
- value: TrackedStateIndexValue {
1721
- change_locator: ChangeLocator {
1722
- source_commit_id: "commit-a".to_string(),
1723
- source_pack_id: 3,
1724
- source_ordinal: 5,
1725
- change_id: "commit-a:change-live".to_string(),
1726
- },
1727
- deleted: false,
1728
- snapshot_ref: Some(snapshot_ref),
1729
- metadata_ref: Some(metadata_ref),
1730
- created_at: "2026-01-01T00:00:00Z".to_string(),
1731
- updated_at: "2026-01-02T00:00:00Z".to_string(),
1732
- },
1733
- },
1734
- TrackedStateDeltaEntry {
1735
- key: TrackedStateKey {
1736
- schema_key: "schema".to_string(),
1737
- file_id: None,
1738
- entity_id,
1739
- },
1740
- value: TrackedStateIndexValue {
1741
- change_locator: ChangeLocator {
1742
- source_commit_id: "source-commit".to_string(),
1743
- source_pack_id: 3,
1744
- source_ordinal: 6,
1745
- change_id: "commit-a:borrowed".to_string(),
1746
- },
1747
- deleted: true,
1748
- snapshot_ref: None,
1749
- metadata_ref: None,
1750
- created_at: "2026-01-03T00:00:00Z".to_string(),
1751
- updated_at: "2026-01-04T00:00:00Z".to_string(),
1752
- },
1753
- },
1754
- ]
1755
- );
1756
- }
1757
-
1758
- #[test]
1759
- fn delta_pack_ref_encoder_roundtrips_mixed_json_pack_indexes() {
1760
- let entity_id = EntityIdentity::single("entity-a");
1761
- let snapshot_ref = JsonRef::from_hash_bytes([1; 32]);
1762
- let metadata_ref = JsonRef::from_hash_bytes([2; 32]);
1763
- let change = crate::commit_store::ChangeRef {
1764
- id: "commit-a:change-live",
1765
- entity_id: &entity_id,
1766
- schema_key: "schema",
1767
- file_id: Some("file-a"),
1768
- snapshot_ref: Some(&snapshot_ref),
1769
- metadata_ref: Some(&metadata_ref),
1770
- created_at: "2026-01-01T00:00:00Z",
1771
- };
1772
- let locator = crate::commit_store::ChangeLocatorRef {
1773
- source_commit_id: "commit-a",
1774
- source_pack_id: 0,
1775
- source_ordinal: 0,
1776
- change_id: "commit-a:change-live",
1777
- };
1778
- let delta = TrackedStateDeltaRef {
1779
- change,
1780
- locator,
1781
- created_at: "2026-01-01T00:00:00Z",
1782
- updated_at: "2026-01-01T00:00:00Z",
1783
- };
1784
- let mut pack_indexes = HashMap::new();
1785
- pack_indexes.insert(*snapshot_ref.as_hash_array(), 1);
1786
- let pack_refs = vec![JsonRef::from_hash_bytes([9; 32]), snapshot_ref];
1787
-
1788
- let inline = encode_delta_pack_refs("commit-a", &[delta]).expect("inline delta pack");
1789
- assert!(!delta_pack_uses_json_pack_indexes(&inline).expect("inline mode should peek"));
1790
- let empty_indexes = HashMap::new();
1791
- let empty_index_pack = encode_delta_pack_refs_with_json_pack_indexes(
1792
- "commit-a",
1793
- &[delta],
1794
- Some(&empty_indexes),
1795
- )
1796
- .expect("empty-index delta pack");
1797
- assert_eq!(empty_index_pack, inline);
1798
- assert!(!delta_pack_uses_json_pack_indexes(&empty_index_pack)
1799
- .expect("empty index mode should peek"));
1800
- decode_delta_pack(&empty_index_pack, None).expect("empty index pack should decode inline");
1801
-
1802
- let mixed = encode_delta_pack_refs_with_json_pack_indexes(
1803
- "commit-a",
1804
- &[delta],
1805
- Some(&pack_indexes),
1806
- )
1807
- .expect("mixed delta pack");
1808
- assert!(delta_pack_uses_json_pack_indexes(&mixed).expect("mixed mode should peek"));
1809
-
1810
- assert!(
1811
- mixed.len() < inline.len(),
1812
- "pack-index refs should be smaller than inline refs"
1813
- );
1814
- assert!(decode_delta_pack(&mixed, None)
1815
- .expect_err("mixed refs require JSON pack refs")
1816
- .to_string()
1817
- .contains("needs JSON pack refs"));
1818
- let (_, decoded) =
1819
- decode_delta_pack(&mixed, Some(&pack_refs)).expect("mixed delta pack should decode");
1820
- assert_eq!(decoded[0].value.snapshot_ref, Some(snapshot_ref));
1821
- assert_eq!(decoded[0].value.metadata_ref, Some(metadata_ref));
1822
- }
1823
-
1824
- #[test]
1825
- fn delta_pack_stream_decoder_rejects_trailing_entry_bytes() {
1826
- let entity_id = EntityIdentity::single("entity");
1827
- let change = crate::commit_store::ChangeRef {
1828
- id: "commit-a:change-0",
1829
- entity_id: &entity_id,
1830
- schema_key: "schema",
1831
- file_id: None,
1832
- snapshot_ref: None,
1833
- metadata_ref: None,
1834
- created_at: "2026-01-01T00:00:00Z",
1835
- };
1836
- let locator = crate::commit_store::ChangeLocatorRef {
1837
- source_commit_id: "commit-a",
1838
- source_pack_id: 0,
1839
- source_ordinal: 0,
1840
- change_id: "commit-a:change-0",
1841
- };
1842
- let mut encoded = encode_delta_pack_refs(
1843
- "commit-a",
1844
- &[TrackedStateDeltaRef {
1845
- change,
1846
- locator,
1847
- created_at: "2026-01-01T00:00:00Z",
1848
- updated_at: "2026-01-01T00:00:00Z",
1849
- }],
1850
- )
1851
- .expect("delta pack should encode");
1852
-
1853
- let mut cursor = 5usize;
1854
- let _ = read_var_sized_string(&encoded, &mut cursor, "delta pack commit_id")
1855
- .expect("commit id should decode");
1856
- assert_eq!(
1857
- read_var_u32(&encoded, &mut cursor, "delta key prefix count")
1858
- .expect("prefix count should decode"),
1859
- 1
1860
- );
1861
- let _ =
1862
- decode_delta_key_prefix(&encoded, &mut cursor).expect("delta key prefix should decode");
1863
- encoded[cursor] = 0;
1864
-
1865
- let error =
1866
- decode_delta_pack(&encoded, None).expect_err("trailing entry bytes should reject");
1867
- assert!(
1868
- error.to_string().contains("trailing bytes"),
1869
- "error should mention trailing bytes: {error}"
1870
- );
1871
- }
1872
-
1873
- #[test]
1874
- fn delta_pack_rejects_overlong_varint() {
1875
- let mut encoded = Vec::new();
1876
- encoded.extend_from_slice(b"LXTD");
1877
- encoded.push(DELTA_PACK_VERSION);
1878
- encoded.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80]);
1879
-
1880
- let error = decode_delta_pack(&encoded, None).expect_err("overlong varint should reject");
1881
- assert!(
1882
- error.to_string().contains("varint exceeds u32"),
1883
- "error should mention overlong varint: {error}"
1884
- );
1885
- }
1886
-
1887
- #[test]
1888
- fn delta_pack_rejects_varint_above_u32() {
1889
- let mut encoded = Vec::new();
1890
- encoded.extend_from_slice(b"LXTD");
1891
- encoded.push(DELTA_PACK_VERSION);
1892
- encoded.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x1f]);
1893
-
1894
- let error = decode_delta_pack(&encoded, None).expect_err("too-large varint should reject");
1895
- assert!(
1896
- error.to_string().contains("varint exceeds u32"),
1897
- "error should mention oversized varint: {error}"
1898
- );
1899
- }
1900
-
1901
- #[test]
1902
- fn delta_pack_rejects_non_canonical_varint() {
1903
- let mut encoded = Vec::new();
1904
- encoded.extend_from_slice(b"LXTD");
1905
- encoded.push(DELTA_PACK_VERSION);
1906
- encoded.extend_from_slice(&[0x80, 0x00]);
1907
-
1908
- let error =
1909
- decode_delta_pack(&encoded, None).expect_err("non-canonical varint should reject");
1910
- assert!(
1911
- error.to_string().contains("non-canonical varint"),
1912
- "error should mention non-canonical varint: {error}"
1913
- );
1914
- }
1915
-
1916
- #[test]
1917
- fn delta_key_decoder_rejects_out_of_bounds_prefix_index() {
1918
- let mut encoded_key = Vec::new();
1919
- push_var_u32(&mut encoded_key, 1, "delta key prefix index").expect("prefix index");
1920
- push_var_entity_identity(&mut encoded_key, &EntityIdentity::single("entity"))
1921
- .expect("entity identity");
1922
-
1923
- let mut cursor = 0usize;
1924
- let err = decode_delta_key(
1925
- &encoded_key,
1926
- &mut cursor,
1927
- &[DeltaKeyPrefix {
1928
- schema_key: "schema".to_string(),
1929
- file_id: None,
1930
- }],
1931
- )
1932
- .expect_err("out-of-bounds prefix index should reject");
1933
-
1934
- assert!(err
1935
- .to_string()
1936
- .contains("tracked-state delta key prefix index 1 is out of bounds"));
1937
- }
1938
-
1939
1025
  #[test]
1940
1026
  fn encoded_value_len_matches_encoded_value_bytes() {
1941
1027
  let values = [
1942
1028
  TrackedStateIndexValue {
1943
- change_locator: ChangeLocator {
1944
- source_commit_id: "commit".to_string(),
1945
- source_pack_id: 0,
1946
- source_ordinal: 0,
1947
- change_id: "change".to_string(),
1948
- },
1029
+ change_id: "change".to_string(),
1030
+ commit_id: "commit".to_string(),
1949
1031
  deleted: false,
1950
1032
  snapshot_ref: None,
1951
1033
  metadata_ref: None,
@@ -1953,12 +1035,8 @@ mod tests {
1953
1035
  updated_at: "2026-01-02T00:00:00Z".to_string(),
1954
1036
  },
1955
1037
  TrackedStateIndexValue {
1956
- change_locator: ChangeLocator {
1957
- source_commit_id: "commit".to_string(),
1958
- source_pack_id: 1,
1959
- source_ordinal: 2,
1960
- change_id: "change-2".to_string(),
1961
- },
1038
+ change_id: "change-2".to_string(),
1039
+ commit_id: "commit".to_string(),
1962
1040
  deleted: true,
1963
1041
  snapshot_ref: Some(JsonRef::from_hash_bytes([3; 32])),
1964
1042
  metadata_ref: None,
@@ -1966,12 +1044,8 @@ mod tests {
1966
1044
  updated_at: "2026-01-02T00:00:00Z".to_string(),
1967
1045
  },
1968
1046
  TrackedStateIndexValue {
1969
- change_locator: ChangeLocator {
1970
- source_commit_id: "other".to_string(),
1971
- source_pack_id: 4,
1972
- source_ordinal: 8,
1973
- change_id: "change-3".to_string(),
1974
- },
1047
+ change_id: "change-3".to_string(),
1048
+ commit_id: "other".to_string(),
1975
1049
  deleted: false,
1976
1050
  snapshot_ref: None,
1977
1051
  metadata_ref: Some(JsonRef::from_hash_bytes([4; 32])),
@@ -2000,7 +1074,7 @@ mod tests {
2000
1074
 
2001
1075
  let encoded = encode_leaf_node(&entries);
2002
1076
  assert_eq!(encoded[0], NODE_KIND_LEAF);
2003
- assert_eq!(encoded[1], NODE_VERSION);
1077
+ assert_eq!(encoded[1], NODE_BRANCH);
2004
1078
  assert_eq!(&encoded[2..6], 2u32.to_be_bytes().as_slice());
2005
1079
  assert_eq!(&encoded[6..10], 0u32.to_be_bytes().as_slice());
2006
1080
 
@@ -2071,6 +1145,30 @@ mod tests {
2071
1145
  .contains("offset table does not cover full payload"));
2072
1146
  }
2073
1147
 
1148
+ #[test]
1149
+ fn leaf_node_codec_rejects_count_that_exceeds_remaining_bytes_before_allocating() {
1150
+ let mut encoded = vec![NODE_KIND_LEAF, NODE_BRANCH];
1151
+ encoded.extend_from_slice(&u32::MAX.to_be_bytes());
1152
+
1153
+ let error = decode_node_ref(&encoded).expect_err("impossible leaf count should reject");
1154
+
1155
+ assert!(error
1156
+ .to_string()
1157
+ .contains("field 'leaf offsets' exceeds remaining node bytes"));
1158
+ }
1159
+
1160
+ #[test]
1161
+ fn internal_node_codec_rejects_count_that_exceeds_remaining_bytes_before_allocating() {
1162
+ let mut encoded = vec![NODE_KIND_INTERNAL, NODE_BRANCH];
1163
+ encoded.extend_from_slice(&u32::MAX.to_be_bytes());
1164
+
1165
+ let error = decode_node_ref(&encoded).expect_err("impossible internal count should reject");
1166
+
1167
+ assert!(error
1168
+ .to_string()
1169
+ .contains("field 'internal children' exceeds remaining node bytes"));
1170
+ }
1171
+
2074
1172
  #[test]
2075
1173
  fn content_hash_is_blake3() {
2076
1174
  assert_eq!(hash_bytes(b"abc"), *blake3::hash(b"abc").as_bytes());