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

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 (274) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +38 -207
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +38 -71
  16. package/SKILL.md +0 -507
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,1614 +0,0 @@
1
- use std::collections::{HashMap, HashSet};
2
-
3
- use async_trait::async_trait;
4
- use bytes::Bytes;
5
-
6
- use super::codec::{
7
- decode_change_record, decode_commit_change_ref_chunk, decode_commit_record,
8
- encode_change_record, encode_commit_change_ref_chunk, encode_commit_record,
9
- };
10
- use super::store::{
11
- change_key, commit_change_ref_chunk_key, commit_change_ref_chunk_prefix, commit_key,
12
- CHANGE_SPACE, COMMIT_CHANGE_REF_CHUNK_SPACE, COMMIT_SPACE,
13
- };
14
- use crate::changelog::{
15
- ChangeLoadBatch, ChangeLoadRequest, ChangeRecord, ChangeScanBatch, ChangeScanRequest,
16
- ChangelogAppend, ChangelogReader, ChangelogWriter, CommitChangeRef, CommitChangeRefChunk,
17
- CommitChangeRefSet, CommitLoadBatch, CommitLoadEntry, CommitLoadRequest, CommitProjection,
18
- CommitRecord, CommitScanBatch, CommitScanRequest, GcPlan, GcRoot,
19
- };
20
- use crate::storage::{
21
- PointReadPlan, ScanPlan, StorageBackend, StorageContext, StorageCoreProjection,
22
- StorageGetOptions, StorageKey, StoragePrefix, StorageProjectedValue, StorageRead,
23
- StorageReadOptions, StorageScanOptions, StorageSpace, StorageWriteSet,
24
- };
25
- use crate::LixError;
26
-
27
- const COMMIT_CHANGE_REF_CHUNK_FORMAT_VERSION: u32 = 1;
28
- const COMMIT_CHANGE_REF_CHUNK_TARGET_BYTES: usize = 64 * 1024;
29
- const COMMIT_CHANGE_REF_CHUNK_MAX_BYTES: usize = 128 * 1024;
30
- const COMMIT_CHANGE_REF_CHUNK_MAX_ENTRIES: usize = 2048;
31
- const SCAN_PAGE_LIMIT: usize = 1024;
32
-
33
- #[derive(Clone, Copy, Debug, Default)]
34
- pub(crate) struct ChangelogContext;
35
-
36
- impl ChangelogContext {
37
- pub(crate) fn new() -> Self {
38
- Self
39
- }
40
-
41
- pub(crate) fn reader<S>(&self, store: S) -> ChangelogStoreReader<S>
42
- where
43
- S: ChangelogStorageRead,
44
- {
45
- ChangelogStoreReader { store }
46
- }
47
-
48
- pub(crate) fn writer<'a, S>(
49
- &self,
50
- store: &'a mut S,
51
- writes: &'a mut StorageWriteSet,
52
- ) -> ChangelogStoreWriter<'a, S>
53
- where
54
- S: ChangelogStorageRead + ?Sized,
55
- {
56
- ChangelogStoreWriter {
57
- store,
58
- writes,
59
- staged_commits: HashMap::new(),
60
- staged_changes: HashMap::new(),
61
- staged_commit_change_ref_chunks: HashMap::new(),
62
- }
63
- }
64
- }
65
-
66
- pub(crate) struct ChangelogStoreReader<S> {
67
- store: S,
68
- }
69
-
70
- pub(crate) struct ChangelogStoreWriter<'a, S: ?Sized> {
71
- store: &'a mut S,
72
- writes: &'a mut StorageWriteSet,
73
- staged_commits: HashMap<String, CommitRecord>,
74
- staged_changes: HashMap<String, ChangeRecord>,
75
- staged_commit_change_ref_chunks: HashMap<String, Vec<CommitChangeRefChunk>>,
76
- }
77
-
78
- #[derive(Debug)]
79
- pub(crate) struct ChangelogScanPage {
80
- pub(super) keys: Vec<Vec<u8>>,
81
- pub(super) values: Vec<Vec<u8>>,
82
- pub(super) resume_after: Option<Vec<u8>>,
83
- }
84
-
85
- #[async_trait]
86
- pub(crate) trait ChangelogStorageRead {
87
- async fn changelog_get_many(
88
- &mut self,
89
- space: StorageSpace,
90
- keys: Vec<Vec<u8>>,
91
- ) -> Result<Vec<Option<Vec<u8>>>, LixError>;
92
-
93
- async fn changelog_scan(
94
- &mut self,
95
- space: StorageSpace,
96
- prefix: Vec<u8>,
97
- after: Option<Vec<u8>>,
98
- limit: usize,
99
- projection: StorageCoreProjection,
100
- ) -> Result<ChangelogScanPage, LixError>;
101
- }
102
-
103
- #[async_trait]
104
- impl<T> ChangelogStorageRead for T
105
- where
106
- T: StorageRead + Send,
107
- {
108
- async fn changelog_get_many(
109
- &mut self,
110
- space: StorageSpace,
111
- keys: Vec<Vec<u8>>,
112
- ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
113
- native_get_many(self, space, keys)
114
- }
115
-
116
- async fn changelog_scan(
117
- &mut self,
118
- space: StorageSpace,
119
- prefix: Vec<u8>,
120
- after: Option<Vec<u8>>,
121
- limit: usize,
122
- projection: StorageCoreProjection,
123
- ) -> Result<ChangelogScanPage, LixError> {
124
- native_scan(self, space, prefix, after, limit, projection)
125
- }
126
- }
127
-
128
- #[async_trait]
129
- impl<B> ChangelogStorageRead for StorageContext<B>
130
- where
131
- B: StorageBackend + Send,
132
- {
133
- async fn changelog_get_many(
134
- &mut self,
135
- space: StorageSpace,
136
- keys: Vec<Vec<u8>>,
137
- ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
138
- let mut read = self.begin_read(StorageReadOptions::default())?;
139
- native_get_many(&mut read, space, keys)
140
- }
141
-
142
- async fn changelog_scan(
143
- &mut self,
144
- space: StorageSpace,
145
- prefix: Vec<u8>,
146
- after: Option<Vec<u8>>,
147
- limit: usize,
148
- projection: StorageCoreProjection,
149
- ) -> Result<ChangelogScanPage, LixError> {
150
- let mut read = self.begin_read(StorageReadOptions::default())?;
151
- native_scan(&mut read, space, prefix, after, limit, projection)
152
- }
153
- }
154
-
155
- #[async_trait]
156
- impl<S> ChangelogReader for ChangelogStoreReader<S>
157
- where
158
- S: ChangelogStorageRead + Send,
159
- {
160
- async fn plan_gc(&mut self, roots: &[GcRoot]) -> Result<GcPlan, LixError> {
161
- Ok(empty_gc_plan(roots))
162
- }
163
-
164
- async fn load_commits(
165
- &mut self,
166
- request: CommitLoadRequest<'_>,
167
- ) -> Result<CommitLoadBatch, LixError> {
168
- load_commits_from_store(&mut self.store, request).await
169
- }
170
-
171
- async fn scan_commits(
172
- &mut self,
173
- request: CommitScanRequest<'_>,
174
- ) -> Result<CommitScanBatch, LixError> {
175
- scan_commits_from_store(&mut self.store, request).await
176
- }
177
-
178
- async fn load_changes(
179
- &mut self,
180
- request: ChangeLoadRequest<'_>,
181
- ) -> Result<ChangeLoadBatch, LixError> {
182
- load_changes_from_store(&mut self.store, request).await
183
- }
184
-
185
- async fn scan_changes(
186
- &mut self,
187
- request: ChangeScanRequest<'_>,
188
- ) -> Result<ChangeScanBatch, LixError> {
189
- scan_changes_from_store(&mut self.store, request).await
190
- }
191
- }
192
-
193
- #[async_trait]
194
- impl<S> ChangelogReader for ChangelogStoreWriter<'_, S>
195
- where
196
- S: ChangelogStorageRead + Send + ?Sized,
197
- {
198
- async fn plan_gc(&mut self, roots: &[GcRoot]) -> Result<GcPlan, LixError> {
199
- Ok(empty_gc_plan(roots))
200
- }
201
-
202
- async fn load_commits(
203
- &mut self,
204
- request: CommitLoadRequest<'_>,
205
- ) -> Result<CommitLoadBatch, LixError> {
206
- let stored = load_commits_from_store(self.store, request).await?;
207
- let entries = request
208
- .commit_ids
209
- .iter()
210
- .zip(stored.entries.into_iter())
211
- .map(|(commit_id, stored)| {
212
- if let Some(record) = self.staged_commits.get(commit_id) {
213
- return Some(project_commit_entry(
214
- request.projection,
215
- record.clone(),
216
- self.staged_commit_change_ref_chunks
217
- .get(commit_id)
218
- .cloned()
219
- .unwrap_or_default(),
220
- ));
221
- }
222
- stored
223
- })
224
- .collect();
225
- Ok(CommitLoadBatch { entries })
226
- }
227
-
228
- async fn scan_commits(
229
- &mut self,
230
- request: CommitScanRequest<'_>,
231
- ) -> Result<CommitScanBatch, LixError> {
232
- let mut batch = scan_commits_from_store(self.store, request).await?;
233
- let mut staged = self
234
- .staged_commits
235
- .values()
236
- .filter(|commit| {
237
- request
238
- .start_after
239
- .map(|start_after| commit.commit_id.as_str() > start_after)
240
- .unwrap_or(true)
241
- })
242
- .cloned()
243
- .collect::<Vec<_>>();
244
- staged.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
245
- for commit in staged {
246
- batch.entries.push(project_commit_entry(
247
- request.projection,
248
- commit.clone(),
249
- self.staged_commit_change_ref_chunks
250
- .get(&commit.commit_id)
251
- .cloned()
252
- .unwrap_or_default(),
253
- ));
254
- }
255
- batch.entries.sort_by(|left, right| {
256
- commit_entry_id(left)
257
- .unwrap_or_default()
258
- .cmp(commit_entry_id(right).unwrap_or_default())
259
- });
260
- let limit = request.limit.unwrap_or(usize::MAX);
261
- if batch.entries.len() > limit {
262
- batch.entries.truncate(limit);
263
- batch.next_start_after = batch
264
- .entries
265
- .last()
266
- .and_then(commit_entry_id)
267
- .map(str::to_string);
268
- }
269
- Ok(batch)
270
- }
271
-
272
- async fn load_changes(
273
- &mut self,
274
- request: ChangeLoadRequest<'_>,
275
- ) -> Result<ChangeLoadBatch, LixError> {
276
- let stored = load_changes_from_store(self.store, request).await?;
277
- let entries = request
278
- .change_ids
279
- .iter()
280
- .zip(stored.entries.into_iter())
281
- .map(|(change_id, stored)| self.staged_changes.get(change_id).cloned().or(stored))
282
- .collect();
283
- Ok(ChangeLoadBatch { entries })
284
- }
285
-
286
- async fn scan_changes(
287
- &mut self,
288
- request: ChangeScanRequest<'_>,
289
- ) -> Result<ChangeScanBatch, LixError> {
290
- let mut batch = scan_changes_from_store(self.store, request).await?;
291
- let mut staged = self
292
- .staged_changes
293
- .values()
294
- .filter(|change| {
295
- request
296
- .start_after
297
- .map(|start_after| change.change_id.as_str() > start_after)
298
- .unwrap_or(true)
299
- })
300
- .cloned()
301
- .collect::<Vec<_>>();
302
- staged.sort_by(|left, right| left.change_id.cmp(&right.change_id));
303
- batch.entries.extend(staged);
304
- batch
305
- .entries
306
- .sort_by(|left, right| left.change_id.cmp(&right.change_id));
307
- batch
308
- .entries
309
- .dedup_by(|left, right| left.change_id == right.change_id);
310
- let limit = request.limit.unwrap_or(usize::MAX);
311
- if batch.entries.len() > limit {
312
- batch.entries.truncate(limit);
313
- batch.next_start_after = batch.entries.last().map(|change| change.change_id.clone());
314
- }
315
- Ok(batch)
316
- }
317
- }
318
-
319
- #[async_trait]
320
- impl<S> ChangelogWriter for ChangelogStoreWriter<'_, S>
321
- where
322
- S: ChangelogStorageRead + Send + ?Sized,
323
- {
324
- async fn stage_append(&mut self, append: ChangelogAppend) -> Result<(), LixError> {
325
- self.validate_append(&append).await?;
326
-
327
- for change in append.changes {
328
- self.writes.put(
329
- CHANGE_SPACE,
330
- change_key(&change.change_id),
331
- encode_change_record(&change)?,
332
- );
333
- self.staged_changes.insert(change.change_id.clone(), change);
334
- }
335
-
336
- let chunks = chunk_commit_change_refs(append.commit_change_refs)?;
337
- for commit in append.commits {
338
- self.writes.put(
339
- COMMIT_SPACE,
340
- commit_key(&commit.commit_id),
341
- encode_commit_record(&commit)?,
342
- );
343
- self.staged_commits.insert(commit.commit_id.clone(), commit);
344
- }
345
-
346
- for (commit_id, commit_chunks) in chunks {
347
- for (chunk_no, chunk) in commit_chunks.iter().enumerate() {
348
- self.writes.put(
349
- COMMIT_CHANGE_REF_CHUNK_SPACE,
350
- commit_change_ref_chunk_key(&commit_id, chunk_no as u32),
351
- encode_commit_change_ref_chunk(chunk)?,
352
- );
353
- }
354
- self.staged_commit_change_ref_chunks
355
- .insert(commit_id, commit_chunks);
356
- }
357
-
358
- Ok(())
359
- }
360
-
361
- async fn collect_garbage(&mut self, roots: &[GcRoot]) -> Result<GcPlan, LixError> {
362
- Ok(empty_gc_plan(roots))
363
- }
364
- }
365
-
366
- impl<S> ChangelogStoreWriter<'_, S>
367
- where
368
- S: ChangelogStorageRead + Send + ?Sized,
369
- {
370
- async fn validate_append(&mut self, append: &ChangelogAppend) -> Result<(), LixError> {
371
- validate_unique(
372
- append
373
- .commits
374
- .iter()
375
- .map(|commit| commit.commit_id.as_str()),
376
- "commit_id",
377
- )?;
378
- validate_unique(
379
- append
380
- .changes
381
- .iter()
382
- .map(|change| change.change_id.as_str()),
383
- "change_id",
384
- )?;
385
- validate_unique(
386
- append
387
- .commits
388
- .iter()
389
- .map(|commit| commit.change_id.as_str()),
390
- "commit change_id",
391
- )?;
392
- validate_unique(
393
- append
394
- .commit_change_refs
395
- .iter()
396
- .map(|refs| refs.commit_id.as_str()),
397
- "commit change ref commit_id",
398
- )?;
399
-
400
- let append_commit_ids = append
401
- .commits
402
- .iter()
403
- .map(|commit| commit.commit_id.as_str())
404
- .collect::<HashSet<_>>();
405
- let append_changes = append
406
- .changes
407
- .iter()
408
- .map(|change| (change.change_id.as_str(), change))
409
- .collect::<HashMap<_, _>>();
410
-
411
- self.reject_existing_commits(&append_commit_ids).await?;
412
- self.reject_existing_changes(append_changes.keys().copied())
413
- .await?;
414
- self.reject_commit_change_id_collisions(append, &append_changes)
415
- .await?;
416
- self.validate_parent_commits(append, &append_commit_ids)
417
- .await?;
418
-
419
- for commit in &append.commits {
420
- if !append
421
- .commit_change_refs
422
- .iter()
423
- .any(|refs| refs.commit_id == commit.commit_id)
424
- {
425
- return Err(LixError::unknown(format!(
426
- "changelog commit '{}' is missing commit change refs",
427
- commit.commit_id
428
- )));
429
- }
430
- }
431
-
432
- for refs in &append.commit_change_refs {
433
- if !append_commit_ids.contains(refs.commit_id.as_str()) {
434
- return Err(LixError::unknown(format!(
435
- "changelog commit change refs target missing staged commit '{}'",
436
- refs.commit_id
437
- )));
438
- }
439
- validate_unique_ref_keys(&refs.entries, &refs.commit_id)?;
440
- self.validate_change_refs(refs, &append_changes).await?;
441
- }
442
-
443
- Ok(())
444
- }
445
-
446
- async fn reject_commit_change_id_collisions(
447
- &mut self,
448
- append: &ChangelogAppend,
449
- append_changes: &HashMap<&str, &ChangeRecord>,
450
- ) -> Result<(), LixError> {
451
- for commit in &append.commits {
452
- if append_changes.contains_key(commit.change_id.as_str())
453
- || self.change_exists(&commit.change_id).await?
454
- || self
455
- .staged_commits
456
- .values()
457
- .any(|staged| staged.change_id == commit.change_id)
458
- {
459
- return Err(LixError::unknown(format!(
460
- "changelog commit '{}' derived change_id '{}' collides with an existing change id",
461
- commit.commit_id, commit.change_id
462
- )));
463
- }
464
- }
465
- let mut start_after = None::<String>;
466
- loop {
467
- let batch = scan_commits_from_store(
468
- self.store,
469
- CommitScanRequest {
470
- start_after: start_after.as_deref(),
471
- limit: Some(SCAN_PAGE_LIMIT),
472
- projection: CommitProjection::Record,
473
- },
474
- )
475
- .await?;
476
- for entry in batch.entries {
477
- let CommitLoadEntry::Record(record) = entry else {
478
- continue;
479
- };
480
- if append
481
- .commits
482
- .iter()
483
- .any(|commit| commit.change_id == record.change_id)
484
- {
485
- return Err(LixError::unknown(format!(
486
- "changelog commit derived change_id '{}' already exists",
487
- record.change_id
488
- )));
489
- }
490
- }
491
- let Some(next) = batch.next_start_after else {
492
- break;
493
- };
494
- start_after = Some(next);
495
- }
496
- Ok(())
497
- }
498
-
499
- async fn reject_existing_commits<'a>(
500
- &mut self,
501
- commit_ids: &HashSet<&'a str>,
502
- ) -> Result<(), LixError> {
503
- let keys = commit_ids
504
- .iter()
505
- .map(|commit_id| commit_key(commit_id))
506
- .collect::<Vec<_>>();
507
- for (commit_id, found) in commit_ids
508
- .iter()
509
- .zip(get_many(self.store, COMMIT_SPACE, keys).await?)
510
- {
511
- if found.is_some() || self.staged_commits.contains_key(*commit_id) {
512
- return Err(LixError::unknown(format!(
513
- "changelog commit '{}' already exists",
514
- commit_id
515
- )));
516
- }
517
- }
518
- Ok(())
519
- }
520
-
521
- async fn reject_existing_changes<'a>(
522
- &mut self,
523
- change_ids: impl IntoIterator<Item = &'a str>,
524
- ) -> Result<(), LixError> {
525
- let change_ids = change_ids.into_iter().collect::<Vec<_>>();
526
- let keys = change_ids
527
- .iter()
528
- .map(|change_id| change_key(change_id))
529
- .collect::<Vec<_>>();
530
- for (change_id, found) in change_ids
531
- .iter()
532
- .zip(get_many(self.store, CHANGE_SPACE, keys).await?)
533
- {
534
- if found.is_some() || self.staged_changes.contains_key(*change_id) {
535
- return Err(LixError::unknown(format!(
536
- "changelog change '{}' already exists",
537
- change_id
538
- )));
539
- }
540
- }
541
- Ok(())
542
- }
543
-
544
- async fn validate_parent_commits(
545
- &mut self,
546
- append: &ChangelogAppend,
547
- append_commit_ids: &HashSet<&str>,
548
- ) -> Result<(), LixError> {
549
- let parent_ids = append
550
- .commits
551
- .iter()
552
- .flat_map(|commit| commit.parent_commit_ids.iter().map(String::as_str))
553
- .filter(|parent_id| !append_commit_ids.contains(parent_id))
554
- .collect::<HashSet<_>>();
555
- let keys = parent_ids
556
- .iter()
557
- .map(|parent_id| commit_key(parent_id))
558
- .collect::<Vec<_>>();
559
- for (parent_id, found) in parent_ids
560
- .iter()
561
- .zip(get_many(self.store, COMMIT_SPACE, keys).await?)
562
- {
563
- if found.is_none() && !self.staged_commits.contains_key(*parent_id) {
564
- return Err(LixError::unknown(format!(
565
- "changelog parent commit '{}' does not exist",
566
- parent_id
567
- )));
568
- }
569
- }
570
- Ok(())
571
- }
572
-
573
- async fn validate_change_refs(
574
- &mut self,
575
- refs: &CommitChangeRefSet,
576
- append_changes: &HashMap<&str, &ChangeRecord>,
577
- ) -> Result<(), LixError> {
578
- let missing_from_append = refs
579
- .entries
580
- .iter()
581
- .filter(|entry| !append_changes.contains_key(entry.change_id.as_str()))
582
- .map(|entry| entry.change_id.as_str())
583
- .collect::<HashSet<_>>();
584
- let stored = self
585
- .load_stored_changes(missing_from_append.iter().copied())
586
- .await?;
587
-
588
- for entry in &refs.entries {
589
- let change = append_changes
590
- .get(entry.change_id.as_str())
591
- .copied()
592
- .or_else(|| self.staged_changes.get(&entry.change_id))
593
- .or_else(|| stored.get(entry.change_id.as_str()))
594
- .ok_or_else(|| {
595
- LixError::unknown(format!(
596
- "changelog commit '{}' references missing change '{}'",
597
- refs.commit_id, entry.change_id
598
- ))
599
- })?;
600
- validate_ref_matches_change(&refs.commit_id, entry, change)?;
601
- }
602
- Ok(())
603
- }
604
-
605
- async fn load_stored_changes<'a>(
606
- &mut self,
607
- change_ids: impl IntoIterator<Item = &'a str>,
608
- ) -> Result<HashMap<String, ChangeRecord>, LixError> {
609
- let change_ids = change_ids.into_iter().collect::<Vec<_>>();
610
- let keys = change_ids
611
- .iter()
612
- .map(|change_id| change_key(change_id))
613
- .collect::<Vec<_>>();
614
- let values = get_many(self.store, CHANGE_SPACE, keys).await?;
615
- let mut out = HashMap::new();
616
- for (change_id, value) in change_ids.into_iter().zip(values) {
617
- if let Some(value) = value {
618
- out.insert(change_id.to_string(), decode_change_record(&value)?);
619
- }
620
- }
621
- Ok(out)
622
- }
623
-
624
- async fn change_exists(&mut self, change_id: &str) -> Result<bool, LixError> {
625
- if self.staged_changes.contains_key(change_id) {
626
- return Ok(true);
627
- }
628
- Ok(get_one(self.store, CHANGE_SPACE, change_key(change_id))
629
- .await?
630
- .is_some())
631
- }
632
- }
633
-
634
- async fn load_commits_from_store(
635
- store: &mut (impl ChangelogStorageRead + ?Sized),
636
- request: CommitLoadRequest<'_>,
637
- ) -> Result<CommitLoadBatch, LixError> {
638
- let keys = request
639
- .commit_ids
640
- .iter()
641
- .map(|commit_id| commit_key(commit_id))
642
- .collect::<Vec<_>>();
643
- let commit_values = get_many(store, COMMIT_SPACE, keys).await?;
644
- let mut entries = Vec::with_capacity(request.commit_ids.len());
645
- for (commit_id, value) in request.commit_ids.iter().zip(commit_values) {
646
- let Some(value) = value else {
647
- entries.push(None);
648
- continue;
649
- };
650
- let record = decode_commit_record(&value)?;
651
- let chunks = match request.projection {
652
- CommitProjection::Record => Vec::new(),
653
- CommitProjection::ChangeRefs | CommitProjection::Full => {
654
- load_commit_change_ref_chunks(store, commit_id).await?
655
- }
656
- };
657
- entries.push(Some(project_commit_entry(
658
- request.projection,
659
- record,
660
- chunks,
661
- )));
662
- }
663
- Ok(CommitLoadBatch { entries })
664
- }
665
-
666
- async fn scan_commits_from_store(
667
- store: &mut (impl ChangelogStorageRead + ?Sized),
668
- request: CommitScanRequest<'_>,
669
- ) -> Result<CommitScanBatch, LixError> {
670
- if request.projection != CommitProjection::Record {
671
- return Err(LixError::new(
672
- LixError::CODE_INTERNAL_ERROR,
673
- "changelog scan_commits currently supports CommitProjection::Record only",
674
- ));
675
- }
676
- let limit = request.limit.unwrap_or(SCAN_PAGE_LIMIT);
677
- if limit == 0 {
678
- return Ok(CommitScanBatch {
679
- entries: Vec::new(),
680
- next_start_after: request.start_after.map(str::to_string),
681
- });
682
- }
683
- let page = store
684
- .changelog_scan(
685
- COMMIT_SPACE,
686
- Vec::new(),
687
- request.start_after.map(commit_key),
688
- limit,
689
- StorageCoreProjection::FullValue,
690
- )
691
- .await?;
692
- let mut entries = Vec::with_capacity(page.values.len());
693
- for (key, value) in page.keys.iter().zip(page.values.iter()) {
694
- let record = decode_commit_record(value)?;
695
- if key.as_slice() != commit_key(&record.commit_id).as_slice() {
696
- return Err(LixError::new(
697
- LixError::CODE_INTERNAL_ERROR,
698
- format!(
699
- "changelog commit scan key does not match decoded commit_id '{}'",
700
- record.commit_id
701
- ),
702
- ));
703
- }
704
- entries.push(CommitLoadEntry::Record(record));
705
- }
706
- let next_start_after = page
707
- .resume_after
708
- .map(|key| {
709
- String::from_utf8(key).map_err(|error| {
710
- LixError::new(
711
- LixError::CODE_INTERNAL_ERROR,
712
- format!("changelog commit scan resume key is not UTF-8: {error}"),
713
- )
714
- })
715
- })
716
- .transpose()?;
717
- Ok(CommitScanBatch {
718
- entries,
719
- next_start_after,
720
- })
721
- }
722
-
723
- async fn load_changes_from_store(
724
- store: &mut (impl ChangelogStorageRead + ?Sized),
725
- request: ChangeLoadRequest<'_>,
726
- ) -> Result<ChangeLoadBatch, LixError> {
727
- let keys = request
728
- .change_ids
729
- .iter()
730
- .map(|change_id| change_key(change_id))
731
- .collect::<Vec<_>>();
732
- let entries = get_many(store, CHANGE_SPACE, keys)
733
- .await?
734
- .into_iter()
735
- .map(|value| value.as_deref().map(decode_change_record).transpose())
736
- .collect::<Result<Vec<_>, LixError>>()?;
737
- Ok(ChangeLoadBatch { entries })
738
- }
739
-
740
- async fn scan_changes_from_store(
741
- store: &mut (impl ChangelogStorageRead + ?Sized),
742
- request: ChangeScanRequest<'_>,
743
- ) -> Result<ChangeScanBatch, LixError> {
744
- let limit = request.limit.unwrap_or(SCAN_PAGE_LIMIT);
745
- if limit == 0 {
746
- return Ok(ChangeScanBatch {
747
- entries: Vec::new(),
748
- next_start_after: request.start_after.map(str::to_string),
749
- });
750
- }
751
- let page = store
752
- .changelog_scan(
753
- CHANGE_SPACE,
754
- Vec::new(),
755
- request.start_after.map(change_key),
756
- limit,
757
- StorageCoreProjection::FullValue,
758
- )
759
- .await?;
760
- let mut entries = Vec::with_capacity(page.values.len());
761
- for (key, value) in page.keys.iter().zip(page.values.iter()) {
762
- let record = decode_change_record(value)?;
763
- if key.as_slice() != change_key(&record.change_id).as_slice() {
764
- return Err(LixError::new(
765
- LixError::CODE_INTERNAL_ERROR,
766
- format!(
767
- "changelog change scan key does not match decoded change_id '{}'",
768
- record.change_id
769
- ),
770
- ));
771
- }
772
- entries.push(record);
773
- }
774
- let next_start_after = page
775
- .resume_after
776
- .map(|key| {
777
- String::from_utf8(key).map_err(|error| {
778
- LixError::new(
779
- LixError::CODE_INTERNAL_ERROR,
780
- format!("changelog change scan resume key is not UTF-8: {error}"),
781
- )
782
- })
783
- })
784
- .transpose()?;
785
- Ok(ChangeScanBatch {
786
- entries,
787
- next_start_after,
788
- })
789
- }
790
-
791
- async fn load_commit_change_ref_chunks(
792
- store: &mut (impl ChangelogStorageRead + ?Sized),
793
- commit_id: &str,
794
- ) -> Result<Vec<CommitChangeRefChunk>, LixError> {
795
- let prefix = commit_change_ref_chunk_prefix(commit_id);
796
- let mut after = None;
797
- let mut chunks = Vec::new();
798
- loop {
799
- let page = store
800
- .changelog_scan(
801
- COMMIT_CHANGE_REF_CHUNK_SPACE,
802
- prefix.clone(),
803
- after,
804
- SCAN_PAGE_LIMIT,
805
- StorageCoreProjection::FullValue,
806
- )
807
- .await?;
808
- for value in page.values {
809
- chunks.push(decode_commit_change_ref_chunk(&value, commit_id)?);
810
- }
811
- let Some(resume_after) = page.resume_after else {
812
- break;
813
- };
814
- after = Some(resume_after);
815
- }
816
- Ok(chunks)
817
- }
818
-
819
- fn project_commit_entry(
820
- projection: CommitProjection,
821
- record: CommitRecord,
822
- change_ref_chunks: Vec<CommitChangeRefChunk>,
823
- ) -> CommitLoadEntry {
824
- match projection {
825
- CommitProjection::Record => CommitLoadEntry::Record(record),
826
- CommitProjection::ChangeRefs => CommitLoadEntry::ChangeRefs(change_ref_chunks),
827
- CommitProjection::Full => CommitLoadEntry::Full {
828
- record,
829
- change_ref_chunks,
830
- },
831
- }
832
- }
833
-
834
- fn commit_entry_id(entry: &CommitLoadEntry) -> Option<&str> {
835
- match entry {
836
- CommitLoadEntry::Record(record) => Some(&record.commit_id),
837
- CommitLoadEntry::Full { record, .. } => Some(&record.commit_id),
838
- CommitLoadEntry::ChangeRefs(chunks) => chunks.first().map(|chunk| chunk.commit_id.as_str()),
839
- }
840
- }
841
-
842
- fn chunk_commit_change_refs(
843
- refs: Vec<CommitChangeRefSet>,
844
- ) -> Result<HashMap<String, Vec<CommitChangeRefChunk>>, LixError> {
845
- refs.into_iter()
846
- .map(|refs| {
847
- let commit_id = refs.commit_id.clone();
848
- Ok((
849
- commit_id,
850
- chunk_one_commit_change_refs(
851
- refs,
852
- COMMIT_CHANGE_REF_CHUNK_TARGET_BYTES,
853
- COMMIT_CHANGE_REF_CHUNK_MAX_BYTES,
854
- COMMIT_CHANGE_REF_CHUNK_MAX_ENTRIES,
855
- )?,
856
- ))
857
- })
858
- .collect()
859
- }
860
-
861
- fn chunk_one_commit_change_refs(
862
- mut refs: CommitChangeRefSet,
863
- target_bytes: usize,
864
- max_bytes: usize,
865
- max_entries: usize,
866
- ) -> Result<Vec<CommitChangeRefChunk>, LixError> {
867
- refs.entries.sort_by(|left, right| {
868
- (
869
- left.schema_key.as_str(),
870
- left.file_id.as_deref(),
871
- &left.entity_pk,
872
- left.change_id.as_str(),
873
- )
874
- .cmp(&(
875
- right.schema_key.as_str(),
876
- right.file_id.as_deref(),
877
- &right.entity_pk,
878
- right.change_id.as_str(),
879
- ))
880
- });
881
-
882
- let mut chunks = Vec::new();
883
- let mut builder = CommitChangeRefChunkBuilder::new(refs.commit_id.clone());
884
- for entry in refs.entries {
885
- let candidate_size = builder.estimated_size_after(&entry);
886
- if !builder.is_empty()
887
- && (builder.len() >= max_entries
888
- || builder.estimated_size() >= target_bytes
889
- || candidate_size > max_bytes)
890
- {
891
- chunks.push(builder.finish()?);
892
- builder = CommitChangeRefChunkBuilder::new(refs.commit_id.clone());
893
- }
894
-
895
- builder.push(entry);
896
- validate_commit_change_ref_chunk_size(&builder, max_bytes)?;
897
- }
898
-
899
- if !builder.is_empty() {
900
- chunks.push(builder.finish()?);
901
- }
902
- Ok(chunks)
903
- }
904
-
905
- fn commit_change_ref_chunk(commit_id: &str, entries: Vec<CommitChangeRef>) -> CommitChangeRefChunk {
906
- CommitChangeRefChunk {
907
- format_version: COMMIT_CHANGE_REF_CHUNK_FORMAT_VERSION,
908
- commit_id: commit_id.to_string(),
909
- entries,
910
- }
911
- }
912
-
913
- fn validate_commit_change_ref_chunk_size(
914
- builder: &CommitChangeRefChunkBuilder,
915
- max_bytes: usize,
916
- ) -> Result<(), LixError> {
917
- let size = builder.estimated_size();
918
- if size > max_bytes {
919
- return Err(LixError::new(
920
- LixError::CODE_INTERNAL_ERROR,
921
- format!(
922
- "single changelog commit_change_ref_chunk entry for commit '{}' exceeds {max_bytes} bytes",
923
- builder.commit_id
924
- ),
925
- ));
926
- }
927
- Ok(())
928
- }
929
-
930
- struct CommitChangeRefChunkBuilder {
931
- commit_id: String,
932
- entries: Vec<CommitChangeRef>,
933
- schema_keys: HashSet<String>,
934
- file_ids: HashSet<Option<String>>,
935
- estimated_size: usize,
936
- }
937
-
938
- impl CommitChangeRefChunkBuilder {
939
- fn new(commit_id: String) -> Self {
940
- Self {
941
- commit_id,
942
- entries: Vec::new(),
943
- schema_keys: HashSet::new(),
944
- file_ids: HashSet::new(),
945
- estimated_size: commit_change_ref_chunk_fixed_size(),
946
- }
947
- }
948
-
949
- fn is_empty(&self) -> bool {
950
- self.entries.is_empty()
951
- }
952
-
953
- fn len(&self) -> usize {
954
- self.entries.len()
955
- }
956
-
957
- fn estimated_size(&self) -> usize {
958
- self.estimated_size
959
- }
960
-
961
- fn estimated_size_after(&self, entry: &CommitChangeRef) -> usize {
962
- self.estimated_size + self.incremental_size(entry)
963
- }
964
-
965
- fn push(&mut self, entry: CommitChangeRef) {
966
- self.estimated_size += self.incremental_size(&entry);
967
- self.schema_keys.insert(entry.schema_key.clone());
968
- self.file_ids.insert(entry.file_id.clone());
969
- self.entries.push(entry);
970
- }
971
-
972
- fn incremental_size(&self, entry: &CommitChangeRef) -> usize {
973
- let schema_dictionary_bytes = if self.schema_keys.contains(&entry.schema_key) {
974
- 0
975
- } else {
976
- encoded_str_size(&entry.schema_key)
977
- };
978
- let file_dictionary_bytes = if self.file_ids.contains(&entry.file_id) {
979
- 0
980
- } else {
981
- encoded_optional_str_size(entry.file_id.as_deref())
982
- };
983
- schema_dictionary_bytes
984
- + file_dictionary_bytes
985
- + encoded_commit_change_ref_entry_size(entry)
986
- }
987
-
988
- fn finish(self) -> Result<CommitChangeRefChunk, LixError> {
989
- let chunk = commit_change_ref_chunk(&self.commit_id, self.entries);
990
- debug_assert_eq!(
991
- self.estimated_size,
992
- encode_commit_change_ref_chunk(&chunk)?.len()
993
- );
994
- Ok(chunk)
995
- }
996
- }
997
-
998
- fn commit_change_ref_chunk_fixed_size() -> usize {
999
- 5 // magic
1000
- + 4 // format_version
1001
- + 4 // schema dictionary length
1002
- + 4 // file dictionary length
1003
- + 4 // entry count
1004
- }
1005
-
1006
- fn encoded_commit_change_ref_entry_size(entry: &CommitChangeRef) -> usize {
1007
- 2 // schema index
1008
- + 2 // file index
1009
- + encoded_entity_pk_compact_size(&entry.entity_pk)
1010
- + encoded_str_size(&entry.change_id)
1011
- }
1012
-
1013
- fn encoded_entity_pk_compact_size(identity: &crate::entity_pk::EntityPk) -> usize {
1014
- if identity.parts.len() == 1 {
1015
- 1 + encoded_str_size(&identity.parts[0])
1016
- } else {
1017
- 1 + 4
1018
- + identity
1019
- .parts
1020
- .iter()
1021
- .map(|part| encoded_str_size(part))
1022
- .sum::<usize>()
1023
- }
1024
- }
1025
-
1026
- fn encoded_optional_str_size(value: Option<&str>) -> usize {
1027
- 1 + value.map(encoded_str_size).unwrap_or(0)
1028
- }
1029
-
1030
- fn encoded_str_size(value: &str) -> usize {
1031
- 4 + value.len()
1032
- }
1033
-
1034
- fn validate_unique<'a>(
1035
- values: impl IntoIterator<Item = &'a str>,
1036
- label: &str,
1037
- ) -> Result<(), LixError> {
1038
- let mut seen = HashSet::new();
1039
- for value in values {
1040
- if !seen.insert(value) {
1041
- return Err(LixError::unknown(format!(
1042
- "changelog append contains duplicate {label} '{value}'"
1043
- )));
1044
- }
1045
- }
1046
- Ok(())
1047
- }
1048
-
1049
- fn validate_unique_ref_keys(entries: &[CommitChangeRef], commit_id: &str) -> Result<(), LixError> {
1050
- let mut seen = HashSet::new();
1051
- for entry in entries {
1052
- let key = (
1053
- entry.schema_key.as_str(),
1054
- entry.file_id.as_deref(),
1055
- &entry.entity_pk,
1056
- );
1057
- if !seen.insert(key) {
1058
- return Err(LixError::unknown(format!(
1059
- "changelog commit '{commit_id}' has duplicate change ref key"
1060
- )));
1061
- }
1062
- }
1063
- Ok(())
1064
- }
1065
-
1066
- fn validate_ref_matches_change(
1067
- commit_id: &str,
1068
- entry: &CommitChangeRef,
1069
- change: &ChangeRecord,
1070
- ) -> Result<(), LixError> {
1071
- if entry.schema_key != change.schema_key
1072
- || entry.file_id != change.file_id
1073
- || entry.entity_pk != change.entity_pk
1074
- {
1075
- return Err(LixError::unknown(format!(
1076
- "changelog commit '{}' change ref '{}' does not match referenced ChangeRecord key",
1077
- commit_id, entry.change_id
1078
- )));
1079
- }
1080
- Ok(())
1081
- }
1082
-
1083
- fn empty_gc_plan(roots: &[GcRoot]) -> GcPlan {
1084
- GcPlan {
1085
- roots: roots.to_vec(),
1086
- ..GcPlan::default()
1087
- }
1088
- }
1089
-
1090
- async fn get_one(
1091
- store: &mut (impl ChangelogStorageRead + ?Sized),
1092
- space: StorageSpace,
1093
- key: Vec<u8>,
1094
- ) -> Result<Option<Vec<u8>>, LixError> {
1095
- Ok(get_many(store, space, vec![key])
1096
- .await?
1097
- .into_iter()
1098
- .next()
1099
- .flatten())
1100
- }
1101
-
1102
- async fn get_many(
1103
- store: &mut (impl ChangelogStorageRead + ?Sized),
1104
- space: StorageSpace,
1105
- keys: Vec<Vec<u8>>,
1106
- ) -> Result<Vec<Option<Vec<u8>>>, LixError> {
1107
- if keys.is_empty() {
1108
- return Ok(Vec::new());
1109
- }
1110
- store.changelog_get_many(space, keys).await
1111
- }
1112
-
1113
- fn native_get_many<R>(
1114
- read: &mut R,
1115
- space: StorageSpace,
1116
- keys: Vec<Vec<u8>>,
1117
- ) -> Result<Vec<Option<Vec<u8>>>, LixError>
1118
- where
1119
- R: StorageRead + ?Sized,
1120
- {
1121
- let keys = keys
1122
- .into_iter()
1123
- .map(|key| StorageKey(Bytes::from(key)))
1124
- .collect::<Vec<_>>();
1125
- let result =
1126
- PointReadPlan::new(space, &keys).materialize(read, StorageGetOptions::default())?;
1127
- Ok(result
1128
- .value
1129
- .into_iter()
1130
- .map(|value| match value {
1131
- Some(StorageProjectedValue::FullValue(bytes)) => Some(bytes.to_vec()),
1132
- Some(StorageProjectedValue::KeyOnly) => Some(Vec::new()),
1133
- None => None,
1134
- })
1135
- .collect())
1136
- }
1137
-
1138
- fn native_scan<R>(
1139
- read: &mut R,
1140
- space: StorageSpace,
1141
- prefix: Vec<u8>,
1142
- after: Option<Vec<u8>>,
1143
- limit: usize,
1144
- projection: StorageCoreProjection,
1145
- ) -> Result<ChangelogScanPage, LixError>
1146
- where
1147
- R: StorageRead + ?Sized,
1148
- {
1149
- let after_key = after.map(|key| StorageKey(Bytes::from(key)));
1150
- let opts = StorageScanOptions {
1151
- projection,
1152
- limit_rows: limit,
1153
- resume_after: after_key.as_ref(),
1154
- };
1155
- let chunk = ScanPlan::prefix(
1156
- space,
1157
- StoragePrefix {
1158
- bytes: Bytes::from(prefix),
1159
- },
1160
- )
1161
- .collect(read, opts)?
1162
- .value;
1163
- let has_more = chunk.has_more;
1164
- let mut keys = Vec::with_capacity(chunk.entries.len());
1165
- let mut values = Vec::with_capacity(chunk.entries.len());
1166
- for entry in chunk.entries {
1167
- keys.push(entry.key.0.to_vec());
1168
- if let StorageProjectedValue::FullValue(bytes) = entry.value {
1169
- values.push(bytes.to_vec());
1170
- }
1171
- }
1172
- let resume_after = has_more.then(|| keys.last().cloned()).flatten();
1173
- Ok(ChangelogScanPage {
1174
- keys,
1175
- values,
1176
- resume_after,
1177
- })
1178
- }
1179
-
1180
- #[cfg(test)]
1181
- mod tests {
1182
- use crate::changelog::test_support::{changelog_test_context, test_append};
1183
- use crate::changelog::{
1184
- ChangeLoadRequest, ChangeRecord, ChangeScanRequest, ChangelogAppend, ChangelogReader,
1185
- ChangelogWriter, CommitLoadEntry, CommitLoadRequest, CommitProjection, CommitScanRequest,
1186
- };
1187
- use crate::entity_pk::EntityPk;
1188
-
1189
- use super::*;
1190
-
1191
- fn test_change_ref(entity: &str, change_id: &str) -> CommitChangeRef {
1192
- CommitChangeRef {
1193
- schema_key: "message".to_string(),
1194
- file_id: None,
1195
- entity_pk: EntityPk::single(entity.to_string()),
1196
- change_id: change_id.to_string(),
1197
- }
1198
- }
1199
-
1200
- #[test]
1201
- fn chunk_one_commit_change_refs_splits_by_encoded_size() {
1202
- let refs = CommitChangeRefSet {
1203
- commit_id: "commit-1".to_string(),
1204
- entries: (0..8)
1205
- .map(|index| {
1206
- test_change_ref(
1207
- &format!("entity-{index:04}-{}", "x".repeat(24)),
1208
- &format!("change-{index:04}-{}", "y".repeat(24)),
1209
- )
1210
- })
1211
- .collect(),
1212
- };
1213
-
1214
- let chunks = chunk_one_commit_change_refs(refs, 180, 260, 2048)
1215
- .expect("refs should chunk under small test limit");
1216
-
1217
- assert!(chunks.len() > 1);
1218
- assert!(chunks
1219
- .iter()
1220
- .all(|chunk| encode_commit_change_ref_chunk(chunk).unwrap().len() <= 260));
1221
- assert_eq!(
1222
- chunks
1223
- .iter()
1224
- .flat_map(|chunk| chunk.entries.iter())
1225
- .map(|entry| entry.change_id.as_str())
1226
- .collect::<Vec<_>>(),
1227
- vec![
1228
- "change-0000-yyyyyyyyyyyyyyyyyyyyyyyy",
1229
- "change-0001-yyyyyyyyyyyyyyyyyyyyyyyy",
1230
- "change-0002-yyyyyyyyyyyyyyyyyyyyyyyy",
1231
- "change-0003-yyyyyyyyyyyyyyyyyyyyyyyy",
1232
- "change-0004-yyyyyyyyyyyyyyyyyyyyyyyy",
1233
- "change-0005-yyyyyyyyyyyyyyyyyyyyyyyy",
1234
- "change-0006-yyyyyyyyyyyyyyyyyyyyyyyy",
1235
- "change-0007-yyyyyyyyyyyyyyyyyyyyyyyy",
1236
- ]
1237
- );
1238
- }
1239
-
1240
- #[test]
1241
- fn chunk_one_commit_change_refs_splits_by_entry_count() {
1242
- let refs = CommitChangeRefSet {
1243
- commit_id: "commit-1".to_string(),
1244
- entries: (0..5)
1245
- .map(|index| {
1246
- test_change_ref(&format!("entity-{index}"), &format!("change-{index}"))
1247
- })
1248
- .collect(),
1249
- };
1250
-
1251
- let chunks = chunk_one_commit_change_refs(refs, usize::MAX, usize::MAX, 2)
1252
- .expect("refs should chunk by entry cap");
1253
-
1254
- assert_eq!(
1255
- chunks
1256
- .iter()
1257
- .map(|chunk| chunk.entries.len())
1258
- .collect::<Vec<_>>(),
1259
- vec![2, 2, 1]
1260
- );
1261
- }
1262
-
1263
- #[tokio::test]
1264
- async fn stage_append_writes_direct_records_and_change_ref_chunks() {
1265
- let (context, storage) = changelog_test_context();
1266
- let append = test_append();
1267
-
1268
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1269
- let mut writes = StorageWriteSet::new();
1270
- {
1271
- let mut writer = context.writer(&mut *transaction, &mut writes);
1272
- writer.stage_append(append).await.unwrap();
1273
- }
1274
- let stats = writes.apply(&mut *transaction).await.unwrap();
1275
- assert_eq!(stats.staged_puts, 3);
1276
- transaction.commit().await.unwrap();
1277
-
1278
- let mut read = storage.begin_read_transaction().await.unwrap();
1279
- let mut reader = context.reader(&mut *read);
1280
- let commits = reader
1281
- .load_commits(CommitLoadRequest {
1282
- commit_ids: &["commit-1".to_string()],
1283
- projection: CommitProjection::Full,
1284
- })
1285
- .await
1286
- .unwrap();
1287
- let Some(CommitLoadEntry::Full {
1288
- record,
1289
- change_ref_chunks,
1290
- }) = commits.entries.into_iter().next().flatten()
1291
- else {
1292
- panic!("expected full commit entry");
1293
- };
1294
- assert_eq!(record.commit_id, "commit-1");
1295
- assert_eq!(record.change_id, "commit-row-change-1");
1296
- assert_eq!(change_ref_chunks.len(), 1);
1297
- assert_eq!(
1298
- change_ref_chunks[0]
1299
- .entries
1300
- .iter()
1301
- .map(|entry| entry.change_id.as_str())
1302
- .collect::<Vec<_>>(),
1303
- vec!["change-1"]
1304
- );
1305
-
1306
- let changes = reader
1307
- .load_changes(ChangeLoadRequest {
1308
- change_ids: &["change-1".to_string(), "missing".to_string()],
1309
- })
1310
- .await
1311
- .unwrap();
1312
- assert_eq!(changes.entries[0].as_ref().unwrap().schema_key, "message");
1313
- assert!(changes.entries[1].is_none());
1314
- }
1315
-
1316
- #[tokio::test]
1317
- async fn stage_append_rejects_ref_key_mismatch() {
1318
- let (context, storage) = changelog_test_context();
1319
- let mut append = test_append();
1320
- append.commit_change_refs[0].entries[0].schema_key = "wrong".to_string();
1321
-
1322
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1323
- let mut writes = StorageWriteSet::new();
1324
- let error = {
1325
- let mut writer = context.writer(&mut *transaction, &mut writes);
1326
- writer.stage_append(append).await.unwrap_err()
1327
- };
1328
- assert!(
1329
- error
1330
- .message
1331
- .contains("does not match referenced ChangeRecord key"),
1332
- "{error:?}"
1333
- );
1334
- }
1335
-
1336
- #[tokio::test]
1337
- async fn stage_append_rejects_commit_missing_change_refs() {
1338
- let (context, storage) = changelog_test_context();
1339
- let mut append = test_append();
1340
- append.commit_change_refs.clear();
1341
-
1342
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1343
- let mut writes = StorageWriteSet::new();
1344
- let error = {
1345
- let mut writer = context.writer(&mut *transaction, &mut writes);
1346
- writer.stage_append(append).await.unwrap_err()
1347
- };
1348
- assert!(
1349
- error.message.contains("is missing commit change refs"),
1350
- "{error:?}"
1351
- );
1352
- }
1353
-
1354
- #[tokio::test]
1355
- async fn stage_append_rejects_commit_change_id_colliding_with_change_record() {
1356
- let (context, storage) = changelog_test_context();
1357
- let mut append = test_append();
1358
- append.changes[0].change_id = append.commits[0].change_id.clone();
1359
- append.commit_change_refs[0].entries[0].change_id = append.commits[0].change_id.clone();
1360
-
1361
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1362
- let mut writes = StorageWriteSet::new();
1363
- let error = {
1364
- let mut writer = context.writer(&mut *transaction, &mut writes);
1365
- writer.stage_append(append).await.unwrap_err()
1366
- };
1367
- assert!(
1368
- error
1369
- .message
1370
- .contains("collides with an existing change id"),
1371
- "{error:?}"
1372
- );
1373
- }
1374
-
1375
- #[tokio::test]
1376
- async fn stage_append_sorts_commit_change_refs_by_canonical_key() {
1377
- let (context, storage) = changelog_test_context();
1378
- let mut append = test_append();
1379
- append.changes.push(ChangeRecord {
1380
- format_version: 1,
1381
- change_id: "change-0".to_string(),
1382
- schema_key: "alpha".to_string(),
1383
- entity_pk: EntityPk::single("entity-0"),
1384
- file_id: None,
1385
- snapshot_ref: None,
1386
- metadata_ref: None,
1387
- created_at: "2026-05-12T00:00:00Z".to_string(),
1388
- });
1389
- append.commit_change_refs[0].entries.insert(
1390
- 0,
1391
- crate::changelog::CommitChangeRef {
1392
- schema_key: "alpha".to_string(),
1393
- file_id: None,
1394
- entity_pk: EntityPk::single("entity-0"),
1395
- change_id: "change-0".to_string(),
1396
- },
1397
- );
1398
- append.commit_change_refs[0].entries.swap(0, 1);
1399
-
1400
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1401
- let mut writes = StorageWriteSet::new();
1402
- {
1403
- let mut writer = context.writer(&mut *transaction, &mut writes);
1404
- writer.stage_append(append).await.unwrap();
1405
- }
1406
- writes.apply(&mut *transaction).await.unwrap();
1407
- transaction.commit().await.unwrap();
1408
-
1409
- let mut read = storage.begin_read_transaction().await.unwrap();
1410
- let mut reader = context.reader(&mut *read);
1411
- let commits = reader
1412
- .load_commits(CommitLoadRequest {
1413
- commit_ids: &["commit-1".to_string()],
1414
- projection: CommitProjection::Full,
1415
- })
1416
- .await
1417
- .unwrap();
1418
- let Some(CommitLoadEntry::Full {
1419
- change_ref_chunks, ..
1420
- }) = commits.entries.into_iter().next().flatten()
1421
- else {
1422
- panic!("expected full commit entry");
1423
- };
1424
- assert_eq!(
1425
- change_ref_chunks[0]
1426
- .entries
1427
- .iter()
1428
- .map(|entry| entry.change_id.as_str())
1429
- .collect::<Vec<_>>(),
1430
- vec!["change-0", "change-1"]
1431
- );
1432
- }
1433
-
1434
- #[tokio::test]
1435
- async fn scan_commits_reads_direct_commit_records_in_key_order() {
1436
- let (context, storage) = changelog_test_context();
1437
- let mut first = test_append();
1438
- first.commits[0].commit_id = "commit-b".to_string();
1439
- first.commits[0].change_id = "commit-b-row-change".to_string();
1440
- first.commit_change_refs[0].commit_id = "commit-b".to_string();
1441
-
1442
- let mut second = test_append();
1443
- second.commits[0].commit_id = "commit-a".to_string();
1444
- second.commits[0].change_id = "commit-a-row-change".to_string();
1445
- second.changes[0].change_id = "change-a".to_string();
1446
- second.commit_change_refs[0].commit_id = "commit-a".to_string();
1447
- second.commit_change_refs[0].entries[0].change_id = "change-a".to_string();
1448
-
1449
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1450
- let mut writes = StorageWriteSet::new();
1451
- {
1452
- let mut writer = context.writer(&mut *transaction, &mut writes);
1453
- writer.stage_append(first).await.unwrap();
1454
- writer.stage_append(second).await.unwrap();
1455
- }
1456
- writes.apply(&mut *transaction).await.unwrap();
1457
- transaction.commit().await.unwrap();
1458
-
1459
- let mut read = storage.begin_read_transaction().await.unwrap();
1460
- let mut reader = context.reader(&mut *read);
1461
- let scan = reader
1462
- .scan_commits(CommitScanRequest {
1463
- start_after: None,
1464
- limit: Some(1),
1465
- projection: CommitProjection::Record,
1466
- })
1467
- .await
1468
- .unwrap();
1469
- assert_eq!(scan.entries.len(), 1);
1470
- assert_eq!(scan.next_start_after.as_deref(), Some("commit-a"));
1471
- let CommitLoadEntry::Record(record) = &scan.entries[0] else {
1472
- panic!("expected record projection");
1473
- };
1474
- assert_eq!(record.commit_id, "commit-a");
1475
-
1476
- let next = reader
1477
- .scan_commits(CommitScanRequest {
1478
- start_after: scan.next_start_after.as_deref(),
1479
- limit: Some(10),
1480
- projection: CommitProjection::Record,
1481
- })
1482
- .await
1483
- .unwrap();
1484
- let ids = next
1485
- .entries
1486
- .iter()
1487
- .map(|entry| {
1488
- let CommitLoadEntry::Record(record) = entry else {
1489
- panic!("expected record projection");
1490
- };
1491
- record.commit_id.as_str()
1492
- })
1493
- .collect::<Vec<_>>();
1494
- assert_eq!(ids, vec!["commit-b"]);
1495
- assert_eq!(next.next_start_after, None);
1496
- }
1497
-
1498
- #[tokio::test]
1499
- async fn scan_changes_reads_direct_change_records_in_key_order() {
1500
- let (context, storage) = changelog_test_context();
1501
- let mut first = test_append();
1502
- first.commits[0].commit_id = "commit-b".to_string();
1503
- first.commits[0].change_id = "commit-b-row-change".to_string();
1504
- first.changes[0].change_id = "change-b".to_string();
1505
- first.commit_change_refs[0].commit_id = "commit-b".to_string();
1506
- first.commit_change_refs[0].entries[0].change_id = "change-b".to_string();
1507
-
1508
- let mut second = test_append();
1509
- second.commits[0].commit_id = "commit-a".to_string();
1510
- second.commits[0].change_id = "commit-a-row-change".to_string();
1511
- second.changes[0].change_id = "change-a".to_string();
1512
- second.commit_change_refs[0].commit_id = "commit-a".to_string();
1513
- second.commit_change_refs[0].entries[0].change_id = "change-a".to_string();
1514
-
1515
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1516
- let mut writes = StorageWriteSet::new();
1517
- {
1518
- let mut writer = context.writer(&mut *transaction, &mut writes);
1519
- writer.stage_append(first).await.unwrap();
1520
- writer.stage_append(second).await.unwrap();
1521
- }
1522
- writes.apply(&mut *transaction).await.unwrap();
1523
- transaction.commit().await.unwrap();
1524
-
1525
- let mut read = storage.begin_read_transaction().await.unwrap();
1526
- let mut reader = context.reader(&mut *read);
1527
- let scan = reader
1528
- .scan_changes(ChangeScanRequest {
1529
- start_after: None,
1530
- limit: Some(1),
1531
- })
1532
- .await
1533
- .unwrap();
1534
- assert_eq!(scan.entries.len(), 1);
1535
- assert_eq!(scan.entries[0].change_id, "change-a");
1536
- assert_eq!(scan.next_start_after.as_deref(), Some("change-a"));
1537
-
1538
- let next = reader
1539
- .scan_changes(ChangeScanRequest {
1540
- start_after: scan.next_start_after.as_deref(),
1541
- limit: Some(10),
1542
- })
1543
- .await
1544
- .unwrap();
1545
- let ids = next
1546
- .entries
1547
- .iter()
1548
- .map(|change| change.change_id.as_str())
1549
- .collect::<Vec<_>>();
1550
- assert_eq!(ids, vec!["change-b"]);
1551
- assert_eq!(next.next_start_after, None);
1552
- }
1553
-
1554
- #[tokio::test]
1555
- async fn scan_changes_pages_all_direct_change_records_without_gaps() {
1556
- let (context, storage) = changelog_test_context();
1557
- let changes = (0..2_500)
1558
- .map(|index| ChangeRecord {
1559
- format_version: 1,
1560
- change_id: format!("change-{index:04}"),
1561
- schema_key: "message".to_string(),
1562
- entity_pk: EntityPk::single(format!("entity-{index:04}")),
1563
- file_id: None,
1564
- snapshot_ref: None,
1565
- metadata_ref: None,
1566
- created_at: "2026-05-20T00:00:00Z".to_string(),
1567
- })
1568
- .collect::<Vec<_>>();
1569
- let expected_ids = changes
1570
- .iter()
1571
- .map(|change| change.change_id.clone())
1572
- .collect::<Vec<_>>();
1573
-
1574
- let mut transaction = storage.begin_write_transaction().await.unwrap();
1575
- let mut writes = StorageWriteSet::new();
1576
- {
1577
- let mut writer = context.writer(&mut *transaction, &mut writes);
1578
- writer
1579
- .stage_append(ChangelogAppend {
1580
- commits: Vec::new(),
1581
- changes,
1582
- commit_change_refs: Vec::new(),
1583
- })
1584
- .await
1585
- .unwrap();
1586
- }
1587
- writes.apply(&mut *transaction).await.unwrap();
1588
- transaction.commit().await.unwrap();
1589
-
1590
- let mut read = storage.begin_read_transaction().await.unwrap();
1591
- let mut reader = context.reader(&mut *read);
1592
- let mut start_after = None::<String>;
1593
- let mut scanned_ids = Vec::new();
1594
- let mut page_sizes = Vec::new();
1595
- loop {
1596
- let page = reader
1597
- .scan_changes(ChangeScanRequest {
1598
- start_after: start_after.as_deref(),
1599
- limit: Some(1_024),
1600
- })
1601
- .await
1602
- .unwrap();
1603
- page_sizes.push(page.entries.len());
1604
- scanned_ids.extend(page.entries.into_iter().map(|change| change.change_id));
1605
- let Some(next_start_after) = page.next_start_after else {
1606
- break;
1607
- };
1608
- start_after = Some(next_start_after);
1609
- }
1610
-
1611
- assert_eq!(page_sizes, vec![1_024, 1_024, 452]);
1612
- assert_eq!(scanned_ids, expected_ids);
1613
- }
1614
- }