@lix-js/sdk 0.6.0-preview.3 → 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 (235) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +105 -65
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +30 -6
  5. package/dist/engine-wasm/wasm/lix_engine.js +187 -117
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +14 -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 +42 -28
  11. package/dist/open-lix.js +49 -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 +819 -124
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +260 -57
  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 +19 -16
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +557 -0
  104. package/dist-engine-src/src/sql2/bind/classify.rs +102 -0
  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} +98 -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 +4 -5
  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 +30 -24
  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 -109
  218. package/dist-engine-src/src/sql2/classify.rs +0 -182
  219. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  220. package/dist-engine-src/src/sql2/execute.rs +0 -3440
  221. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  222. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  223. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -166
  224. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -25
  225. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  226. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  227. package/dist-engine-src/src/storage/types.rs +0 -501
  228. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  229. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  230. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  231. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  232. package/dist-engine-src/src/version/mod.rs +0 -13
  233. package/dist-engine-src/src/version/refs.rs +0 -330
  234. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  235. package/dist-engine-src/src/version/types.rs +0 -21
@@ -0,0 +1,867 @@
1
+ #[cfg(test)]
2
+ mod tests {
3
+ use std::cell::RefCell;
4
+ use std::ops::Bound;
5
+
6
+ use bytes::Bytes;
7
+
8
+ use crate::backend::{
9
+ BackendError, BackendRangeScan, BackendRead, BufferedRangeScan, CoreProjection, GetOptions,
10
+ InMemoryBackend, Key, KeyRange, KeyRef, PointVisitor, Prefix, ProjectedValue,
11
+ ProjectedValueRef, ReadOptions, ScanOptions, ScanResult, ScanVisitor, SpaceId, StoredValue,
12
+ WriteOptions,
13
+ };
14
+ use crate::storage::{
15
+ PointReadBuffer, PointReadPlan, ScanBuffer, ScanPlan, StorageContext, StorageRead,
16
+ StorageSpace,
17
+ };
18
+
19
+ fn key(bytes: &'static str) -> Key {
20
+ Key(Bytes::from_static(bytes.as_bytes()))
21
+ }
22
+
23
+ fn key_bytes(bytes: &'static [u8]) -> Key {
24
+ Key(Bytes::from_static(bytes))
25
+ }
26
+
27
+ fn value(bytes: &'static str) -> StoredValue {
28
+ StoredValue {
29
+ bytes: Bytes::from_static(bytes.as_bytes()),
30
+ }
31
+ }
32
+
33
+ fn space(id: u32) -> StorageSpace {
34
+ match id {
35
+ 1 => StorageSpace::new(SpaceId(1), "test.space.one"),
36
+ _ => StorageSpace::new(SpaceId(id), "test.space.other"),
37
+ }
38
+ }
39
+
40
+ #[derive(Default)]
41
+ struct SpyRead {
42
+ get_many_keys: RefCell<Vec<Key>>,
43
+ scan_range: RefCell<Option<KeyRange>>,
44
+ scan_range_calls: RefCell<u64>,
45
+ }
46
+
47
+ impl BackendRead for SpyRead {
48
+ type RangeScan<'a> = BufferedRangeScan;
49
+
50
+ fn visit_keys<V>(
51
+ &self,
52
+ keys: &[Key],
53
+ opts: GetOptions<'_>,
54
+ visitor: &mut V,
55
+ ) -> Result<(), BackendError>
56
+ where
57
+ V: PointVisitor + ?Sized,
58
+ {
59
+ self.get_many_keys.replace(keys.to_vec());
60
+ for (index, key) in keys.iter().enumerate() {
61
+ let value = match opts.projection {
62
+ CoreProjection::KeyOnly => ProjectedValueRef::KeyOnly,
63
+ CoreProjection::FullValue => ProjectedValueRef::FullValue(key.0.as_ref()),
64
+ };
65
+ visitor.visit(index, key, Some(value))?;
66
+ }
67
+ Ok(())
68
+ }
69
+
70
+ fn with_range_scan<T, F>(
71
+ &self,
72
+ range: KeyRange,
73
+ _opts: ScanOptions<'_>,
74
+ f: F,
75
+ ) -> Result<T, BackendError>
76
+ where
77
+ F: FnOnce(&mut Self::RangeScan<'_>) -> Result<T, BackendError>,
78
+ {
79
+ *self.scan_range_calls.borrow_mut() += 1;
80
+ self.scan_range.replace(Some(range));
81
+ let mut cursor = BufferedRangeScan::default();
82
+ f(&mut cursor)
83
+ }
84
+ }
85
+
86
+ #[derive(Default)]
87
+ struct RequestedOrderRead {
88
+ get_many_keys: RefCell<Vec<Key>>,
89
+ }
90
+
91
+ impl BackendRead for RequestedOrderRead {
92
+ type RangeScan<'a> = BufferedRangeScan;
93
+
94
+ fn visit_keys<V>(
95
+ &self,
96
+ keys: &[Key],
97
+ _opts: GetOptions<'_>,
98
+ visitor: &mut V,
99
+ ) -> Result<(), BackendError>
100
+ where
101
+ V: PointVisitor + ?Sized,
102
+ {
103
+ self.get_many_keys.replace(keys.to_vec());
104
+ for (index, key) in keys.iter().enumerate() {
105
+ let value = (!key.0.ends_with(b"missing"))
106
+ .then_some(ProjectedValueRef::FullValue(key.0.as_ref()));
107
+ visitor.visit(index, key, value)?;
108
+ }
109
+ Ok(())
110
+ }
111
+
112
+ fn with_range_scan<T, F>(
113
+ &self,
114
+ _range: KeyRange,
115
+ _opts: ScanOptions<'_>,
116
+ _f: F,
117
+ ) -> Result<T, BackendError>
118
+ where
119
+ F: FnOnce(&mut Self::RangeScan<'_>) -> Result<T, BackendError>,
120
+ {
121
+ unreachable!("requested-order point-read test does not scan")
122
+ }
123
+ }
124
+
125
+ #[test]
126
+ fn point_reads_reconstruct_caller_order_duplicates_and_missing() {
127
+ let storage = StorageContext::new(InMemoryBackend::new());
128
+ let mut writes = storage.new_write_set();
129
+ writes.put(space(1), key("a"), value("A"));
130
+ writes.put(space(1), key("b"), value("B"));
131
+ storage
132
+ .commit_write_set(writes, WriteOptions::default())
133
+ .expect("seed");
134
+ let read = storage
135
+ .begin_read(ReadOptions::default())
136
+ .expect("begin read");
137
+
138
+ let result = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")])
139
+ .materialize(&read, GetOptions::default())
140
+ .expect("caller order");
141
+
142
+ assert_eq!(
143
+ result.value[0],
144
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B")))
145
+ );
146
+ assert_eq!(result.value[1], None);
147
+ assert_eq!(
148
+ result.value[2],
149
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"A")))
150
+ );
151
+ assert_eq!(
152
+ result.value[3],
153
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B")))
154
+ );
155
+ }
156
+
157
+ #[test]
158
+ fn point_reads_dedupe_before_backend_call() {
159
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
160
+ let result = PointReadPlan::new(
161
+ space(1),
162
+ &[key("b"), key("a"), key("b"), key("missing"), key("missing")],
163
+ )
164
+ .materialize(&read, GetOptions::default())
165
+ .expect("caller order");
166
+
167
+ assert_eq!(
168
+ read.backend_read().get_many_keys.borrow().as_slice(),
169
+ [
170
+ space(1).encode_key(&key("b")),
171
+ space(1).encode_key(&key("a")),
172
+ space(1).encode_key(&key("missing"))
173
+ ]
174
+ );
175
+ assert_eq!(
176
+ result.value,
177
+ vec![
178
+ Some(ProjectedValue::FullValue(space(1).encode_key(&key("b")).0)),
179
+ Some(ProjectedValue::FullValue(space(1).encode_key(&key("a")).0)),
180
+ Some(ProjectedValue::FullValue(space(1).encode_key(&key("b")).0)),
181
+ Some(ProjectedValue::FullValue(
182
+ space(1).encode_key(&key("missing")).0
183
+ )),
184
+ Some(ProjectedValue::FullValue(
185
+ space(1).encode_key(&key("missing")).0
186
+ )),
187
+ ]
188
+ );
189
+ }
190
+
191
+ #[test]
192
+ fn point_reads_can_return_values_without_echoing_keys() {
193
+ let storage = StorageContext::new(InMemoryBackend::new());
194
+ let mut writes = storage.new_write_set();
195
+ writes.put(space(1), key("a"), value("A"));
196
+ writes.put(space(1), key("b"), value("B"));
197
+ storage
198
+ .commit_write_set(writes, WriteOptions::default())
199
+ .expect("seed");
200
+ let read = storage
201
+ .begin_read(ReadOptions::default())
202
+ .expect("begin read");
203
+
204
+ let values = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")])
205
+ .materialize(&read, GetOptions::default())
206
+ .expect("caller order values");
207
+
208
+ assert_eq!(
209
+ values.value,
210
+ vec![
211
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B"))),
212
+ None,
213
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"A"))),
214
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B"))),
215
+ ]
216
+ );
217
+ }
218
+
219
+ #[test]
220
+ fn point_reads_can_return_indexed_values_without_duplicate_value_clones() {
221
+ let storage = StorageContext::new(InMemoryBackend::new());
222
+ let mut writes = storage.new_write_set();
223
+ writes.put(space(1), key("a"), value("A"));
224
+ writes.put(space(1), key("b"), value("B"));
225
+ storage
226
+ .commit_write_set(writes, WriteOptions::default())
227
+ .expect("seed");
228
+ let read = storage
229
+ .begin_read(ReadOptions::default())
230
+ .expect("begin read");
231
+
232
+ let plan = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
233
+ let indexed = plan
234
+ .collect(&read, GetOptions::default())
235
+ .expect("indexed caller order values")
236
+ .value;
237
+
238
+ assert_eq!(indexed.len(), 4);
239
+ assert_eq!(indexed.unique_values.len(), 3);
240
+ assert_eq!(indexed.requested_to_unique.to_vec(), vec![0, 1, 2, 0]);
241
+ assert_eq!(
242
+ indexed.value_at(0),
243
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"B")))
244
+ );
245
+ assert_eq!(indexed.value_at(1), None);
246
+ assert_eq!(
247
+ indexed.value_at(2),
248
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"A")))
249
+ );
250
+ assert_eq!(
251
+ indexed.materialize_caller_order(),
252
+ vec![
253
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B"))),
254
+ None,
255
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"A"))),
256
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B"))),
257
+ ]
258
+ );
259
+ }
260
+
261
+ #[test]
262
+ fn point_request_plan_can_be_reused_for_indexed_reads() {
263
+ let storage = StorageContext::new(InMemoryBackend::new());
264
+ let mut writes = storage.new_write_set();
265
+ writes.put(space(1), key("a"), value("A"));
266
+ writes.put(space(1), key("b"), value("B"));
267
+ storage
268
+ .commit_write_set(writes, WriteOptions::default())
269
+ .expect("seed");
270
+ let read = storage
271
+ .begin_read(ReadOptions::default())
272
+ .expect("begin read");
273
+ let plan = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
274
+
275
+ assert_eq!(plan.len(), 4);
276
+ assert_eq!(
277
+ plan.logical_unique_keys,
278
+ vec![key("b"), key("missing"), key("a")]
279
+ );
280
+ assert_eq!(plan.requested_to_unique().to_vec(), vec![0, 1, 2, 0]);
281
+
282
+ let result = plan
283
+ .collect(&read, GetOptions::default())
284
+ .expect("planned indexed read");
285
+
286
+ assert_eq!(result.stats.requested_keys, 4);
287
+ assert_eq!(result.stats.unique_backend_keys, 3);
288
+ assert_eq!(result.stats.backend_calls, 1);
289
+ assert_eq!(result.value.requested_to_unique.to_vec(), vec![0, 1, 2, 0]);
290
+ assert_eq!(
291
+ result.value.value_at(0),
292
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"B")))
293
+ );
294
+ assert_eq!(result.value.value_at(1), None);
295
+ assert_eq!(
296
+ result.value.value_at(2),
297
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"A")))
298
+ );
299
+
300
+ let borrowed = plan
301
+ .collect(&read, GetOptions::default())
302
+ .expect("borrowed planned indexed read");
303
+
304
+ assert_eq!(borrowed.stats.requested_keys, 4);
305
+ assert_eq!(
306
+ borrowed.value.requested_to_unique,
307
+ plan.requested_to_unique()
308
+ );
309
+ assert_eq!(
310
+ borrowed.value.value_at(0),
311
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"B")))
312
+ );
313
+ assert_eq!(borrowed.value.value_at(1), None);
314
+ }
315
+
316
+ #[test]
317
+ fn planned_point_reads_can_reuse_value_buffer() {
318
+ let storage = StorageContext::new(InMemoryBackend::new());
319
+ let mut writes = storage.new_write_set();
320
+ writes.put(space(1), key("a"), value("A"));
321
+ writes.put(space(1), key("b"), value("B"));
322
+ writes.put(space(1), key("c"), value("C"));
323
+ storage
324
+ .commit_write_set(writes, WriteOptions::default())
325
+ .expect("seed");
326
+ let read = storage
327
+ .begin_read(ReadOptions::default())
328
+ .expect("begin read");
329
+ let first_plan =
330
+ PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
331
+ let second_plan = PointReadPlan::new(space(1), &[key("c")]);
332
+ let mut buffer = PointReadBuffer::new();
333
+
334
+ let first = first_plan
335
+ .collect_into(&read, GetOptions::default(), &mut buffer)
336
+ .expect("first buffered planned indexed read");
337
+
338
+ assert_eq!(first.stats.requested_keys, 4);
339
+ assert_eq!(first.stats.unique_backend_keys, 3);
340
+ assert_eq!(first.value.len(), 4);
341
+ assert_eq!(first.value.unique_values.len(), 3);
342
+ assert_eq!(
343
+ first.value.value_at(0),
344
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"B")))
345
+ );
346
+ assert_eq!(first.value.value_at(1), None);
347
+ assert_eq!(
348
+ first.value.value_at(2),
349
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"A")))
350
+ );
351
+ drop(first);
352
+
353
+ let capacity_after_first = buffer.capacity();
354
+ let second = second_plan
355
+ .collect_into(&read, GetOptions::default(), &mut buffer)
356
+ .expect("second buffered planned indexed read");
357
+
358
+ assert_eq!(second.stats.requested_keys, 1);
359
+ assert_eq!(second.stats.unique_backend_keys, 1);
360
+ assert_eq!(second.value.unique_values.len(), 1);
361
+ assert_eq!(
362
+ second.value.value_at(0),
363
+ Some(&ProjectedValue::FullValue(Bytes::from_static(b"C")))
364
+ );
365
+ drop(second);
366
+ assert!(
367
+ buffer.capacity() >= capacity_after_first,
368
+ "buffer allocation should be retained for reuse"
369
+ );
370
+ }
371
+
372
+ #[test]
373
+ fn point_request_plan_can_be_built_from_known_unique_keys() {
374
+ let plan = PointReadPlan::from_unique_keys(space(1), vec![key("a"), key("b"), key("c")]);
375
+
376
+ assert_eq!(plan.len(), 3);
377
+ assert_eq!(plan.logical_unique_keys, vec![key("a"), key("b"), key("c")]);
378
+ assert_eq!(plan.requested_to_unique().to_vec(), vec![0, 1, 2]);
379
+ }
380
+
381
+ #[test]
382
+ fn planned_point_reads_use_backend_requested_order_slots() {
383
+ let read = crate::storage::StorageReadScope::new(RequestedOrderRead::default());
384
+ let plan = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
385
+
386
+ let result = plan
387
+ .collect(&read, GetOptions::default())
388
+ .expect("borrowed planned indexed read");
389
+
390
+ assert_eq!(
391
+ read.backend_read().get_many_keys.borrow().as_slice(),
392
+ [
393
+ space(1).encode_key(&key("b")),
394
+ space(1).encode_key(&key("missing")),
395
+ space(1).encode_key(&key("a"))
396
+ ]
397
+ );
398
+ assert_eq!(result.stats.requested_keys, 4);
399
+ assert_eq!(result.stats.unique_backend_keys, 3);
400
+ assert_eq!(result.stats.backend_calls, 1);
401
+ assert_eq!(
402
+ result.value.value_at(0),
403
+ Some(&ProjectedValue::FullValue(Bytes::from_static(
404
+ b"\0\0\0\x01b"
405
+ )))
406
+ );
407
+ assert_eq!(result.value.value_at(1), None);
408
+ assert_eq!(
409
+ result.value.value_at(2),
410
+ Some(&ProjectedValue::FullValue(Bytes::from_static(
411
+ b"\0\0\0\x01a"
412
+ )))
413
+ );
414
+ }
415
+
416
+ #[test]
417
+ fn physical_point_request_plan_reuses_encoded_backend_keys() {
418
+ let read = crate::storage::StorageReadScope::new(RequestedOrderRead::default());
419
+ let plan = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
420
+
421
+ assert_eq!(
422
+ plan.logical_unique_keys,
423
+ vec![key("b"), key("missing"), key("a")]
424
+ );
425
+ assert_eq!(
426
+ plan.physical_unique_keys,
427
+ vec![
428
+ space(1).encode_key(&key("b")),
429
+ space(1).encode_key(&key("missing")),
430
+ space(1).encode_key(&key("a")),
431
+ ]
432
+ );
433
+
434
+ let result = plan
435
+ .collect(&read, GetOptions::default())
436
+ .expect("borrowed physical planned indexed read");
437
+
438
+ assert_eq!(
439
+ read.backend_read().get_many_keys.borrow().as_slice(),
440
+ plan.physical_unique_keys.as_slice()
441
+ );
442
+ assert_eq!(result.stats.requested_keys, 4);
443
+ assert_eq!(result.stats.unique_backend_keys, 3);
444
+ assert_eq!(result.stats.backend_calls, 1);
445
+ assert_eq!(
446
+ result.value.value_at(0),
447
+ Some(&ProjectedValue::FullValue(Bytes::from_static(
448
+ b"\0\0\0\x01b"
449
+ )))
450
+ );
451
+ assert_eq!(result.value.value_at(1), None);
452
+ }
453
+
454
+ #[test]
455
+ fn planned_point_reads_can_visit_unique_values_without_materializing_indexed_result() {
456
+ let storage = StorageContext::new(InMemoryBackend::new());
457
+ let mut writes = storage.new_write_set();
458
+ writes.put(space(1), key("a"), value("A"));
459
+ writes.put(space(1), key("b"), value("B"));
460
+ storage
461
+ .commit_write_set(writes, WriteOptions::default())
462
+ .expect("seed");
463
+ let read = storage
464
+ .begin_read(ReadOptions::default())
465
+ .expect("begin read");
466
+ let plan = PointReadPlan::new(space(1), &[key("b"), key("missing"), key("a"), key("b")]);
467
+
468
+ let mut visited = Vec::new();
469
+ let stats = plan
470
+ .visit(&read, GetOptions::default(), &mut |unique_index: usize,
471
+ key: &Key,
472
+ value: Option<
473
+ ProjectedValueRef<'_>,
474
+ >| {
475
+ visited.push((
476
+ unique_index,
477
+ key.clone(),
478
+ value.map(|value| value.to_owned()),
479
+ ));
480
+ Ok(())
481
+ })
482
+ .expect("visit unique point values");
483
+
484
+ assert_eq!(stats.requested_keys, 4);
485
+ assert_eq!(stats.unique_backend_keys, 3);
486
+ assert_eq!(stats.backend_calls, 1);
487
+ assert_eq!(
488
+ visited,
489
+ vec![
490
+ (
491
+ 0,
492
+ key("b"),
493
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"B")))
494
+ ),
495
+ (1, key("missing"), None),
496
+ (
497
+ 2,
498
+ key("a"),
499
+ Some(ProjectedValue::FullValue(Bytes::from_static(b"A")))
500
+ ),
501
+ ]
502
+ );
503
+ }
504
+
505
+ #[test]
506
+ fn point_reads_report_shape_stats() {
507
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
508
+ let result = PointReadPlan::new(space(1), &[key("b"), key("a"), key("b"), key("missing")])
509
+ .materialize(&read, GetOptions::default())
510
+ .expect("caller order");
511
+
512
+ assert_eq!(result.value.len(), 4);
513
+ assert_eq!(result.stats.requested_keys, 4);
514
+ assert_eq!(result.stats.unique_backend_keys, 3);
515
+ assert_eq!(result.stats.backend_calls, 1);
516
+ assert_eq!(result.stats.prefix_lowered, 0);
517
+ assert_eq!(result.stats.range_scan_chunks, 0);
518
+ assert_eq!(result.stats.prefix_scan_chunks, 0);
519
+ assert_eq!(result.stats.scan_key_only_chunks, 0);
520
+ assert_eq!(result.stats.scan_full_value_chunks, 0);
521
+ assert_eq!(result.stats.scan_rows, 0);
522
+ assert_eq!(result.stats.scan_has_more, 0);
523
+ assert_eq!(result.stats.scan_resume_after, 0);
524
+ assert_eq!(result.stats.scan_limit_rows_total, 0);
525
+ assert_eq!(result.stats.scan_limit_rows_max, 0);
526
+ }
527
+
528
+ #[test]
529
+ fn prefix_scan_lowers_to_range_and_respects_key_only_projection() {
530
+ let storage = StorageContext::new(InMemoryBackend::new());
531
+ let mut writes = storage.new_write_set();
532
+ writes.put(space(1), key("aa"), value("AA"));
533
+ writes.put(space(1), key("ab"), value("AB"));
534
+ writes.put(space(1), key("b"), value("B"));
535
+ storage
536
+ .commit_write_set(writes, WriteOptions::default())
537
+ .expect("seed");
538
+
539
+ let read = storage
540
+ .begin_read(ReadOptions::default())
541
+ .expect("begin read");
542
+ let chunk = ScanPlan::prefix(
543
+ space(1),
544
+ Prefix {
545
+ bytes: Bytes::from_static(b"a"),
546
+ },
547
+ )
548
+ .collect(
549
+ &read,
550
+ ScanOptions {
551
+ projection: CoreProjection::KeyOnly,
552
+ limit_rows: 10,
553
+ resume_after: None,
554
+ },
555
+ )
556
+ .expect("prefix scan");
557
+
558
+ assert_eq!(
559
+ chunk
560
+ .value
561
+ .entries
562
+ .into_iter()
563
+ .map(|entry| (entry.key, entry.value))
564
+ .collect::<Vec<_>>(),
565
+ vec![
566
+ (key("aa"), ProjectedValue::KeyOnly),
567
+ (key("ab"), ProjectedValue::KeyOnly),
568
+ ]
569
+ );
570
+ assert!(!chunk.value.has_more);
571
+ }
572
+
573
+ #[test]
574
+ fn scan_range_into_reuses_storage_buffer() {
575
+ let storage = StorageContext::new(InMemoryBackend::new());
576
+ let mut writes = storage.new_write_set();
577
+ writes.put(space(1), key("aa"), value("AA"));
578
+ writes.put(space(1), key("ab"), value("AB"));
579
+ writes.put(space(1), key("b"), value("B"));
580
+ storage
581
+ .commit_write_set(writes, WriteOptions::default())
582
+ .expect("seed");
583
+
584
+ let read = storage
585
+ .begin_read(ReadOptions::default())
586
+ .expect("begin read");
587
+ let mut buffer = ScanBuffer::with_capacity(8);
588
+
589
+ {
590
+ let chunk = ScanPlan::range(
591
+ space(1),
592
+ KeyRange {
593
+ lower: Bound::Included(key("a")),
594
+ upper: Bound::Excluded(key("b")),
595
+ },
596
+ )
597
+ .collect_into(
598
+ &read,
599
+ ScanOptions {
600
+ projection: CoreProjection::KeyOnly,
601
+ limit_rows: 10,
602
+ resume_after: None,
603
+ },
604
+ &mut buffer,
605
+ )
606
+ .expect("scan range into");
607
+
608
+ assert_eq!(
609
+ chunk
610
+ .value
611
+ .entries
612
+ .iter()
613
+ .map(|entry| (&entry.key, &entry.value))
614
+ .collect::<Vec<_>>(),
615
+ vec![
616
+ (&key("aa"), &ProjectedValue::KeyOnly),
617
+ (&key("ab"), &ProjectedValue::KeyOnly),
618
+ ]
619
+ );
620
+ assert!(!chunk.value.has_more);
621
+ }
622
+
623
+ let capacity_after_first_scan = buffer.capacity();
624
+ assert!(capacity_after_first_scan >= 8);
625
+
626
+ {
627
+ let chunk = ScanPlan::prefix(
628
+ space(1),
629
+ Prefix {
630
+ bytes: Bytes::from_static(b"a"),
631
+ },
632
+ )
633
+ .collect_into(
634
+ &read,
635
+ ScanOptions {
636
+ projection: CoreProjection::FullValue,
637
+ limit_rows: 10,
638
+ resume_after: None,
639
+ },
640
+ &mut buffer,
641
+ )
642
+ .expect("scan prefix into");
643
+
644
+ assert_eq!(
645
+ chunk
646
+ .value
647
+ .entries
648
+ .iter()
649
+ .map(|entry| (&entry.key, &entry.value))
650
+ .collect::<Vec<_>>(),
651
+ vec![
652
+ (
653
+ &key("aa"),
654
+ &ProjectedValue::FullValue(Bytes::from_static(b"AA"))
655
+ ),
656
+ (
657
+ &key("ab"),
658
+ &ProjectedValue::FullValue(Bytes::from_static(b"AB"))
659
+ ),
660
+ ]
661
+ );
662
+ assert!(!chunk.value.has_more);
663
+ }
664
+
665
+ assert_eq!(buffer.capacity(), capacity_after_first_scan);
666
+ }
667
+
668
+ #[test]
669
+ fn visit_scan_prefix_lowers_without_materializing_entries() {
670
+ let storage = StorageContext::new(InMemoryBackend::new());
671
+ let mut writes = storage.new_write_set();
672
+ writes.put(space(1), key("aa"), value("AA"));
673
+ writes.put(space(1), key("ab"), value("AB"));
674
+ writes.put(space(1), key("b"), value("B"));
675
+ storage
676
+ .commit_write_set(writes, WriteOptions::default())
677
+ .expect("seed");
678
+
679
+ let read = storage
680
+ .begin_read(ReadOptions::default())
681
+ .expect("begin read");
682
+ let mut visited = Vec::new();
683
+ let result = ScanPlan::prefix(
684
+ space(1),
685
+ Prefix {
686
+ bytes: Bytes::from_static(b"a"),
687
+ },
688
+ )
689
+ .visit(
690
+ &read,
691
+ ScanOptions {
692
+ projection: CoreProjection::FullValue,
693
+ limit_rows: 10,
694
+ resume_after: None,
695
+ },
696
+ &mut |key: KeyRef<'_>, value: ProjectedValueRef<'_>| {
697
+ visited.push((key.to_owned_key(), value.to_owned()));
698
+ Ok(())
699
+ },
700
+ )
701
+ .expect("visit scan prefix");
702
+
703
+ assert_eq!(result.value.emitted, 2);
704
+ assert!(!result.value.has_more);
705
+ assert_eq!(
706
+ visited,
707
+ vec![
708
+ (
709
+ key("aa"),
710
+ ProjectedValue::FullValue(Bytes::from_static(b"AA"))
711
+ ),
712
+ (
713
+ key("ab"),
714
+ ProjectedValue::FullValue(Bytes::from_static(b"AB"))
715
+ ),
716
+ ]
717
+ );
718
+ }
719
+
720
+ #[test]
721
+ fn prefix_scan_lowers_expected_range() {
722
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
723
+ ScanPlan::prefix(
724
+ space(1),
725
+ Prefix {
726
+ bytes: Bytes::from_static(b"a\xff"),
727
+ },
728
+ )
729
+ .collect(&read, ScanOptions::default())
730
+ .expect("prefix scan");
731
+
732
+ let range = read
733
+ .backend_read()
734
+ .scan_range
735
+ .borrow()
736
+ .clone()
737
+ .expect("range captured");
738
+ assert_eq!(
739
+ range.lower,
740
+ Bound::Included(space(1).encode_key(&key_bytes(b"a\xff")))
741
+ );
742
+ assert_eq!(range.upper, Bound::Excluded(space(1).encode_key(&key("b"))));
743
+ }
744
+
745
+ #[test]
746
+ fn scan_range_reports_shape_stats() {
747
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
748
+ let result = ScanPlan::range(
749
+ space(1),
750
+ KeyRange {
751
+ lower: Bound::Included(key("a")),
752
+ upper: Bound::Excluded(key("z")),
753
+ },
754
+ )
755
+ .collect(&read, ScanOptions::default())
756
+ .expect("scan range");
757
+
758
+ assert_eq!(result.stats.requested_keys, 0);
759
+ assert_eq!(result.stats.unique_backend_keys, 0);
760
+ assert_eq!(result.stats.backend_calls, 1);
761
+ assert_eq!(result.stats.prefix_lowered, 0);
762
+ assert_eq!(result.stats.range_scan_chunks, 1);
763
+ assert_eq!(result.stats.prefix_scan_chunks, 0);
764
+ assert_eq!(result.stats.scan_key_only_chunks, 0);
765
+ assert_eq!(result.stats.scan_full_value_chunks, 1);
766
+ assert_eq!(result.stats.scan_rows, 0);
767
+ assert_eq!(result.stats.scan_has_more, 0);
768
+ assert_eq!(result.stats.scan_resume_after, 0);
769
+ assert_eq!(result.stats.scan_limit_rows_total, 1024);
770
+ assert_eq!(result.stats.scan_limit_rows_max, 1024);
771
+ }
772
+
773
+ #[test]
774
+ fn prefix_scan_reports_shape_stats() {
775
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
776
+ let result = ScanPlan::prefix(
777
+ space(1),
778
+ Prefix {
779
+ bytes: Bytes::from_static(b"a"),
780
+ },
781
+ )
782
+ .collect(&read, ScanOptions::default())
783
+ .expect("prefix scan");
784
+
785
+ assert_eq!(result.stats.requested_keys, 0);
786
+ assert_eq!(result.stats.unique_backend_keys, 0);
787
+ assert_eq!(result.stats.backend_calls, 1);
788
+ assert_eq!(result.stats.prefix_lowered, 1);
789
+ assert_eq!(result.stats.range_scan_chunks, 0);
790
+ assert_eq!(result.stats.prefix_scan_chunks, 1);
791
+ assert_eq!(result.stats.scan_full_value_chunks, 1);
792
+ assert_eq!(*read.backend_read().scan_range_calls.borrow(), 1);
793
+ }
794
+
795
+ #[test]
796
+ fn visit_scan_reports_trace_stats() {
797
+ let storage = StorageContext::new(InMemoryBackend::new());
798
+ let mut writes = storage.new_write_set();
799
+ writes.put(space(1), key("aa"), value("AA"));
800
+ writes.put(space(1), key("ab"), value("AB"));
801
+ writes.put(space(1), key("ac"), value("AC"));
802
+ storage
803
+ .commit_write_set(writes, WriteOptions::default())
804
+ .expect("seed");
805
+ let read = storage
806
+ .begin_read(ReadOptions::default())
807
+ .expect("begin read");
808
+
809
+ let result = ScanPlan::prefix(
810
+ space(1),
811
+ Prefix {
812
+ bytes: Bytes::from_static(b"a"),
813
+ },
814
+ )
815
+ .visit(
816
+ &read,
817
+ ScanOptions {
818
+ projection: CoreProjection::KeyOnly,
819
+ limit_rows: 2,
820
+ resume_after: Some(&key("aa")),
821
+ },
822
+ &mut |_key: KeyRef<'_>, value: ProjectedValueRef<'_>| {
823
+ assert!(matches!(value, ProjectedValueRef::KeyOnly));
824
+ Ok(())
825
+ },
826
+ )
827
+ .expect("visit scan prefix with stats");
828
+
829
+ assert_eq!(result.value.emitted, 2);
830
+ assert!(!result.value.has_more);
831
+ assert_eq!(result.stats.backend_calls, 1);
832
+ assert_eq!(result.stats.prefix_lowered, 1);
833
+ assert_eq!(result.stats.range_scan_chunks, 0);
834
+ assert_eq!(result.stats.prefix_scan_chunks, 1);
835
+ assert_eq!(result.stats.scan_key_only_chunks, 1);
836
+ assert_eq!(result.stats.scan_full_value_chunks, 0);
837
+ assert_eq!(result.stats.scan_rows, 2);
838
+ assert_eq!(result.stats.scan_has_more, 0);
839
+ assert_eq!(result.stats.scan_resume_after, 1);
840
+ assert_eq!(result.stats.scan_limit_rows_total, 2);
841
+ assert_eq!(result.stats.scan_limit_rows_max, 2);
842
+ }
843
+
844
+ #[test]
845
+ fn prefix_scan_limit_zero_reports_no_backend_call() {
846
+ let read = crate::storage::StorageReadScope::new(SpyRead::default());
847
+ let result = ScanPlan::prefix(
848
+ space(1),
849
+ Prefix {
850
+ bytes: Bytes::from_static(b"a"),
851
+ },
852
+ )
853
+ .collect(
854
+ &read,
855
+ ScanOptions {
856
+ limit_rows: 0,
857
+ ..ScanOptions::default()
858
+ },
859
+ )
860
+ .expect("prefix scan");
861
+
862
+ assert!(result.value.entries.is_empty());
863
+ assert_eq!(result.stats.backend_calls, 0);
864
+ assert_eq!(result.stats.prefix_lowered, 1);
865
+ assert_eq!(*read.backend_read().scan_range_calls.borrow(), 0);
866
+ }
867
+ }