@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,18 +1,28 @@
1
- use crate::entity_identity::EntityIdentity;
2
- use crate::untracked_state::{UntrackedStateRow, UntrackedStateRowRef};
1
+ use crate::entity_pk::EntityPk;
2
+ use crate::untracked_state::{UntrackedStateIdentity, UntrackedStateRow, UntrackedStateRowRef};
3
3
  use crate::LixError;
4
4
 
5
5
  const UNTRACKED_STATE_FILE_IDENTIFIER: &str = "LXUS";
6
+ // Durable payload bytes:
7
+ // b"LXUP" | branch:u8 |
8
+ // snapshot_content_tag:u8 | [snapshot_content_len:u32be | snapshot_content:utf8] |
9
+ // metadata_tag:u8 | [metadata_len:u32be | metadata:utf8] |
10
+ // created_at_len:u32be | created_at:utf8 |
11
+ // updated_at_len:u32be | updated_at:utf8 |
12
+ // global:u8
13
+ const UNTRACKED_STATE_PAYLOAD_IDENTIFIER: &[u8; 4] = b"LXUP";
14
+ const UNTRACKED_STATE_PAYLOAD_BRANCH_V1: u8 = 1;
6
15
 
16
+ #[cfg_attr(not(feature = "storage-benches"), allow(dead_code))]
7
17
  pub(crate) fn encode_row_ref(row: UntrackedStateRowRef<'_>) -> Result<Vec<u8>, LixError> {
8
- let entity_id = row.entity_id.as_json_array_text().map_err(|error| {
18
+ let entity_pk = row.entity_pk.as_json_array_text().map_err(|error| {
9
19
  LixError::unknown(format!(
10
- "failed to encode untracked-state entity identity: {error}"
20
+ "failed to encode untracked-state entity primary key: {error}"
11
21
  ))
12
22
  })?;
13
23
 
14
24
  let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(256);
15
- let entity_id = builder.create_string(&entity_id);
25
+ let entity_pk = builder.create_string(&entity_pk);
16
26
  let schema_key = builder.create_string(row.schema_key);
17
27
  let file_id = row.file_id.map(|value| builder.create_string(value));
18
28
  let snapshot_content = row
@@ -21,12 +31,12 @@ pub(crate) fn encode_row_ref(row: UntrackedStateRowRef<'_>) -> Result<Vec<u8>, L
21
31
  let metadata = row.metadata.map(|value| builder.create_string(value));
22
32
  let created_at = builder.create_string(row.created_at);
23
33
  let updated_at = builder.create_string(row.updated_at);
24
- let version_id = builder.create_string(row.version_id);
34
+ let branch_id = builder.create_string(row.branch_id);
25
35
 
26
36
  let root = flatbuffer::create_untracked_state_row(
27
37
  &mut builder,
28
38
  &flatbuffer::UntrackedStateRowArgs {
29
- entity_id,
39
+ entity_pk,
30
40
  schema_key,
31
41
  file_id,
32
42
  snapshot_content,
@@ -34,13 +44,70 @@ pub(crate) fn encode_row_ref(row: UntrackedStateRowRef<'_>) -> Result<Vec<u8>, L
34
44
  created_at,
35
45
  updated_at,
36
46
  global: row.global,
37
- version_id,
47
+ branch_id,
38
48
  },
39
49
  );
40
50
  builder.finish(root, Some(UNTRACKED_STATE_FILE_IDENTIFIER));
41
51
  Ok(builder.finished_data().to_vec())
42
52
  }
43
53
 
54
+ pub(crate) fn encode_payload_ref(row: UntrackedStateRowRef<'_>) -> Result<Vec<u8>, LixError> {
55
+ let mut out = Vec::with_capacity(payload_capacity(row));
56
+ out.extend_from_slice(UNTRACKED_STATE_PAYLOAD_IDENTIFIER);
57
+ out.push(UNTRACKED_STATE_PAYLOAD_BRANCH_V1);
58
+ push_optional_string(&mut out, row.snapshot_content)?;
59
+ push_optional_string(&mut out, row.metadata)?;
60
+ push_string(&mut out, row.created_at)?;
61
+ push_string(&mut out, row.updated_at)?;
62
+ out.push(u8::from(row.global));
63
+ Ok(out)
64
+ }
65
+
66
+ pub(crate) fn decode_payload_with_identity(
67
+ identity: UntrackedStateIdentity,
68
+ bytes: &[u8],
69
+ ) -> Result<UntrackedStateRow, LixError> {
70
+ if !bytes.starts_with(UNTRACKED_STATE_PAYLOAD_IDENTIFIER) {
71
+ return Err(LixError::new(
72
+ "LIX_ERROR_UNKNOWN",
73
+ "failed to decode untracked-state payload: invalid payload identifier",
74
+ ));
75
+ }
76
+
77
+ let mut cursor = UNTRACKED_STATE_PAYLOAD_IDENTIFIER.len();
78
+ let branch = read_u8(bytes, &mut cursor, "branch")?;
79
+ if branch != UNTRACKED_STATE_PAYLOAD_BRANCH_V1 {
80
+ return Err(LixError::new(
81
+ "LIX_ERROR_UNKNOWN",
82
+ format!("failed to decode untracked-state payload: unsupported branch {branch}"),
83
+ ));
84
+ }
85
+ let snapshot_content = read_optional_string(bytes, &mut cursor, "snapshot_content")?;
86
+ let metadata = read_optional_string(bytes, &mut cursor, "metadata")?;
87
+ let created_at = read_string(bytes, &mut cursor, "created_at")?;
88
+ let updated_at = read_string(bytes, &mut cursor, "updated_at")?;
89
+ let global = read_bool(bytes, &mut cursor, "global")?;
90
+ if cursor != bytes.len() {
91
+ return Err(LixError::new(
92
+ "LIX_ERROR_UNKNOWN",
93
+ "failed to decode untracked-state payload: trailing bytes",
94
+ ));
95
+ }
96
+
97
+ Ok(UntrackedStateRow {
98
+ entity_pk: identity.entity_pk,
99
+ schema_key: identity.schema_key,
100
+ file_id: identity.file_id,
101
+ snapshot_content,
102
+ metadata,
103
+ created_at,
104
+ updated_at,
105
+ global,
106
+ branch_id: identity.branch_id,
107
+ })
108
+ }
109
+
110
+ #[allow(dead_code)]
44
111
  pub(crate) fn decode_row(bytes: &[u8]) -> Result<UntrackedStateRow, LixError> {
45
112
  if bytes.len() < flatbuffers::SIZE_UOFFSET + flatbuffers::FILE_IDENTIFIER_LENGTH
46
113
  || !flatbuffers::buffer_has_identifier(bytes, UNTRACKED_STATE_FILE_IDENTIFIER, false)
@@ -58,15 +125,15 @@ pub(crate) fn decode_row(bytes: &[u8]) -> Result<UntrackedStateRow, LixError> {
58
125
  )
59
126
  })?;
60
127
 
61
- let entity_id = required_str(row.entity_id(), "entity_id")?;
62
- let entity_id = EntityIdentity::from_json_array_text(entity_id).map_err(|error| {
128
+ let entity_pk = required_str(row.entity_pk(), "entity_pk")?;
129
+ let entity_pk = EntityPk::from_json_array_text(entity_pk).map_err(|error| {
63
130
  LixError::unknown(format!(
64
- "failed to decode untracked-state entity identity: {error}"
131
+ "failed to decode untracked-state entity primary key: {error}"
65
132
  ))
66
133
  })?;
67
134
 
68
135
  Ok(UntrackedStateRow {
69
- entity_id,
136
+ entity_pk,
70
137
  schema_key: required_str(row.schema_key(), "schema_key")?.to_string(),
71
138
  file_id: row.file_id().map(ToString::to_string),
72
139
  snapshot_content: row.snapshot_content().map(ToString::to_string),
@@ -74,10 +141,134 @@ pub(crate) fn decode_row(bytes: &[u8]) -> Result<UntrackedStateRow, LixError> {
74
141
  created_at: required_str(row.created_at(), "created_at")?.to_string(),
75
142
  updated_at: required_str(row.updated_at(), "updated_at")?.to_string(),
76
143
  global: row.global(),
77
- version_id: required_str(row.version_id(), "version_id")?.to_string(),
144
+ branch_id: required_str(row.branch_id(), "branch_id")?.to_string(),
78
145
  })
79
146
  }
80
147
 
148
+ fn payload_capacity(row: UntrackedStateRowRef<'_>) -> usize {
149
+ UNTRACKED_STATE_PAYLOAD_IDENTIFIER.len()
150
+ + 1
151
+ + optional_string_capacity(row.snapshot_content)
152
+ + optional_string_capacity(row.metadata)
153
+ + string_capacity(row.created_at)
154
+ + string_capacity(row.updated_at)
155
+ + 1
156
+ }
157
+
158
+ fn optional_string_capacity(value: Option<&str>) -> usize {
159
+ 1 + value.map_or(0, string_capacity)
160
+ }
161
+
162
+ fn string_capacity(value: &str) -> usize {
163
+ 4 + value.len()
164
+ }
165
+
166
+ fn push_optional_string(out: &mut Vec<u8>, value: Option<&str>) -> Result<(), LixError> {
167
+ match value {
168
+ Some(value) => {
169
+ out.push(1);
170
+ push_string(out, value)?;
171
+ }
172
+ None => out.push(0),
173
+ }
174
+ Ok(())
175
+ }
176
+
177
+ fn push_string(out: &mut Vec<u8>, value: &str) -> Result<(), LixError> {
178
+ let len = u32::try_from(value.len()).map_err(|_| {
179
+ LixError::new(
180
+ "LIX_ERROR_UNKNOWN",
181
+ "failed to encode untracked-state payload: string length exceeds u32",
182
+ )
183
+ })?;
184
+ out.extend_from_slice(&len.to_be_bytes());
185
+ out.extend_from_slice(value.as_bytes());
186
+ Ok(())
187
+ }
188
+
189
+ fn read_optional_string(
190
+ bytes: &[u8],
191
+ cursor: &mut usize,
192
+ field: &str,
193
+ ) -> Result<Option<String>, LixError> {
194
+ let tag = read_u8(bytes, cursor, field)?;
195
+ match tag {
196
+ 0 => Ok(None),
197
+ 1 => read_string(bytes, cursor, field).map(Some),
198
+ _ => Err(LixError::new(
199
+ "LIX_ERROR_UNKNOWN",
200
+ format!("failed to decode untracked-state payload: invalid optional tag for `{field}`"),
201
+ )),
202
+ }
203
+ }
204
+
205
+ fn read_string(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<String, LixError> {
206
+ let len = read_u32(bytes, cursor, field)? as usize;
207
+ let end = cursor.checked_add(len).ok_or_else(|| {
208
+ LixError::new(
209
+ "LIX_ERROR_UNKNOWN",
210
+ format!("failed to decode untracked-state payload: `{field}` length overflow"),
211
+ )
212
+ })?;
213
+ let value = bytes.get(*cursor..end).ok_or_else(|| {
214
+ LixError::new(
215
+ "LIX_ERROR_UNKNOWN",
216
+ format!("failed to decode untracked-state payload: truncated `{field}`"),
217
+ )
218
+ })?;
219
+ *cursor = end;
220
+ std::str::from_utf8(value)
221
+ .map(str::to_string)
222
+ .map_err(|error| {
223
+ LixError::new(
224
+ "LIX_ERROR_UNKNOWN",
225
+ format!("failed to decode untracked-state payload: invalid utf-8 for `{field}`: {error}"),
226
+ )
227
+ })
228
+ }
229
+
230
+ fn read_bool(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<bool, LixError> {
231
+ match read_u8(bytes, cursor, field)? {
232
+ 0 => Ok(false),
233
+ 1 => Ok(true),
234
+ _ => Err(LixError::new(
235
+ "LIX_ERROR_UNKNOWN",
236
+ format!("failed to decode untracked-state payload: invalid boolean for `{field}`"),
237
+ )),
238
+ }
239
+ }
240
+
241
+ fn read_u32(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<u32, LixError> {
242
+ let end = cursor.checked_add(4).ok_or_else(|| {
243
+ LixError::new(
244
+ "LIX_ERROR_UNKNOWN",
245
+ format!("failed to decode untracked-state payload: `{field}` cursor overflow"),
246
+ )
247
+ })?;
248
+ let raw = bytes.get(*cursor..end).ok_or_else(|| {
249
+ LixError::new(
250
+ "LIX_ERROR_UNKNOWN",
251
+ format!("failed to decode untracked-state payload: truncated `{field}` length"),
252
+ )
253
+ })?;
254
+ *cursor = end;
255
+ Ok(u32::from_be_bytes(
256
+ raw.try_into().expect("slice length checked"),
257
+ ))
258
+ }
259
+
260
+ fn read_u8(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<u8, LixError> {
261
+ let value = bytes.get(*cursor).copied().ok_or_else(|| {
262
+ LixError::new(
263
+ "LIX_ERROR_UNKNOWN",
264
+ format!("failed to decode untracked-state payload: truncated `{field}`"),
265
+ )
266
+ })?;
267
+ *cursor += 1;
268
+ Ok(value)
269
+ }
270
+
271
+ #[allow(dead_code)]
81
272
  fn required_str<'a>(value: Option<&'a str>, field: &str) -> Result<&'a str, LixError> {
82
273
  value.ok_or_else(|| {
83
274
  LixError::new(
@@ -87,6 +278,121 @@ fn required_str<'a>(value: Option<&'a str>, field: &str) -> Result<&'a str, LixE
87
278
  })
88
279
  }
89
280
 
281
+ #[cfg(test)]
282
+ mod tests {
283
+ use super::*;
284
+
285
+ fn row_ref<'a>(
286
+ entity_pk: &'a EntityPk,
287
+ snapshot_content: Option<&'a str>,
288
+ metadata: Option<&'a str>,
289
+ ) -> UntrackedStateRowRef<'a> {
290
+ UntrackedStateRowRef {
291
+ entity_pk,
292
+ schema_key: "schema.unicode",
293
+ file_id: Some("file-1"),
294
+ snapshot_content,
295
+ metadata,
296
+ created_at: "2026-05-19T00:00:00.000Z",
297
+ updated_at: "2026-05-19T00:00:01.000Z",
298
+ global: false,
299
+ branch_id: "branch-1",
300
+ }
301
+ }
302
+
303
+ fn identity(entity_pk: EntityPk) -> UntrackedStateIdentity {
304
+ UntrackedStateIdentity {
305
+ branch_id: "branch-1".to_string(),
306
+ schema_key: "schema.unicode".to_string(),
307
+ entity_pk,
308
+ file_id: Some("file-1".to_string()),
309
+ }
310
+ }
311
+
312
+ #[test]
313
+ fn payload_v1_roundtrips_with_key_identity() {
314
+ let entity_pk = EntityPk::tuple(vec!["id-1".to_string(), "東京".to_string()])
315
+ .expect("entity primary key should build");
316
+ let bytes = encode_payload_ref(row_ref(
317
+ &entity_pk,
318
+ Some("{\"hello\":\"world\"}"),
319
+ Some("{\"meta\":true}"),
320
+ ))
321
+ .expect("payload should encode");
322
+
323
+ assert_eq!(&bytes[..4], b"LXUP");
324
+ assert_eq!(bytes[4], 1);
325
+
326
+ let decoded = decode_payload_with_identity(identity(entity_pk.clone()), &bytes)
327
+ .expect("payload should decode");
328
+ assert_eq!(decoded.entity_pk, entity_pk);
329
+ assert_eq!(decoded.schema_key, "schema.unicode");
330
+ assert_eq!(decoded.file_id.as_deref(), Some("file-1"));
331
+ assert_eq!(
332
+ decoded.snapshot_content.as_deref(),
333
+ Some("{\"hello\":\"world\"}")
334
+ );
335
+ assert_eq!(decoded.metadata.as_deref(), Some("{\"meta\":true}"));
336
+ assert_eq!(decoded.created_at, "2026-05-19T00:00:00.000Z");
337
+ assert_eq!(decoded.updated_at, "2026-05-19T00:00:01.000Z");
338
+ assert!(!decoded.global);
339
+ assert_eq!(decoded.branch_id, "branch-1");
340
+ }
341
+
342
+ #[test]
343
+ fn payload_v1_roundtrips_absent_optional_fields() {
344
+ let entity_pk = EntityPk::single("id-1");
345
+ let bytes =
346
+ encode_payload_ref(row_ref(&entity_pk, None, None)).expect("payload should encode");
347
+ let decoded = decode_payload_with_identity(identity(entity_pk), &bytes)
348
+ .expect("payload should decode");
349
+ assert_eq!(decoded.snapshot_content, None);
350
+ assert_eq!(decoded.metadata, None);
351
+ }
352
+
353
+ #[test]
354
+ fn payload_decode_rejects_invalid_identifier() {
355
+ let entity_pk = EntityPk::single("id-1");
356
+ let error = decode_payload_with_identity(identity(entity_pk), b"LXUSnot-payload")
357
+ .expect_err("old full-row values are not accepted in v1 payload storage");
358
+ assert!(error.to_string().contains("invalid payload identifier"));
359
+ }
360
+
361
+ #[test]
362
+ fn payload_decode_rejects_unknown_branch() {
363
+ let entity_pk = EntityPk::single("id-1");
364
+ let mut bytes = encode_payload_ref(row_ref(&entity_pk, Some("{}"), None))
365
+ .expect("payload should encode");
366
+ bytes[4] = 2;
367
+ let error = decode_payload_with_identity(identity(entity_pk), &bytes)
368
+ .expect_err("unknown payload branch should fail");
369
+ assert!(error.to_string().contains("unsupported branch 2"));
370
+ }
371
+
372
+ #[test]
373
+ fn payload_decode_rejects_trailing_bytes() {
374
+ let entity_pk = EntityPk::single("id-1");
375
+ let mut bytes = encode_payload_ref(row_ref(&entity_pk, Some("{}"), None))
376
+ .expect("payload should encode");
377
+ bytes.push(0);
378
+ let error = decode_payload_with_identity(identity(entity_pk), &bytes)
379
+ .expect_err("trailing bytes should fail");
380
+ assert!(error.to_string().contains("trailing bytes"));
381
+ }
382
+
383
+ #[test]
384
+ fn payload_decode_rejects_truncated_string() {
385
+ let entity_pk = EntityPk::single("id-1");
386
+ let mut bytes = encode_payload_ref(row_ref(&entity_pk, Some("{}"), None))
387
+ .expect("payload should encode");
388
+ bytes.truncate(bytes.len() - 2);
389
+ let error = decode_payload_with_identity(identity(entity_pk), &bytes)
390
+ .expect_err("truncated payload should fail");
391
+ assert!(error.to_string().contains("truncated"));
392
+ }
393
+ }
394
+
395
+ #[allow(dead_code)]
90
396
  mod flatbuffer {
91
397
  #[derive(Copy, Clone, PartialEq)]
92
398
  pub(super) struct UntrackedStateRow<'a> {
@@ -105,7 +411,7 @@ mod flatbuffer {
105
411
  }
106
412
 
107
413
  impl<'a> UntrackedStateRow<'a> {
108
- const VT_ENTITY_ID: flatbuffers::VOffsetT = 4;
414
+ const VT_ENTITY_PK: flatbuffers::VOffsetT = 4;
109
415
  const VT_SCHEMA_KEY: flatbuffers::VOffsetT = 6;
110
416
  const VT_FILE_ID: flatbuffers::VOffsetT = 8;
111
417
  const VT_SNAPSHOT_CONTENT: flatbuffers::VOffsetT = 10;
@@ -113,13 +419,13 @@ mod flatbuffer {
113
419
  const VT_CREATED_AT: flatbuffers::VOffsetT = 14;
114
420
  const VT_UPDATED_AT: flatbuffers::VOffsetT = 16;
115
421
  const VT_GLOBAL: flatbuffers::VOffsetT = 18;
116
- const VT_VERSION_ID: flatbuffers::VOffsetT = 20;
422
+ const VT_BRANCH_ID: flatbuffers::VOffsetT = 20;
117
423
 
118
424
  #[inline]
119
- pub(super) fn entity_id(&self) -> Option<&'a str> {
425
+ pub(super) fn entity_pk(&self) -> Option<&'a str> {
120
426
  unsafe {
121
427
  self.table
122
- .get::<flatbuffers::ForwardsUOffset<&str>>(Self::VT_ENTITY_ID, None)
428
+ .get::<flatbuffers::ForwardsUOffset<&str>>(Self::VT_ENTITY_PK, None)
123
429
  }
124
430
  }
125
431
 
@@ -176,10 +482,10 @@ mod flatbuffer {
176
482
  }
177
483
 
178
484
  #[inline]
179
- pub(super) fn version_id(&self) -> Option<&'a str> {
485
+ pub(super) fn branch_id(&self) -> Option<&'a str> {
180
486
  unsafe {
181
487
  self.table
182
- .get::<flatbuffers::ForwardsUOffset<&str>>(Self::VT_VERSION_ID, None)
488
+ .get::<flatbuffers::ForwardsUOffset<&str>>(Self::VT_BRANCH_ID, None)
183
489
  }
184
490
  }
185
491
  }
@@ -193,8 +499,8 @@ mod flatbuffer {
193
499
  verifier
194
500
  .visit_table(position)?
195
501
  .visit_field::<flatbuffers::ForwardsUOffset<&str>>(
196
- "entity_id",
197
- Self::VT_ENTITY_ID,
502
+ "entity_pk",
503
+ Self::VT_ENTITY_PK,
198
504
  true,
199
505
  )?
200
506
  .visit_field::<flatbuffers::ForwardsUOffset<&str>>(
@@ -229,8 +535,8 @@ mod flatbuffer {
229
535
  )?
230
536
  .visit_field::<bool>("global", Self::VT_GLOBAL, false)?
231
537
  .visit_field::<flatbuffers::ForwardsUOffset<&str>>(
232
- "version_id",
233
- Self::VT_VERSION_ID,
538
+ "branch_id",
539
+ Self::VT_BRANCH_ID,
234
540
  true,
235
541
  )?
236
542
  .finish();
@@ -238,8 +544,9 @@ mod flatbuffer {
238
544
  }
239
545
  }
240
546
 
547
+ #[cfg_attr(not(feature = "storage-benches"), allow(dead_code))]
241
548
  pub(super) struct UntrackedStateRowArgs<'a> {
242
- pub(super) entity_id: flatbuffers::WIPOffset<&'a str>,
549
+ pub(super) entity_pk: flatbuffers::WIPOffset<&'a str>,
243
550
  pub(super) schema_key: flatbuffers::WIPOffset<&'a str>,
244
551
  pub(super) file_id: Option<flatbuffers::WIPOffset<&'a str>>,
245
552
  pub(super) snapshot_content: Option<flatbuffers::WIPOffset<&'a str>>,
@@ -247,17 +554,18 @@ mod flatbuffer {
247
554
  pub(super) created_at: flatbuffers::WIPOffset<&'a str>,
248
555
  pub(super) updated_at: flatbuffers::WIPOffset<&'a str>,
249
556
  pub(super) global: bool,
250
- pub(super) version_id: flatbuffers::WIPOffset<&'a str>,
557
+ pub(super) branch_id: flatbuffers::WIPOffset<&'a str>,
251
558
  }
252
559
 
560
+ #[cfg_attr(not(feature = "storage-benches"), allow(dead_code))]
253
561
  pub(super) fn create_untracked_state_row<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>(
254
562
  builder: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>,
255
563
  args: &'args UntrackedStateRowArgs<'args>,
256
564
  ) -> flatbuffers::WIPOffset<UntrackedStateRow<'bldr>> {
257
565
  let start = builder.start_table();
258
566
  builder.push_slot_always::<flatbuffers::WIPOffset<_>>(
259
- UntrackedStateRow::VT_VERSION_ID,
260
- args.version_id,
567
+ UntrackedStateRow::VT_BRANCH_ID,
568
+ args.branch_id,
261
569
  );
262
570
  builder.push_slot::<bool>(UntrackedStateRow::VT_GLOBAL, args.global, false);
263
571
  builder.push_slot_always::<flatbuffers::WIPOffset<_>>(
@@ -291,8 +599,8 @@ mod flatbuffer {
291
599
  args.schema_key,
292
600
  );
293
601
  builder.push_slot_always::<flatbuffers::WIPOffset<_>>(
294
- UntrackedStateRow::VT_ENTITY_ID,
295
- args.entity_id,
602
+ UntrackedStateRow::VT_ENTITY_PK,
603
+ args.entity_pk,
296
604
  );
297
605
  let offset = builder.end_table(start);
298
606
  flatbuffers::WIPOffset::new(offset.value())
@@ -1,4 +1,4 @@
1
- use crate::storage::{StorageReader, StorageWriteSet};
1
+ use crate::storage::{StorageRead, StorageWriteSet};
2
2
  use crate::untracked_state::{
3
3
  MaterializedUntrackedStateRow, UntrackedStateIdentity, UntrackedStateIdentityRef,
4
4
  UntrackedStateRowRef, UntrackedStateRowRequest, UntrackedStateScanRequest,
@@ -23,7 +23,7 @@ impl UntrackedStateContext {
23
23
  /// The caller decides which KV store supplies visibility for the read.
24
24
  pub(crate) fn reader<S>(&self, store: S) -> UntrackedStateStoreReader<S>
25
25
  where
26
- S: StorageReader,
26
+ S: StorageRead + Send + Sync,
27
27
  {
28
28
  UntrackedStateStoreReader { store }
29
29
  }
@@ -44,20 +44,20 @@ pub(crate) struct UntrackedStateStoreReader<S> {
44
44
 
45
45
  impl<S> UntrackedStateStoreReader<S>
46
46
  where
47
- S: StorageReader,
47
+ S: StorageRead + Send + Sync,
48
48
  {
49
49
  pub(crate) async fn scan_rows(
50
50
  &mut self,
51
51
  request: &UntrackedStateScanRequest,
52
52
  ) -> Result<Vec<MaterializedUntrackedStateRow>, LixError> {
53
- crate::untracked_state::storage::scan_rows(&mut self.store, request).await
53
+ crate::untracked_state::storage::scan_rows(&self.store, request).await
54
54
  }
55
55
 
56
56
  pub(crate) async fn load_row(
57
57
  &mut self,
58
58
  request: &UntrackedStateRowRequest,
59
59
  ) -> Result<Option<MaterializedUntrackedStateRow>, LixError> {
60
- crate::untracked_state::storage::load_row(&mut self.store, request).await
60
+ crate::untracked_state::storage::load_row(&self.store, request).await
61
61
  }
62
62
 
63
63
  pub(crate) async fn existing_identities<'a, I>(
@@ -67,7 +67,7 @@ where
67
67
  where
68
68
  I: IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
69
69
  {
70
- crate::untracked_state::storage::existing_identities(&mut self.store, identities).await
70
+ crate::untracked_state::storage::existing_identities(&self.store, identities).await
71
71
  }
72
72
  }
73
73
 
@@ -89,7 +89,7 @@ impl UntrackedStateWriter<'_> {
89
89
  }
90
90
 
91
91
  /// Removes untracked rows by exact identity.
92
- pub(crate) fn stage_delete_rows<'a, I>(&mut self, identities: I)
92
+ pub(crate) fn stage_delete_rows<'a, I>(&mut self, identities: I) -> Result<(), LixError>
93
93
  where
94
94
  I: IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
95
95
  {
@@ -17,7 +17,7 @@ pub(crate) fn materialize_row(
17
17
  None
18
18
  };
19
19
  Ok(MaterializedUntrackedStateRow {
20
- entity_id: row.entity_id,
20
+ entity_pk: row.entity_pk,
21
21
  schema_key: row.schema_key,
22
22
  file_id: row.file_id,
23
23
  snapshot_content,
@@ -26,7 +26,7 @@ pub(crate) fn materialize_row(
26
26
  created_at: row.created_at,
27
27
  updated_at: row.updated_at,
28
28
  global: row.global,
29
- version_id: row.version_id,
29
+ branch_id: row.branch_id,
30
30
  })
31
31
  }
32
32
 
@@ -1,4 +1,4 @@
1
- mod codec;
1
+ pub(crate) mod codec;
2
2
  mod context;
3
3
  mod materialization;
4
4
  pub(crate) mod storage;