@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +4 -5
- package/dist/engine-wasm/wasm/lix_engine.js +1 -1
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +1 -1
- package/dist-engine-src/src/binary_cas/types.rs +0 -6
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/mod.rs +1 -1
- package/dist-engine-src/src/cel/provider.rs +1 -1
- package/dist-engine-src/src/commit_graph/context.rs +328 -1015
- package/dist-engine-src/src/commit_graph/mod.rs +2 -3
- package/dist-engine-src/src/commit_graph/types.rs +7 -43
- package/dist-engine-src/src/commit_graph/walker.rs +57 -81
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/identity.rs +15 -5
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +17 -12
- package/dist-engine-src/src/common/mod.rs +5 -5
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +29 -43
- package/dist-engine-src/src/entity_identity.rs +238 -118
- package/dist-engine-src/src/functions/context.rs +17 -52
- package/dist-engine-src/src/functions/deterministic.rs +1 -1
- package/dist-engine-src/src/functions/mod.rs +1 -1
- package/dist-engine-src/src/functions/provider.rs +4 -4
- package/dist-engine-src/src/functions/state.rs +39 -66
- package/dist-engine-src/src/functions/types.rs +1 -1
- package/dist-engine-src/src/init.rs +204 -151
- package/dist-engine-src/src/json_store/context.rs +354 -60
- package/dist-engine-src/src/json_store/encoded.rs +6 -6
- package/dist-engine-src/src/json_store/mod.rs +4 -1
- package/dist-engine-src/src/json_store/store.rs +884 -11
- package/dist-engine-src/src/json_store/types.rs +166 -1
- package/dist-engine-src/src/lib.rs +10 -9
- package/dist-engine-src/src/live_state/context.rs +608 -830
- package/dist-engine-src/src/live_state/mod.rs +3 -3
- package/dist-engine-src/src/live_state/overlay.rs +7 -7
- package/dist-engine-src/src/live_state/reader.rs +5 -5
- package/dist-engine-src/src/live_state/types.rs +19 -36
- package/dist-engine-src/src/live_state/visibility.rs +19 -14
- package/dist-engine-src/src/plugin/archive.rs +3 -6
- package/dist-engine-src/src/plugin/install.rs +0 -18
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
- package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +47 -17
- package/dist-engine-src/src/schema/definition.rs +202 -96
- package/dist-engine-src/src/schema/key.rs +9 -77
- package/dist-engine-src/src/schema/mod.rs +4 -4
- package/dist-engine-src/src/schema/tests.rs +133 -92
- package/dist-engine-src/src/session/context.rs +40 -42
- package/dist-engine-src/src/session/create_version.rs +22 -14
- package/dist-engine-src/src/session/execute.rs +45 -14
- package/dist-engine-src/src/session/merge/apply.rs +4 -4
- package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
- package/dist-engine-src/src/session/merge/stats.rs +1 -1
- package/dist-engine-src/src/session/merge/version.rs +35 -45
- package/dist-engine-src/src/session/mod.rs +4 -2
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +16 -28
- package/dist-engine-src/src/sql2/change_provider.rs +14 -20
- package/dist-engine-src/src/sql2/classify.rs +61 -26
- package/dist-engine-src/src/sql2/context.rs +22 -18
- package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
- package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
- package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
- package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
- package/dist-engine-src/src/sql2/error.rs +21 -1
- package/dist-engine-src/src/sql2/execute.rs +325 -264
- package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
- package/dist-engine-src/src/sql2/file_provider.rs +533 -108
- package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
- package/dist-engine-src/src/sql2/history_projection.rs +3 -27
- package/dist-engine-src/src/sql2/history_provider.rs +11 -17
- package/dist-engine-src/src/sql2/history_route.rs +22 -8
- package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
- package/dist-engine-src/src/sql2/mod.rs +6 -3
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +10 -12
- package/dist-engine-src/src/sql2/session.rs +7 -10
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +46 -31
- package/dist-engine-src/src/sql2/version_scope.rs +4 -4
- package/dist-engine-src/src/storage_bench.rs +1782 -325
- package/dist-engine-src/src/test_support.rs +183 -36
- package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
- package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
- package/dist-engine-src/src/tracked_state/context.rs +1155 -271
- package/dist-engine-src/src/tracked_state/diff.rs +249 -57
- package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +37 -19
- package/dist-engine-src/src/tracked_state/mod.rs +8 -7
- package/dist-engine-src/src/tracked_state/storage.rs +138 -6
- package/dist-engine-src/src/tracked_state/tree.rs +695 -252
- package/dist-engine-src/src/tracked_state/types.rs +176 -6
- package/dist-engine-src/src/transaction/commit.rs +695 -435
- package/dist-engine-src/src/transaction/context.rs +551 -310
- package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
- package/dist-engine-src/src/transaction/mod.rs +2 -0
- package/dist-engine-src/src/transaction/normalization.rs +311 -447
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
- package/dist-engine-src/src/transaction/staging.rs +701 -406
- package/dist-engine-src/src/transaction/types.rs +231 -122
- package/dist-engine-src/src/transaction/validation.rs +2717 -1698
- package/dist-engine-src/src/untracked_state/codec.rs +40 -96
- package/dist-engine-src/src/untracked_state/context.rs +21 -5
- package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
- package/dist-engine-src/src/untracked_state/mod.rs +3 -5
- package/dist-engine-src/src/untracked_state/storage.rs +105 -57
- package/dist-engine-src/src/untracked_state/types.rs +63 -13
- package/dist-engine-src/src/version/context.rs +1 -13
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +3 -2
- package/dist-engine-src/src/version/refs.rs +12 -103
- package/dist-engine-src/src/version/stage_rows.rs +15 -19
- package/package.json +1 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -321
- package/dist-engine-src/src/changelog/context.rs +0 -92
- package/dist-engine-src/src/changelog/materialization.rs +0 -121
- package/dist-engine-src/src/changelog/mod.rs +0 -13
- package/dist-engine-src/src/changelog/reader.rs +0 -20
- package/dist-engine-src/src/changelog/storage.rs +0 -220
- package/dist-engine-src/src/changelog/types.rs +0 -38
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
- package/dist-engine-src/src/schema_registry.rs +0 -294
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
- package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
- package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
use std::{
|
|
1
|
+
use std::{
|
|
2
|
+
collections::{BTreeMap, VecDeque},
|
|
3
|
+
future::Future,
|
|
4
|
+
ops::Range,
|
|
5
|
+
pin::Pin,
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
use crate::storage::{StorageReader, StorageWriteSet};
|
|
4
9
|
use crate::tracked_state::codec::{
|
|
5
|
-
boundary_trigger, child_summary_from_node, decode_key,
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
boundary_trigger, child_summary_from_node, decode_key, decode_key_with_trusted_prefix,
|
|
11
|
+
decode_node, decode_node_ref, decode_value, decode_visible_value, encode_internal_node,
|
|
12
|
+
encode_internal_node_refs, encode_key, encode_leaf_node, encode_leaf_node_refs,
|
|
13
|
+
encode_schema_file_prefix, encode_schema_key_prefix, ChildSummary, ChildSummaryRef,
|
|
14
|
+
DecodedLeafNodeRef, DecodedNode, DecodedNodeRef, EncodedLeafEntry, EncodedLeafEntryRef,
|
|
8
15
|
PendingChunkWrite,
|
|
9
16
|
};
|
|
10
17
|
use crate::tracked_state::storage;
|
|
11
|
-
use crate::tracked_state::
|
|
12
|
-
TrackedStateApplyResult, TrackedStateKey, TrackedStateMutation,
|
|
13
|
-
TrackedStateTreeDiffEntry, TrackedStateTreeScanRequest,
|
|
18
|
+
use crate::tracked_state::types::{
|
|
19
|
+
TrackedStateApplyResult, TrackedStateIndexValue, TrackedStateKey, TrackedStateMutation,
|
|
20
|
+
TrackedStateRootId, TrackedStateTreeDiffEntry, TrackedStateTreeScanRequest,
|
|
14
21
|
TRACKED_STATE_HASH_BYTES,
|
|
15
22
|
};
|
|
16
23
|
use crate::{LixError, NullableKeyFilter};
|
|
@@ -22,6 +29,11 @@ pub(crate) struct TrackedStateTreeOptions {
|
|
|
22
29
|
pub(crate) max_chunk_bytes: usize,
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
enum MutationApply<T> {
|
|
33
|
+
Applied(TrackedStateApplyResult),
|
|
34
|
+
Fallback(T),
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
impl Default for TrackedStateTreeOptions {
|
|
26
38
|
fn default() -> Self {
|
|
27
39
|
Self {
|
|
@@ -61,12 +73,13 @@ impl TrackedStateTree {
|
|
|
61
73
|
storage::load_root(store, commit_id).await
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
#[cfg(test)]
|
|
64
77
|
pub(crate) async fn get(
|
|
65
78
|
&self,
|
|
66
79
|
store: &mut impl StorageReader,
|
|
67
80
|
root_id: &TrackedStateRootId,
|
|
68
81
|
key: &TrackedStateKey,
|
|
69
|
-
) -> Result<Option<
|
|
82
|
+
) -> Result<Option<TrackedStateIndexValue>, LixError> {
|
|
70
83
|
let encoded_key = encode_key(key);
|
|
71
84
|
let mut current = *root_id.as_bytes();
|
|
72
85
|
loop {
|
|
@@ -102,7 +115,7 @@ impl TrackedStateTree {
|
|
|
102
115
|
store: &mut impl StorageReader,
|
|
103
116
|
root_id: &TrackedStateRootId,
|
|
104
117
|
keys: &[TrackedStateKey],
|
|
105
|
-
) -> Result<Vec<Option<
|
|
118
|
+
) -> Result<Vec<Option<TrackedStateIndexValue>>, LixError> {
|
|
106
119
|
if keys.is_empty() {
|
|
107
120
|
return Ok(Vec::new());
|
|
108
121
|
}
|
|
@@ -140,15 +153,23 @@ impl TrackedStateTree {
|
|
|
140
153
|
store: &mut impl StorageReader,
|
|
141
154
|
root_id: &TrackedStateRootId,
|
|
142
155
|
request: &TrackedStateTreeScanRequest,
|
|
143
|
-
) -> Result<Vec<(TrackedStateKey,
|
|
156
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
144
157
|
if request.limit == Some(0) {
|
|
145
158
|
return Ok(Vec::new());
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
let ranges = scan_ranges(request);
|
|
162
|
+
let key_decode_hint = scan_key_decode_hint(request, &ranges);
|
|
149
163
|
let mut rows = Vec::new();
|
|
150
|
-
self.scan_node(
|
|
151
|
-
|
|
164
|
+
self.scan_node(
|
|
165
|
+
store,
|
|
166
|
+
*root_id.as_bytes(),
|
|
167
|
+
request,
|
|
168
|
+
&ranges,
|
|
169
|
+
key_decode_hint,
|
|
170
|
+
&mut rows,
|
|
171
|
+
)
|
|
172
|
+
.await?;
|
|
152
173
|
Ok(rows)
|
|
153
174
|
}
|
|
154
175
|
|
|
@@ -215,38 +236,41 @@ impl TrackedStateTree {
|
|
|
215
236
|
store: &mut (impl StorageReader + ?Sized),
|
|
216
237
|
writes: &mut StorageWriteSet,
|
|
217
238
|
base_root: Option<&TrackedStateRootId>,
|
|
218
|
-
mutations: Vec<TrackedStateMutation>,
|
|
239
|
+
mut mutations: Vec<TrackedStateMutation>,
|
|
219
240
|
commit_id: Option<&str>,
|
|
220
241
|
) -> Result<TrackedStateApplyResult, LixError> {
|
|
221
242
|
let mut overlay = storage::TrackedStateChunkOverlay::new();
|
|
222
243
|
if let Some(root_id) = base_root {
|
|
223
244
|
if mutations.len() == 1 {
|
|
224
|
-
|
|
245
|
+
let mutation = mutations.pop().expect("single mutation should exist");
|
|
246
|
+
match self
|
|
225
247
|
.apply_single_mutation(
|
|
226
248
|
store,
|
|
227
249
|
writes,
|
|
228
250
|
&mut overlay,
|
|
229
251
|
root_id,
|
|
230
|
-
|
|
252
|
+
mutation,
|
|
231
253
|
commit_id,
|
|
232
254
|
)
|
|
233
255
|
.await?
|
|
234
256
|
{
|
|
235
|
-
return Ok(result)
|
|
257
|
+
MutationApply::Applied(result) => return Ok(result),
|
|
258
|
+
MutationApply::Fallback(mutation) => mutations = vec![mutation],
|
|
236
259
|
}
|
|
237
260
|
} else if mutations.len() > 1 {
|
|
238
|
-
|
|
261
|
+
match self
|
|
239
262
|
.apply_sorted_mutations_chunker(
|
|
240
263
|
store,
|
|
241
264
|
writes,
|
|
242
265
|
&mut overlay,
|
|
243
266
|
root_id,
|
|
244
|
-
|
|
267
|
+
mutations,
|
|
245
268
|
commit_id,
|
|
246
269
|
)
|
|
247
270
|
.await?
|
|
248
271
|
{
|
|
249
|
-
return Ok(result)
|
|
272
|
+
MutationApply::Applied(result) => return Ok(result),
|
|
273
|
+
MutationApply::Fallback(fallback_mutations) => mutations = fallback_mutations,
|
|
250
274
|
}
|
|
251
275
|
}
|
|
252
276
|
}
|
|
@@ -264,11 +288,7 @@ impl TrackedStateTree {
|
|
|
264
288
|
// Apply in caller order so repeated writes to the same key behave like
|
|
265
289
|
// normal transaction staging: the latest mutation wins.
|
|
266
290
|
for mutation in mutations {
|
|
267
|
-
|
|
268
|
-
TrackedStateMutation::Put { key, value } => {
|
|
269
|
-
entries.insert(encode_key(&key), encode_value(&value));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
291
|
+
entries.insert(mutation.encoded_key, mutation.encoded_value);
|
|
272
292
|
}
|
|
273
293
|
|
|
274
294
|
let built = self.build_tree_from_entries(
|
|
@@ -301,60 +321,71 @@ impl TrackedStateTree {
|
|
|
301
321
|
writes: &mut StorageWriteSet,
|
|
302
322
|
overlay: &mut storage::TrackedStateChunkOverlay,
|
|
303
323
|
root_id: &TrackedStateRootId,
|
|
304
|
-
mutation:
|
|
324
|
+
mutation: TrackedStateMutation,
|
|
305
325
|
commit_id: Option<&str>,
|
|
306
|
-
) -> Result<
|
|
307
|
-
let
|
|
308
|
-
let encoded_key = encode_key(key);
|
|
309
|
-
let encoded_value = encode_value(value);
|
|
310
|
-
|
|
311
|
-
if let Some(result) = self
|
|
326
|
+
) -> Result<MutationApply<TrackedStateMutation>, LixError> {
|
|
327
|
+
let mutation = match self
|
|
312
328
|
.apply_single_mutation_from_seek_path(
|
|
313
|
-
store,
|
|
314
|
-
writes,
|
|
315
|
-
overlay,
|
|
316
|
-
root_id,
|
|
317
|
-
&encoded_key,
|
|
318
|
-
&encoded_value,
|
|
319
|
-
commit_id,
|
|
329
|
+
store, writes, overlay, root_id, mutation, commit_id,
|
|
320
330
|
)
|
|
321
331
|
.await?
|
|
322
332
|
{
|
|
323
|
-
return Ok(result)
|
|
324
|
-
|
|
333
|
+
MutationApply::Applied(result) => return Ok(MutationApply::Applied(result)),
|
|
334
|
+
MutationApply::Fallback(mutation) => mutation,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
let TrackedStateMutation {
|
|
338
|
+
encoded_key,
|
|
339
|
+
encoded_value,
|
|
340
|
+
} = mutation;
|
|
325
341
|
|
|
326
342
|
let levels = self
|
|
327
343
|
.collect_summary_levels_with_overlay(store, overlay, root_id)
|
|
328
344
|
.await?;
|
|
329
345
|
let Some(leaves) = levels.first() else {
|
|
330
|
-
return Ok(
|
|
346
|
+
return Ok(MutationApply::Fallback(TrackedStateMutation {
|
|
347
|
+
encoded_key,
|
|
348
|
+
encoded_value,
|
|
349
|
+
}));
|
|
331
350
|
};
|
|
332
351
|
let target_leaf_index = leaves
|
|
333
352
|
.iter()
|
|
334
353
|
.position(|leaf| leaf.last_key.as_slice() >= encoded_key.as_slice())
|
|
335
354
|
.unwrap_or_else(|| leaves.len().saturating_sub(1));
|
|
336
355
|
let Some(target_leaf) = leaves.get(target_leaf_index).cloned() else {
|
|
337
|
-
return Ok(
|
|
356
|
+
return Ok(MutationApply::Fallback(TrackedStateMutation {
|
|
357
|
+
encoded_key,
|
|
358
|
+
encoded_value,
|
|
359
|
+
}));
|
|
338
360
|
};
|
|
339
361
|
|
|
340
362
|
let mut entries = self
|
|
341
363
|
.load_leaf_entries_with_overlay(store, overlay, &target_leaf.child_hash)
|
|
342
364
|
.await?;
|
|
343
|
-
match entries
|
|
365
|
+
let mutation_entry_index = match entries
|
|
366
|
+
.binary_search_by(|entry| entry.key.as_slice().cmp(encoded_key.as_slice()))
|
|
367
|
+
{
|
|
344
368
|
Ok(index) => {
|
|
345
|
-
if entries[index].value == encoded_value {
|
|
346
|
-
return Ok(
|
|
369
|
+
if entries[index].value.as_slice() == encoded_value.as_slice() {
|
|
370
|
+
return Ok(MutationApply::Fallback(TrackedStateMutation {
|
|
371
|
+
encoded_key,
|
|
372
|
+
encoded_value,
|
|
373
|
+
}));
|
|
347
374
|
}
|
|
348
375
|
entries[index].value = encoded_value;
|
|
376
|
+
index
|
|
349
377
|
}
|
|
350
|
-
Err(index) =>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
Err(index) => {
|
|
379
|
+
entries.insert(
|
|
380
|
+
index,
|
|
381
|
+
EncodedLeafEntry {
|
|
382
|
+
key: encoded_key,
|
|
383
|
+
value: encoded_value,
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
index
|
|
387
|
+
}
|
|
388
|
+
};
|
|
358
389
|
|
|
359
390
|
let mut chunks = BTreeMap::new();
|
|
360
391
|
let mut suffix_entries = entries;
|
|
@@ -366,20 +397,25 @@ impl TrackedStateTree {
|
|
|
366
397
|
// existing post-mutation leaf, then reuse the rest of the old suffix.
|
|
367
398
|
loop {
|
|
368
399
|
let mut candidate_chunks = BTreeMap::new();
|
|
369
|
-
let candidate_summaries =
|
|
370
|
-
|
|
400
|
+
let candidate_summaries = self.build_leaf_level_from_refs(
|
|
401
|
+
suffix_entries.iter().map(EncodedLeafEntry::as_ref),
|
|
402
|
+
&mut candidate_chunks,
|
|
403
|
+
);
|
|
371
404
|
|
|
372
405
|
if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
|
|
373
406
|
&candidate_summaries,
|
|
374
407
|
&leaves[target_leaf_index..],
|
|
375
|
-
|
|
408
|
+
suffix_entries[mutation_entry_index].key.as_slice(),
|
|
376
409
|
) {
|
|
377
410
|
for summary in &candidate_summaries[..generated_resync_index] {
|
|
378
411
|
if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
|
|
379
412
|
chunks.entry(chunk.hash).or_insert(chunk);
|
|
380
413
|
}
|
|
381
414
|
}
|
|
382
|
-
replacement_leaves = candidate_summaries
|
|
415
|
+
replacement_leaves = candidate_summaries
|
|
416
|
+
.into_iter()
|
|
417
|
+
.take(generated_resync_index)
|
|
418
|
+
.collect();
|
|
383
419
|
old_leaf_count = existing_resync_index;
|
|
384
420
|
break;
|
|
385
421
|
}
|
|
@@ -408,7 +444,7 @@ impl TrackedStateTree {
|
|
|
408
444
|
old_leaf_count,
|
|
409
445
|
std::mem::take(&mut replacement_leaves),
|
|
410
446
|
chunks,
|
|
411
|
-
|
|
447
|
+
suffix_entries[mutation_entry_index].key.as_slice(),
|
|
412
448
|
)?;
|
|
413
449
|
overlay.stage_chunks(writes, &built.chunks);
|
|
414
450
|
let persisted_root = if let Some(commit_id) = commit_id {
|
|
@@ -418,7 +454,7 @@ impl TrackedStateTree {
|
|
|
418
454
|
false
|
|
419
455
|
};
|
|
420
456
|
|
|
421
|
-
Ok(
|
|
457
|
+
Ok(MutationApply::Applied(TrackedStateApplyResult {
|
|
422
458
|
root_id: built.root_id,
|
|
423
459
|
row_count: built.row_count,
|
|
424
460
|
tree_height: built.tree_height,
|
|
@@ -652,48 +688,65 @@ impl TrackedStateTree {
|
|
|
652
688
|
writes: &mut StorageWriteSet,
|
|
653
689
|
overlay: &mut storage::TrackedStateChunkOverlay,
|
|
654
690
|
root_id: &TrackedStateRootId,
|
|
655
|
-
mutations:
|
|
691
|
+
mutations: Vec<TrackedStateMutation>,
|
|
656
692
|
commit_id: Option<&str>,
|
|
657
|
-
) -> Result<
|
|
693
|
+
) -> Result<MutationApply<Vec<TrackedStateMutation>>, LixError> {
|
|
658
694
|
let mut mutation_map = BTreeMap::new();
|
|
659
695
|
for mutation in mutations {
|
|
660
|
-
|
|
661
|
-
mutation_map.insert(encode_key(key), encode_value(value));
|
|
696
|
+
mutation_map.insert(mutation.encoded_key, mutation.encoded_value);
|
|
662
697
|
}
|
|
663
698
|
if mutation_map.is_empty() {
|
|
664
|
-
return Ok(
|
|
699
|
+
return Ok(MutationApply::Fallback(Vec::new()));
|
|
665
700
|
}
|
|
666
701
|
|
|
667
|
-
let mutations = mutation_map.into_iter().collect::<Vec<_>>();
|
|
668
|
-
|
|
669
702
|
let levels = self
|
|
670
703
|
.collect_summary_levels_with_overlay(store, overlay, root_id)
|
|
671
704
|
.await?;
|
|
672
705
|
let Some(leaves) = levels.first() else {
|
|
673
|
-
return Ok(
|
|
706
|
+
return Ok(MutationApply::Fallback(
|
|
707
|
+
mutation_map
|
|
708
|
+
.into_iter()
|
|
709
|
+
.map(|(encoded_key, encoded_value)| TrackedStateMutation {
|
|
710
|
+
encoded_key,
|
|
711
|
+
encoded_value,
|
|
712
|
+
})
|
|
713
|
+
.collect(),
|
|
714
|
+
));
|
|
674
715
|
};
|
|
675
716
|
|
|
676
717
|
let base_row_count = leaves
|
|
677
718
|
.iter()
|
|
678
719
|
.map(|leaf| leaf.subtree_count as usize)
|
|
679
720
|
.sum::<usize>();
|
|
721
|
+
let first_mutation_key = mutation_map
|
|
722
|
+
.keys()
|
|
723
|
+
.next()
|
|
724
|
+
.expect("non-empty mutation map should have first key");
|
|
680
725
|
let append_only = leaves
|
|
681
726
|
.last()
|
|
682
|
-
.is_some_and(|leaf|
|
|
683
|
-
if !append_only &&
|
|
684
|
-
return Ok(
|
|
727
|
+
.is_some_and(|leaf| first_mutation_key.as_slice() > leaf.last_key.as_slice());
|
|
728
|
+
if !append_only && mutation_map.len() * 2 > base_row_count {
|
|
729
|
+
return Ok(MutationApply::Fallback(
|
|
730
|
+
mutation_map
|
|
731
|
+
.into_iter()
|
|
732
|
+
.map(|(encoded_key, encoded_value)| TrackedStateMutation {
|
|
733
|
+
encoded_key,
|
|
734
|
+
encoded_value,
|
|
735
|
+
})
|
|
736
|
+
.collect(),
|
|
737
|
+
));
|
|
685
738
|
}
|
|
686
739
|
|
|
740
|
+
let mut mutations = mutation_map.into_iter().collect::<VecDeque<_>>();
|
|
687
741
|
let mut output_leaves = Vec::new();
|
|
688
742
|
let mut chunks = BTreeMap::new();
|
|
689
743
|
let mut leaf_index = 0usize;
|
|
690
|
-
let mut next_mutation_index = 0usize;
|
|
691
744
|
|
|
692
745
|
while leaf_index < leaves.len() {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
{
|
|
746
|
+
let current_leaf_has_mutation = mutations
|
|
747
|
+
.front()
|
|
748
|
+
.is_some_and(|(key, _)| key.as_slice() <= leaves[leaf_index].last_key.as_slice());
|
|
749
|
+
if !current_leaf_has_mutation {
|
|
697
750
|
output_leaves.push(leaves[leaf_index].clone());
|
|
698
751
|
leaf_index += 1;
|
|
699
752
|
continue;
|
|
@@ -701,7 +754,10 @@ impl TrackedStateTree {
|
|
|
701
754
|
|
|
702
755
|
let window_start = leaf_index;
|
|
703
756
|
let mut window_entries = BTreeMap::new();
|
|
704
|
-
let mut window_mutation_ceiling = mutations
|
|
757
|
+
let mut window_mutation_ceiling = mutations
|
|
758
|
+
.front()
|
|
759
|
+
.map(|(key, _)| key.clone())
|
|
760
|
+
.expect("window with mutation should have front mutation");
|
|
705
761
|
|
|
706
762
|
loop {
|
|
707
763
|
if leaf_index < leaves.len() {
|
|
@@ -713,46 +769,45 @@ impl TrackedStateTree {
|
|
|
713
769
|
window_entries.insert(entry.key, entry.value);
|
|
714
770
|
}
|
|
715
771
|
|
|
716
|
-
while
|
|
717
|
-
|
|
772
|
+
while mutations
|
|
773
|
+
.front()
|
|
774
|
+
.is_some_and(|(key, _)| key.as_slice() <= leaf.last_key.as_slice())
|
|
718
775
|
{
|
|
719
|
-
let (key, value) =
|
|
720
|
-
|
|
776
|
+
let (key, value) = mutations
|
|
777
|
+
.pop_front()
|
|
778
|
+
.expect("front mutation should be present");
|
|
721
779
|
window_mutation_ceiling = key.clone();
|
|
722
|
-
|
|
780
|
+
window_entries.insert(key, value);
|
|
723
781
|
}
|
|
724
782
|
leaf_index += 1;
|
|
725
783
|
}
|
|
726
784
|
|
|
727
|
-
while
|
|
728
|
-
let (key, value) = &mutations[next_mutation_index];
|
|
785
|
+
while let Some((key, _)) = mutations.front() {
|
|
729
786
|
if leaf_index < leaves.len()
|
|
730
787
|
&& key.as_slice() >= leaves[leaf_index].first_key.as_slice()
|
|
731
788
|
{
|
|
732
789
|
break;
|
|
733
790
|
}
|
|
734
|
-
|
|
791
|
+
let (key, value) = mutations
|
|
792
|
+
.pop_front()
|
|
793
|
+
.expect("front mutation should be present");
|
|
735
794
|
window_mutation_ceiling = key.clone();
|
|
736
|
-
|
|
795
|
+
window_entries.insert(key, value);
|
|
737
796
|
}
|
|
738
797
|
|
|
739
|
-
if
|
|
740
|
-
&&
|
|
741
|
-
|
|
742
|
-
|
|
798
|
+
if leaf_index < leaves.len()
|
|
799
|
+
&& mutations.front().is_some_and(|(key, _)| {
|
|
800
|
+
key.as_slice() <= leaves[leaf_index].last_key.as_slice()
|
|
801
|
+
})
|
|
743
802
|
{
|
|
744
803
|
continue;
|
|
745
804
|
}
|
|
746
805
|
|
|
747
806
|
let mut candidate_chunks = BTreeMap::new();
|
|
748
|
-
let candidate_leaves = self.
|
|
807
|
+
let candidate_leaves = self.build_leaf_level_from_refs(
|
|
749
808
|
window_entries
|
|
750
809
|
.iter()
|
|
751
|
-
.map(|(key, value)|
|
|
752
|
-
key: key.clone(),
|
|
753
|
-
value: value.clone(),
|
|
754
|
-
})
|
|
755
|
-
.collect(),
|
|
810
|
+
.map(|(key, value)| EncodedLeafEntryRef { key, value }),
|
|
756
811
|
&mut candidate_chunks,
|
|
757
812
|
);
|
|
758
813
|
|
|
@@ -766,7 +821,7 @@ impl TrackedStateTree {
|
|
|
766
821
|
chunks.entry(chunk.hash).or_insert(chunk);
|
|
767
822
|
}
|
|
768
823
|
}
|
|
769
|
-
output_leaves.
|
|
824
|
+
output_leaves.extend(candidate_leaves.into_iter().take(generated_resync_index));
|
|
770
825
|
leaf_index = window_start + existing_resync_index;
|
|
771
826
|
break;
|
|
772
827
|
}
|
|
@@ -779,20 +834,19 @@ impl TrackedStateTree {
|
|
|
779
834
|
}
|
|
780
835
|
}
|
|
781
836
|
|
|
782
|
-
if
|
|
783
|
-
let
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
value: value.clone(),
|
|
788
|
-
});
|
|
789
|
-
}
|
|
837
|
+
if !mutations.is_empty() {
|
|
838
|
+
let entries = mutations
|
|
839
|
+
.into_iter()
|
|
840
|
+
.map(|(key, value)| EncodedLeafEntry { key, value })
|
|
841
|
+
.collect();
|
|
790
842
|
output_leaves.extend(self.build_leaf_level(entries, &mut chunks));
|
|
791
843
|
}
|
|
792
844
|
|
|
793
845
|
let built = self.build_tree_from_leaf_summaries(output_leaves, chunks)?;
|
|
794
|
-
|
|
795
|
-
.
|
|
846
|
+
Ok(MutationApply::Applied(
|
|
847
|
+
self.persist_built_tree(writes, overlay, built, commit_id)
|
|
848
|
+
.await?,
|
|
849
|
+
))
|
|
796
850
|
}
|
|
797
851
|
|
|
798
852
|
async fn apply_single_mutation_from_seek_path(
|
|
@@ -801,10 +855,13 @@ impl TrackedStateTree {
|
|
|
801
855
|
writes: &mut StorageWriteSet,
|
|
802
856
|
overlay: &mut storage::TrackedStateChunkOverlay,
|
|
803
857
|
root_id: &TrackedStateRootId,
|
|
804
|
-
|
|
805
|
-
encoded_value: &[u8],
|
|
858
|
+
mutation: TrackedStateMutation,
|
|
806
859
|
commit_id: Option<&str>,
|
|
807
|
-
) -> Result<
|
|
860
|
+
) -> Result<MutationApply<TrackedStateMutation>, LixError> {
|
|
861
|
+
let TrackedStateMutation {
|
|
862
|
+
encoded_key,
|
|
863
|
+
encoded_value,
|
|
864
|
+
} = mutation;
|
|
808
865
|
let mut current = *root_id.as_bytes();
|
|
809
866
|
let mut path = Vec::new();
|
|
810
867
|
let mut entries = loop {
|
|
@@ -817,7 +874,7 @@ impl TrackedStateTree {
|
|
|
817
874
|
let children = internal.children().to_vec();
|
|
818
875
|
let child_index = children
|
|
819
876
|
.iter()
|
|
820
|
-
.position(|child| child.last_key.as_slice() >= encoded_key)
|
|
877
|
+
.position(|child| child.last_key.as_slice() >= encoded_key.as_slice())
|
|
821
878
|
.or_else(|| (!children.is_empty()).then_some(children.len() - 1))
|
|
822
879
|
.ok_or_else(|| {
|
|
823
880
|
LixError::new(
|
|
@@ -834,21 +891,30 @@ impl TrackedStateTree {
|
|
|
834
891
|
}
|
|
835
892
|
};
|
|
836
893
|
|
|
837
|
-
match entries
|
|
894
|
+
let mutation_entry_index = match entries
|
|
895
|
+
.binary_search_by(|entry| entry.key.as_slice().cmp(encoded_key.as_slice()))
|
|
896
|
+
{
|
|
838
897
|
Ok(index) => {
|
|
839
|
-
if entries[index].value == encoded_value {
|
|
840
|
-
return Ok(
|
|
898
|
+
if entries[index].value.as_slice() == encoded_value.as_slice() {
|
|
899
|
+
return Ok(MutationApply::Fallback(TrackedStateMutation {
|
|
900
|
+
encoded_key,
|
|
901
|
+
encoded_value,
|
|
902
|
+
}));
|
|
841
903
|
}
|
|
842
|
-
entries[index].value = encoded_value
|
|
904
|
+
entries[index].value = encoded_value;
|
|
905
|
+
index
|
|
843
906
|
}
|
|
844
|
-
Err(index) =>
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
907
|
+
Err(index) => {
|
|
908
|
+
entries.insert(
|
|
909
|
+
index,
|
|
910
|
+
EncodedLeafEntry {
|
|
911
|
+
key: encoded_key,
|
|
912
|
+
value: encoded_value,
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
index
|
|
916
|
+
}
|
|
917
|
+
};
|
|
852
918
|
|
|
853
919
|
let mut chunks = BTreeMap::new();
|
|
854
920
|
let mut replacement_children;
|
|
@@ -856,10 +922,10 @@ impl TrackedStateTree {
|
|
|
856
922
|
|
|
857
923
|
let Some(leaf_parent) = path.pop() else {
|
|
858
924
|
let built = self.build_tree_from_entries(entries)?;
|
|
859
|
-
return
|
|
860
|
-
.persist_built_tree(writes, overlay, built, commit_id)
|
|
861
|
-
|
|
862
|
-
|
|
925
|
+
return Ok(MutationApply::Applied(
|
|
926
|
+
self.persist_built_tree(writes, overlay, built, commit_id)
|
|
927
|
+
.await?,
|
|
928
|
+
));
|
|
863
929
|
};
|
|
864
930
|
let mutation_is_right_edge = leaf_parent.child_index + 1 == leaf_parent.children.len()
|
|
865
931
|
&& path
|
|
@@ -870,26 +936,35 @@ impl TrackedStateTree {
|
|
|
870
936
|
let mut next_leaf_index = leaf_parent.child_index + 1;
|
|
871
937
|
loop {
|
|
872
938
|
let mut candidate_chunks = BTreeMap::new();
|
|
873
|
-
let candidate_leaves =
|
|
874
|
-
|
|
939
|
+
let candidate_leaves = self.build_leaf_level_from_refs(
|
|
940
|
+
leaf_entries.iter().map(EncodedLeafEntry::as_ref),
|
|
941
|
+
&mut candidate_chunks,
|
|
942
|
+
);
|
|
875
943
|
if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
|
|
876
944
|
&candidate_leaves,
|
|
877
945
|
&leaf_parent.children[leaf_parent.child_index..],
|
|
878
|
-
|
|
946
|
+
leaf_entries[mutation_entry_index].key.as_slice(),
|
|
879
947
|
) {
|
|
880
948
|
for summary in &candidate_leaves[..generated_resync_index] {
|
|
881
949
|
if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
|
|
882
950
|
chunks.entry(chunk.hash).or_insert(chunk);
|
|
883
951
|
}
|
|
884
952
|
}
|
|
885
|
-
replacement_children = candidate_leaves
|
|
953
|
+
replacement_children = candidate_leaves
|
|
954
|
+
.into_iter()
|
|
955
|
+
.take(generated_resync_index)
|
|
956
|
+
.collect();
|
|
886
957
|
old_child_count = existing_resync_index;
|
|
887
958
|
break;
|
|
888
959
|
}
|
|
889
960
|
|
|
890
961
|
if next_leaf_index >= leaf_parent.children.len() {
|
|
891
962
|
if !mutation_is_right_edge {
|
|
892
|
-
|
|
963
|
+
let entry = leaf_entries.remove(mutation_entry_index);
|
|
964
|
+
return Ok(MutationApply::Fallback(TrackedStateMutation {
|
|
965
|
+
encoded_key: entry.key,
|
|
966
|
+
encoded_value: entry.value,
|
|
967
|
+
}));
|
|
893
968
|
}
|
|
894
969
|
chunks.extend(candidate_chunks);
|
|
895
970
|
replacement_children = candidate_leaves;
|
|
@@ -941,10 +1016,10 @@ impl TrackedStateTree {
|
|
|
941
1016
|
tree_height,
|
|
942
1017
|
chunk_bytes,
|
|
943
1018
|
};
|
|
944
|
-
return
|
|
945
|
-
.persist_built_tree(writes, overlay, built, commit_id)
|
|
946
|
-
|
|
947
|
-
|
|
1019
|
+
return Ok(MutationApply::Applied(
|
|
1020
|
+
self.persist_built_tree(writes, overlay, built, commit_id)
|
|
1021
|
+
.await?,
|
|
1022
|
+
));
|
|
948
1023
|
};
|
|
949
1024
|
|
|
950
1025
|
child_index = frame.child_index;
|
|
@@ -959,7 +1034,7 @@ impl TrackedStateTree {
|
|
|
959
1034
|
overlay: &mut storage::TrackedStateChunkOverlay,
|
|
960
1035
|
built: BuiltTree,
|
|
961
1036
|
commit_id: Option<&str>,
|
|
962
|
-
) -> Result<
|
|
1037
|
+
) -> Result<TrackedStateApplyResult, LixError> {
|
|
963
1038
|
overlay.stage_chunks(writes, &built.chunks);
|
|
964
1039
|
let persisted_root = if let Some(commit_id) = commit_id {
|
|
965
1040
|
storage::stage_root(writes, commit_id, &built.root_id);
|
|
@@ -967,14 +1042,14 @@ impl TrackedStateTree {
|
|
|
967
1042
|
} else {
|
|
968
1043
|
false
|
|
969
1044
|
};
|
|
970
|
-
Ok(
|
|
1045
|
+
Ok(TrackedStateApplyResult {
|
|
971
1046
|
root_id: built.root_id,
|
|
972
1047
|
row_count: built.row_count,
|
|
973
1048
|
tree_height: built.tree_height,
|
|
974
1049
|
chunk_count: built.chunks.len(),
|
|
975
1050
|
chunk_bytes: built.chunk_bytes,
|
|
976
1051
|
persisted_root,
|
|
977
|
-
})
|
|
1052
|
+
})
|
|
978
1053
|
}
|
|
979
1054
|
|
|
980
1055
|
fn build_tree_from_entries(
|
|
@@ -1130,15 +1205,23 @@ impl TrackedStateTree {
|
|
|
1130
1205
|
let parent_end_child_range =
|
|
1131
1206
|
child_range_for_parent(old_children, &old_parents[parent_end])?;
|
|
1132
1207
|
let mut window_children = Vec::new();
|
|
1133
|
-
window_children.
|
|
1134
|
-
|
|
1135
|
-
|
|
1208
|
+
window_children.extend(
|
|
1209
|
+
old_children[parent_child_range.start..child_start]
|
|
1210
|
+
.iter()
|
|
1211
|
+
.map(ChildSummary::as_ref),
|
|
1212
|
+
);
|
|
1213
|
+
window_children.extend(replacement_children.iter().map(ChildSummary::as_ref));
|
|
1214
|
+
window_children.extend(
|
|
1215
|
+
old_children[old_child_end..parent_end_child_range.end]
|
|
1216
|
+
.iter()
|
|
1217
|
+
.map(ChildSummary::as_ref),
|
|
1218
|
+
);
|
|
1136
1219
|
let mut next_parent_index = parent_end + 1;
|
|
1137
1220
|
|
|
1138
1221
|
loop {
|
|
1139
1222
|
let mut candidate_chunks = BTreeMap::new();
|
|
1140
|
-
let candidate_parents = self.
|
|
1141
|
-
window_children.
|
|
1223
|
+
let candidate_parents = self.build_internal_level_from_refs(
|
|
1224
|
+
window_children.iter().copied(),
|
|
1142
1225
|
parent_level,
|
|
1143
1226
|
&mut candidate_chunks,
|
|
1144
1227
|
);
|
|
@@ -1156,7 +1239,10 @@ impl TrackedStateTree {
|
|
|
1156
1239
|
return Ok(ParentLevelPatch {
|
|
1157
1240
|
parent_start,
|
|
1158
1241
|
old_parent_count: existing_resync_index,
|
|
1159
|
-
replacement_parents: candidate_parents
|
|
1242
|
+
replacement_parents: candidate_parents
|
|
1243
|
+
.into_iter()
|
|
1244
|
+
.take(generated_resync_index)
|
|
1245
|
+
.collect(),
|
|
1160
1246
|
});
|
|
1161
1247
|
}
|
|
1162
1248
|
|
|
@@ -1170,7 +1256,7 @@ impl TrackedStateTree {
|
|
|
1170
1256
|
}
|
|
1171
1257
|
|
|
1172
1258
|
let next_range = child_range_for_parent(old_children, &old_parents[next_parent_index])?;
|
|
1173
|
-
window_children.
|
|
1259
|
+
window_children.extend(old_children[next_range].iter().map(ChildSummary::as_ref));
|
|
1174
1260
|
next_parent_index += 1;
|
|
1175
1261
|
}
|
|
1176
1262
|
}
|
|
@@ -1204,6 +1290,35 @@ impl TrackedStateTree {
|
|
|
1204
1290
|
.collect()
|
|
1205
1291
|
}
|
|
1206
1292
|
|
|
1293
|
+
fn build_leaf_level_from_refs<'a>(
|
|
1294
|
+
&self,
|
|
1295
|
+
entries: impl IntoIterator<Item = EncodedLeafEntryRef<'a>>,
|
|
1296
|
+
chunks: &mut BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
|
|
1297
|
+
) -> Vec<ChildSummary> {
|
|
1298
|
+
let groups = chunk_leaf_entry_refs(entries, &self.options);
|
|
1299
|
+
groups
|
|
1300
|
+
.into_iter()
|
|
1301
|
+
.map(|group| {
|
|
1302
|
+
let subtree_count = group.entries.len() as u64;
|
|
1303
|
+
let first_key = group
|
|
1304
|
+
.entries
|
|
1305
|
+
.first()
|
|
1306
|
+
.map(|entry| entry.key.to_vec())
|
|
1307
|
+
.unwrap_or_default();
|
|
1308
|
+
let last_key = group
|
|
1309
|
+
.entries
|
|
1310
|
+
.last()
|
|
1311
|
+
.map(|entry| entry.key.to_vec())
|
|
1312
|
+
.unwrap_or_default();
|
|
1313
|
+
let node = encode_leaf_node_refs(&group.entries);
|
|
1314
|
+
let (chunk, summary) =
|
|
1315
|
+
child_summary_from_node(node, first_key, last_key, subtree_count);
|
|
1316
|
+
chunks.entry(chunk.hash).or_insert(chunk);
|
|
1317
|
+
summary
|
|
1318
|
+
})
|
|
1319
|
+
.collect()
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1207
1322
|
fn build_internal_level(
|
|
1208
1323
|
&self,
|
|
1209
1324
|
children: Vec<ChildSummary>,
|
|
@@ -1234,6 +1349,36 @@ impl TrackedStateTree {
|
|
|
1234
1349
|
.collect()
|
|
1235
1350
|
}
|
|
1236
1351
|
|
|
1352
|
+
fn build_internal_level_from_refs<'a>(
|
|
1353
|
+
&self,
|
|
1354
|
+
children: impl IntoIterator<Item = ChildSummaryRef<'a>>,
|
|
1355
|
+
level: usize,
|
|
1356
|
+
chunks: &mut BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
|
|
1357
|
+
) -> Vec<ChildSummary> {
|
|
1358
|
+
let groups = chunk_internal_entry_refs(children, &self.options, level);
|
|
1359
|
+
groups
|
|
1360
|
+
.into_iter()
|
|
1361
|
+
.map(|group| {
|
|
1362
|
+
let subtree_count = group.children.iter().map(|child| child.subtree_count).sum();
|
|
1363
|
+
let first_key = group
|
|
1364
|
+
.children
|
|
1365
|
+
.first()
|
|
1366
|
+
.map(|child| child.first_key.to_vec())
|
|
1367
|
+
.unwrap_or_default();
|
|
1368
|
+
let last_key = group
|
|
1369
|
+
.children
|
|
1370
|
+
.last()
|
|
1371
|
+
.map(|child| child.last_key.to_vec())
|
|
1372
|
+
.unwrap_or_default();
|
|
1373
|
+
let node = encode_internal_node_refs(&group.children);
|
|
1374
|
+
let (chunk, summary) =
|
|
1375
|
+
child_summary_from_node(node, first_key, last_key, subtree_count);
|
|
1376
|
+
chunks.entry(chunk.hash).or_insert(chunk);
|
|
1377
|
+
summary
|
|
1378
|
+
})
|
|
1379
|
+
.collect()
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1237
1382
|
async fn collect_leaf_entries(
|
|
1238
1383
|
&self,
|
|
1239
1384
|
store: &mut (impl StorageReader + ?Sized),
|
|
@@ -1261,7 +1406,7 @@ impl TrackedStateTree {
|
|
|
1261
1406
|
store: &mut impl StorageReader,
|
|
1262
1407
|
root_id: &TrackedStateRootId,
|
|
1263
1408
|
request: &TrackedStateTreeScanRequest,
|
|
1264
|
-
) -> Result<Vec<(TrackedStateKey,
|
|
1409
|
+
) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
|
|
1265
1410
|
self.scan(store, root_id, request).await
|
|
1266
1411
|
}
|
|
1267
1412
|
|
|
@@ -1271,39 +1416,66 @@ impl TrackedStateTree {
|
|
|
1271
1416
|
hash: [u8; TRACKED_STATE_HASH_BYTES],
|
|
1272
1417
|
request: &'a TrackedStateTreeScanRequest,
|
|
1273
1418
|
ranges: &'a [EncodedScanRange],
|
|
1274
|
-
|
|
1419
|
+
key_decode_hint: Option<ScanKeyDecodeHint<'a>>,
|
|
1420
|
+
rows: &'a mut Vec<(TrackedStateKey, TrackedStateIndexValue)>,
|
|
1275
1421
|
) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
|
|
1276
1422
|
where
|
|
1277
1423
|
S: StorageReader + Send + 'a,
|
|
1278
1424
|
{
|
|
1279
1425
|
Box::pin(async move {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1426
|
+
let bytes = self.load_node_bytes(store, &hash).await?;
|
|
1427
|
+
match decode_node_ref(&bytes)? {
|
|
1428
|
+
DecodedNodeRef::Leaf(leaf) => {
|
|
1429
|
+
for index in 0..leaf.len() {
|
|
1283
1430
|
if scan_limit_reached(request, rows.len()) {
|
|
1284
1431
|
break;
|
|
1285
1432
|
}
|
|
1286
|
-
|
|
1433
|
+
let entry = leaf.entry(index)?.ok_or_else(|| {
|
|
1434
|
+
LixError::new(
|
|
1435
|
+
"LIX_ERROR_UNKNOWN",
|
|
1436
|
+
"tracked-state leaf entry disappeared during scan",
|
|
1437
|
+
)
|
|
1438
|
+
})?;
|
|
1439
|
+
if !encoded_key_in_scan_ranges(entry.key, ranges) {
|
|
1287
1440
|
continue;
|
|
1288
1441
|
}
|
|
1289
|
-
let key =
|
|
1290
|
-
|
|
1442
|
+
let key = match key_decode_hint {
|
|
1443
|
+
Some(hint) => decode_key_with_trusted_prefix(
|
|
1444
|
+
entry.key,
|
|
1445
|
+
hint.schema_key,
|
|
1446
|
+
hint.file_id,
|
|
1447
|
+
hint.prefix_len,
|
|
1448
|
+
)?,
|
|
1449
|
+
None => decode_key(entry.key)?,
|
|
1450
|
+
};
|
|
1451
|
+
if key_decode_hint.is_none() && !key_matches_scan_filters(request, &key) {
|
|
1291
1452
|
continue;
|
|
1292
1453
|
}
|
|
1293
|
-
let value =
|
|
1294
|
-
|
|
1454
|
+
let Some(value) =
|
|
1455
|
+
decode_visible_value(entry.value, request.include_tombstones)?
|
|
1456
|
+
else {
|
|
1457
|
+
continue;
|
|
1458
|
+
};
|
|
1459
|
+
if key_decode_hint.is_some() || request.matches(&key, &value) {
|
|
1295
1460
|
rows.push((key, value));
|
|
1296
1461
|
}
|
|
1297
1462
|
}
|
|
1298
1463
|
}
|
|
1299
|
-
|
|
1464
|
+
DecodedNodeRef::Internal(internal) => {
|
|
1300
1465
|
for child in internal.children() {
|
|
1301
1466
|
if scan_limit_reached(request, rows.len()) {
|
|
1302
1467
|
break;
|
|
1303
1468
|
}
|
|
1304
1469
|
if child_summary_overlaps_scan_ranges(child, ranges) {
|
|
1305
|
-
self.scan_node(
|
|
1306
|
-
|
|
1470
|
+
self.scan_node(
|
|
1471
|
+
store,
|
|
1472
|
+
child.child_hash,
|
|
1473
|
+
request,
|
|
1474
|
+
ranges,
|
|
1475
|
+
key_decode_hint,
|
|
1476
|
+
rows,
|
|
1477
|
+
)
|
|
1478
|
+
.await?;
|
|
1307
1479
|
}
|
|
1308
1480
|
}
|
|
1309
1481
|
}
|
|
@@ -1317,7 +1489,7 @@ impl TrackedStateTree {
|
|
|
1317
1489
|
store: &'a mut S,
|
|
1318
1490
|
hash: [u8; TRACKED_STATE_HASH_BYTES],
|
|
1319
1491
|
encoded_keys: &'a [(usize, Vec<u8>)],
|
|
1320
|
-
values: &'a mut [Option<
|
|
1492
|
+
values: &'a mut [Option<TrackedStateIndexValue>],
|
|
1321
1493
|
) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
|
|
1322
1494
|
where
|
|
1323
1495
|
S: StorageReader + Send + 'a,
|
|
@@ -1327,21 +1499,22 @@ impl TrackedStateTree {
|
|
|
1327
1499
|
return Ok(());
|
|
1328
1500
|
}
|
|
1329
1501
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1502
|
+
let bytes = self.load_node_bytes(store, &hash).await?;
|
|
1503
|
+
match decode_node_ref(&bytes)? {
|
|
1504
|
+
DecodedNodeRef::Leaf(leaf) => {
|
|
1332
1505
|
for (original_index, encoded_key) in encoded_keys {
|
|
1333
|
-
let Some(entry_index) = leaf
|
|
1334
|
-
.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1506
|
+
if let Some(entry_index) = binary_search_leaf_key(&leaf, encoded_key)? {
|
|
1507
|
+
let entry = leaf.entry(entry_index)?.ok_or_else(|| {
|
|
1508
|
+
LixError::new(
|
|
1509
|
+
"LIX_ERROR_UNKNOWN",
|
|
1510
|
+
"tracked-state leaf entry disappeared during get_many",
|
|
1511
|
+
)
|
|
1512
|
+
})?;
|
|
1513
|
+
values[*original_index] = Some(decode_value(entry.value)?);
|
|
1514
|
+
}
|
|
1342
1515
|
}
|
|
1343
1516
|
}
|
|
1344
|
-
|
|
1517
|
+
DecodedNodeRef::Internal(internal) => {
|
|
1345
1518
|
let mut start = 0usize;
|
|
1346
1519
|
let children = internal.children();
|
|
1347
1520
|
for (child_index, child) in children.iter().enumerate() {
|
|
@@ -1540,11 +1713,20 @@ impl TrackedStateTree {
|
|
|
1540
1713
|
store: &mut (impl StorageReader + ?Sized),
|
|
1541
1714
|
hash: &[u8; TRACKED_STATE_HASH_BYTES],
|
|
1542
1715
|
) -> Result<DecodedNode, LixError> {
|
|
1716
|
+
let bytes = self.load_node_bytes(store, hash).await?;
|
|
1717
|
+
decode_node(&bytes)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
async fn load_node_bytes(
|
|
1721
|
+
&self,
|
|
1722
|
+
store: &mut (impl StorageReader + ?Sized),
|
|
1723
|
+
hash: &[u8; TRACKED_STATE_HASH_BYTES],
|
|
1724
|
+
) -> Result<Vec<u8>, LixError> {
|
|
1543
1725
|
let bytes = storage::read_chunk(store, hash).await?.ok_or_else(|| {
|
|
1544
1726
|
LixError::new("LIX_ERROR_UNKNOWN", "tracked-state tree chunk is missing")
|
|
1545
1727
|
})?;
|
|
1546
1728
|
storage::verify_chunk_hash(hash, &bytes)?;
|
|
1547
|
-
|
|
1729
|
+
Ok(bytes)
|
|
1548
1730
|
}
|
|
1549
1731
|
|
|
1550
1732
|
async fn load_node_with_overlay(
|
|
@@ -1587,6 +1769,36 @@ struct EncodedScanRange {
|
|
|
1587
1769
|
end: Option<Vec<u8>>,
|
|
1588
1770
|
}
|
|
1589
1771
|
|
|
1772
|
+
#[derive(Debug, Clone, Copy)]
|
|
1773
|
+
struct ScanKeyDecodeHint<'a> {
|
|
1774
|
+
schema_key: &'a str,
|
|
1775
|
+
file_id: Option<&'a str>,
|
|
1776
|
+
prefix_len: usize,
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
fn binary_search_leaf_key(
|
|
1780
|
+
leaf: &DecodedLeafNodeRef<'_>,
|
|
1781
|
+
encoded_key: &[u8],
|
|
1782
|
+
) -> Result<Option<usize>, LixError> {
|
|
1783
|
+
let mut low = 0usize;
|
|
1784
|
+
let mut high = leaf.len();
|
|
1785
|
+
while low < high {
|
|
1786
|
+
let mid = low + (high - low) / 2;
|
|
1787
|
+
let key = leaf.key(mid)?.ok_or_else(|| {
|
|
1788
|
+
LixError::new(
|
|
1789
|
+
"LIX_ERROR_UNKNOWN",
|
|
1790
|
+
"tracked-state leaf key disappeared during binary search",
|
|
1791
|
+
)
|
|
1792
|
+
})?;
|
|
1793
|
+
match key.cmp(encoded_key) {
|
|
1794
|
+
std::cmp::Ordering::Less => low = mid + 1,
|
|
1795
|
+
std::cmp::Ordering::Equal => return Ok(Some(mid)),
|
|
1796
|
+
std::cmp::Ordering::Greater => high = mid,
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
Ok(None)
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1590
1802
|
struct LeafSummaryCursor {
|
|
1591
1803
|
stack: Vec<LeafSummaryCursorFrame>,
|
|
1592
1804
|
current: Option<ChildSummary>,
|
|
@@ -1702,6 +1914,13 @@ struct LeafChunkAccumulator {
|
|
|
1702
1914
|
value_bytes: usize,
|
|
1703
1915
|
}
|
|
1704
1916
|
|
|
1917
|
+
#[derive(Debug, Default)]
|
|
1918
|
+
struct LeafChunkRefAccumulator<'a> {
|
|
1919
|
+
entries: Vec<EncodedLeafEntryRef<'a>>,
|
|
1920
|
+
key_bytes: usize,
|
|
1921
|
+
value_bytes: usize,
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1705
1924
|
#[derive(Debug, Default)]
|
|
1706
1925
|
struct InternalChunkAccumulator {
|
|
1707
1926
|
children: Vec<ChildSummary>,
|
|
@@ -1709,6 +1928,13 @@ struct InternalChunkAccumulator {
|
|
|
1709
1928
|
last_key_bytes: usize,
|
|
1710
1929
|
}
|
|
1711
1930
|
|
|
1931
|
+
#[derive(Debug, Default)]
|
|
1932
|
+
struct InternalChunkRefAccumulator<'a> {
|
|
1933
|
+
children: Vec<ChildSummaryRef<'a>>,
|
|
1934
|
+
first_key_bytes: usize,
|
|
1935
|
+
last_key_bytes: usize,
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1712
1938
|
fn chunk_leaf_entries(
|
|
1713
1939
|
entries: Vec<EncodedLeafEntry>,
|
|
1714
1940
|
options: &TrackedStateTreeOptions,
|
|
@@ -1719,7 +1945,7 @@ fn chunk_leaf_entries(
|
|
|
1719
1945
|
let mut groups = Vec::new();
|
|
1720
1946
|
let mut current = LeafChunkAccumulator::default();
|
|
1721
1947
|
for entry in entries {
|
|
1722
|
-
let item_size = entry.key.len()
|
|
1948
|
+
let item_size = estimate_leaf_entry_size(entry.key.len(), entry.value.len());
|
|
1723
1949
|
let projected_size = estimate_leaf_chunk_size(
|
|
1724
1950
|
current.entries.len() + 1,
|
|
1725
1951
|
current.key_bytes + entry.key.len(),
|
|
@@ -1758,6 +1984,56 @@ fn chunk_leaf_entries(
|
|
|
1758
1984
|
groups
|
|
1759
1985
|
}
|
|
1760
1986
|
|
|
1987
|
+
fn chunk_leaf_entry_refs<'a>(
|
|
1988
|
+
entries: impl IntoIterator<Item = EncodedLeafEntryRef<'a>>,
|
|
1989
|
+
options: &TrackedStateTreeOptions,
|
|
1990
|
+
) -> Vec<LeafChunkRefAccumulator<'a>> {
|
|
1991
|
+
let mut iter = entries.into_iter().peekable();
|
|
1992
|
+
if iter.peek().is_none() {
|
|
1993
|
+
return vec![LeafChunkRefAccumulator::default()];
|
|
1994
|
+
}
|
|
1995
|
+
let mut groups = Vec::new();
|
|
1996
|
+
let mut current = LeafChunkRefAccumulator::default();
|
|
1997
|
+
for entry in iter {
|
|
1998
|
+
let item_size = estimate_leaf_entry_size(entry.key.len(), entry.value.len());
|
|
1999
|
+
let projected_size = estimate_leaf_chunk_size(
|
|
2000
|
+
current.entries.len() + 1,
|
|
2001
|
+
current.key_bytes + entry.key.len(),
|
|
2002
|
+
current.value_bytes + entry.value.len(),
|
|
2003
|
+
);
|
|
2004
|
+
if !current.entries.is_empty() && projected_size > options.max_chunk_bytes {
|
|
2005
|
+
groups.push(std::mem::take(&mut current));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
current.key_bytes += entry.key.len();
|
|
2009
|
+
current.value_bytes += entry.value.len();
|
|
2010
|
+
current.entries.push(entry);
|
|
2011
|
+
let current_size = estimate_leaf_chunk_size(
|
|
2012
|
+
current.entries.len(),
|
|
2013
|
+
current.key_bytes,
|
|
2014
|
+
current.value_bytes,
|
|
2015
|
+
);
|
|
2016
|
+
if current_size >= options.min_chunk_bytes
|
|
2017
|
+
&& (current_size >= options.max_chunk_bytes
|
|
2018
|
+
|| current.entries.last().is_some_and(|entry| {
|
|
2019
|
+
boundary_trigger(
|
|
2020
|
+
entry.key,
|
|
2021
|
+
0,
|
|
2022
|
+
current_size,
|
|
2023
|
+
item_size,
|
|
2024
|
+
options.target_chunk_bytes,
|
|
2025
|
+
)
|
|
2026
|
+
}))
|
|
2027
|
+
{
|
|
2028
|
+
groups.push(std::mem::take(&mut current));
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if !current.entries.is_empty() {
|
|
2032
|
+
groups.push(current);
|
|
2033
|
+
}
|
|
2034
|
+
groups
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1761
2037
|
fn chunk_internal_entries(
|
|
1762
2038
|
children: Vec<ChildSummary>,
|
|
1763
2039
|
options: &TrackedStateTreeOptions,
|
|
@@ -1808,8 +2084,62 @@ fn chunk_internal_entries(
|
|
|
1808
2084
|
groups
|
|
1809
2085
|
}
|
|
1810
2086
|
|
|
2087
|
+
fn chunk_internal_entry_refs<'a>(
|
|
2088
|
+
children: impl IntoIterator<Item = ChildSummaryRef<'a>>,
|
|
2089
|
+
options: &TrackedStateTreeOptions,
|
|
2090
|
+
level: usize,
|
|
2091
|
+
) -> Vec<InternalChunkRefAccumulator<'a>> {
|
|
2092
|
+
let mut groups = Vec::new();
|
|
2093
|
+
let mut current = InternalChunkRefAccumulator::default();
|
|
2094
|
+
for child in children {
|
|
2095
|
+
let item_size = child.first_key.len()
|
|
2096
|
+
+ child.last_key.len()
|
|
2097
|
+
+ TRACKED_STATE_HASH_BYTES
|
|
2098
|
+
+ std::mem::size_of::<u64>();
|
|
2099
|
+
let projected_size = estimate_internal_chunk_size(
|
|
2100
|
+
current.children.len() + 1,
|
|
2101
|
+
current.first_key_bytes + child.first_key.len(),
|
|
2102
|
+
current.last_key_bytes + child.last_key.len(),
|
|
2103
|
+
);
|
|
2104
|
+
if !current.children.is_empty() && projected_size > options.max_chunk_bytes {
|
|
2105
|
+
groups.push(std::mem::take(&mut current));
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
current.first_key_bytes += child.first_key.len();
|
|
2109
|
+
current.last_key_bytes += child.last_key.len();
|
|
2110
|
+
current.children.push(child);
|
|
2111
|
+
let current_size = estimate_internal_chunk_size(
|
|
2112
|
+
current.children.len(),
|
|
2113
|
+
current.first_key_bytes,
|
|
2114
|
+
current.last_key_bytes,
|
|
2115
|
+
);
|
|
2116
|
+
if current_size >= options.min_chunk_bytes
|
|
2117
|
+
&& (current_size >= options.max_chunk_bytes
|
|
2118
|
+
|| current.children.last().is_some_and(|child| {
|
|
2119
|
+
boundary_trigger(
|
|
2120
|
+
child.first_key,
|
|
2121
|
+
level,
|
|
2122
|
+
current_size,
|
|
2123
|
+
item_size,
|
|
2124
|
+
options.target_chunk_bytes,
|
|
2125
|
+
)
|
|
2126
|
+
}))
|
|
2127
|
+
{
|
|
2128
|
+
groups.push(std::mem::take(&mut current));
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if !current.children.is_empty() {
|
|
2132
|
+
groups.push(current);
|
|
2133
|
+
}
|
|
2134
|
+
groups
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1811
2137
|
fn estimate_leaf_chunk_size(entry_count: usize, key_bytes: usize, value_bytes: usize) -> usize {
|
|
1812
|
-
|
|
2138
|
+
10 + entry_count * 12 + key_bytes + value_bytes
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
fn estimate_leaf_entry_size(key_bytes: usize, value_bytes: usize) -> usize {
|
|
2142
|
+
12 + key_bytes + value_bytes
|
|
1813
2143
|
}
|
|
1814
2144
|
|
|
1815
2145
|
fn estimate_internal_chunk_size(
|
|
@@ -1863,7 +2193,7 @@ async fn child_summaries_are_leaves(
|
|
|
1863
2193
|
|
|
1864
2194
|
fn decode_entry(
|
|
1865
2195
|
entry: &EncodedLeafEntry,
|
|
1866
|
-
) -> Result<(TrackedStateKey,
|
|
2196
|
+
) -> Result<(TrackedStateKey, TrackedStateIndexValue), LixError> {
|
|
1867
2197
|
Ok((decode_key(&entry.key)?, decode_value(&entry.value)?))
|
|
1868
2198
|
}
|
|
1869
2199
|
|
|
@@ -2024,6 +2354,28 @@ fn scan_ranges(request: &TrackedStateTreeScanRequest) -> Vec<EncodedScanRange> {
|
|
|
2024
2354
|
ranges
|
|
2025
2355
|
}
|
|
2026
2356
|
|
|
2357
|
+
fn scan_key_decode_hint<'a>(
|
|
2358
|
+
request: &'a TrackedStateTreeScanRequest,
|
|
2359
|
+
ranges: &[EncodedScanRange],
|
|
2360
|
+
) -> Option<ScanKeyDecodeHint<'a>> {
|
|
2361
|
+
if ranges.len() != 1 || request.schema_keys.len() != 1 || request.file_ids.len() != 1 {
|
|
2362
|
+
return None;
|
|
2363
|
+
}
|
|
2364
|
+
if !request.entity_ids.is_empty() {
|
|
2365
|
+
return None;
|
|
2366
|
+
}
|
|
2367
|
+
let file_id = match request.file_ids.first()? {
|
|
2368
|
+
NullableKeyFilter::Null => None,
|
|
2369
|
+
NullableKeyFilter::Value(file_id) => Some(file_id.as_str()),
|
|
2370
|
+
NullableKeyFilter::Any => return None,
|
|
2371
|
+
};
|
|
2372
|
+
Some(ScanKeyDecodeHint {
|
|
2373
|
+
schema_key: request.schema_keys.first()?.as_str(),
|
|
2374
|
+
file_id,
|
|
2375
|
+
prefix_len: ranges.first()?.start.len(),
|
|
2376
|
+
})
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2027
2379
|
fn prefix_scan_range(prefix: Vec<u8>) -> EncodedScanRange {
|
|
2028
2380
|
EncodedScanRange {
|
|
2029
2381
|
end: lexicographic_successor(&prefix),
|
|
@@ -2113,6 +2465,7 @@ mod tests {
|
|
|
2113
2465
|
use crate::backend::testing::UnitTestBackend;
|
|
2114
2466
|
use crate::entity_identity::EntityIdentity;
|
|
2115
2467
|
use crate::storage::{StorageContext, StorageWriteTransaction};
|
|
2468
|
+
use crate::tracked_state::codec::encode_value;
|
|
2116
2469
|
|
|
2117
2470
|
#[tokio::test]
|
|
2118
2471
|
async fn exact_read_roundtrips_from_stored_root() {
|
|
@@ -2129,7 +2482,7 @@ mod tests {
|
|
|
2129
2482
|
&tree,
|
|
2130
2483
|
transaction.as_mut(),
|
|
2131
2484
|
None,
|
|
2132
|
-
vec![
|
|
2485
|
+
vec![mutation(&key, &value)],
|
|
2133
2486
|
Some("commit-1"),
|
|
2134
2487
|
)
|
|
2135
2488
|
.await
|
|
@@ -2159,6 +2512,8 @@ mod tests {
|
|
|
2159
2512
|
let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
|
|
2160
2513
|
let tree = TrackedStateTree::new();
|
|
2161
2514
|
let key = key("schema", None, "entity");
|
|
2515
|
+
let old_value = value("change-old", Some("{\"v\":1}"));
|
|
2516
|
+
let new_value = value("change-new", Some("{\"v\":2}"));
|
|
2162
2517
|
|
|
2163
2518
|
let mut transaction = storage
|
|
2164
2519
|
.begin_write_transaction()
|
|
@@ -2168,10 +2523,7 @@ mod tests {
|
|
|
2168
2523
|
&tree,
|
|
2169
2524
|
transaction.as_mut(),
|
|
2170
2525
|
None,
|
|
2171
|
-
vec![
|
|
2172
|
-
TrackedStateMutation::put(key.clone(), value("change-old", Some("{\"v\":1}"))),
|
|
2173
|
-
TrackedStateMutation::put(key.clone(), value("change-new", Some("{\"v\":2}"))),
|
|
2174
|
-
],
|
|
2526
|
+
vec![mutation(&key, &old_value), mutation(&key, &new_value)],
|
|
2175
2527
|
None,
|
|
2176
2528
|
)
|
|
2177
2529
|
.await
|
|
@@ -2187,15 +2539,12 @@ mod tests {
|
|
|
2187
2539
|
.await
|
|
2188
2540
|
.expect("row should load")
|
|
2189
2541
|
.expect("row should exist");
|
|
2190
|
-
assert_eq!(loaded.change_id, "change-new");
|
|
2191
|
-
assert_eq!(
|
|
2192
|
-
loaded.snapshot_ref,
|
|
2193
|
-
Some(crate::json_store::JsonRef::from_hash_bytes([2; 32]))
|
|
2194
|
-
);
|
|
2542
|
+
assert_eq!(loaded.change_locator.change_id, "change-new");
|
|
2543
|
+
assert_eq!(loaded.change_locator.source_commit_id, "commit");
|
|
2195
2544
|
}
|
|
2196
2545
|
|
|
2197
2546
|
#[tokio::test]
|
|
2198
|
-
async fn
|
|
2547
|
+
async fn scan_filters_by_index_key_without_materializing_tombstones() {
|
|
2199
2548
|
let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
|
|
2200
2549
|
let tree = TrackedStateTree::new();
|
|
2201
2550
|
|
|
@@ -2208,12 +2557,9 @@ mod tests {
|
|
|
2208
2557
|
transaction.as_mut(),
|
|
2209
2558
|
None,
|
|
2210
2559
|
vec![
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
),
|
|
2215
|
-
TrackedStateMutation::put(key("schema-a", None, "deleted"), value("c2", None)),
|
|
2216
|
-
TrackedStateMutation::put(key("schema-b", None, "other"), value("c3", Some("{}"))),
|
|
2560
|
+
mutation_owned(key("schema-a", None, "visible"), value("c1", Some("{}"))),
|
|
2561
|
+
mutation_owned(key("schema-a", None, "deleted"), value("c2", None)),
|
|
2562
|
+
mutation_owned(key("schema-b", None, "other"), value("c3", Some("{}"))),
|
|
2217
2563
|
],
|
|
2218
2564
|
None,
|
|
2219
2565
|
)
|
|
@@ -2236,11 +2582,30 @@ mod tests {
|
|
|
2236
2582
|
)
|
|
2237
2583
|
.await
|
|
2238
2584
|
.expect("scan should succeed");
|
|
2239
|
-
assert_eq!(rows.len(),
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
"
|
|
2243
|
-
|
|
2585
|
+
assert_eq!(rows.len(), 2);
|
|
2586
|
+
let identities = rows
|
|
2587
|
+
.iter()
|
|
2588
|
+
.map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
|
|
2589
|
+
.collect::<Vec<_>>();
|
|
2590
|
+
assert_eq!(identities, vec!["deleted", "visible"]);
|
|
2591
|
+
|
|
2592
|
+
let live_rows = tree
|
|
2593
|
+
.scan(
|
|
2594
|
+
&mut store,
|
|
2595
|
+
&result.root_id,
|
|
2596
|
+
&TrackedStateTreeScanRequest {
|
|
2597
|
+
schema_keys: vec!["schema-a".to_string()],
|
|
2598
|
+
include_tombstones: false,
|
|
2599
|
+
..Default::default()
|
|
2600
|
+
},
|
|
2601
|
+
)
|
|
2602
|
+
.await
|
|
2603
|
+
.expect("live scan should succeed");
|
|
2604
|
+
let live_identities = live_rows
|
|
2605
|
+
.iter()
|
|
2606
|
+
.map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
|
|
2607
|
+
.collect::<Vec<_>>();
|
|
2608
|
+
assert_eq!(live_identities, vec!["visible"]);
|
|
2244
2609
|
}
|
|
2245
2610
|
|
|
2246
2611
|
#[tokio::test]
|
|
@@ -2257,19 +2622,19 @@ mod tests {
|
|
|
2257
2622
|
transaction.as_mut(),
|
|
2258
2623
|
None,
|
|
2259
2624
|
vec![
|
|
2260
|
-
|
|
2625
|
+
mutation_owned(
|
|
2261
2626
|
key("schema-a", Some("file-a"), "entity-a"),
|
|
2262
2627
|
value("c1", Some("{}")),
|
|
2263
2628
|
),
|
|
2264
|
-
|
|
2629
|
+
mutation_owned(
|
|
2265
2630
|
key("schema-a", Some("file-b"), "entity-a"),
|
|
2266
2631
|
value("c2", Some("{}")),
|
|
2267
2632
|
),
|
|
2268
|
-
|
|
2633
|
+
mutation_owned(
|
|
2269
2634
|
key("schema-a", Some("file-a"), "entity-b"),
|
|
2270
2635
|
value("c3", Some("{}")),
|
|
2271
2636
|
),
|
|
2272
|
-
|
|
2637
|
+
mutation_owned(
|
|
2273
2638
|
key("schema-b", Some("file-a"), "entity-a"),
|
|
2274
2639
|
value("c4", Some("{}")),
|
|
2275
2640
|
),
|
|
@@ -2301,18 +2666,93 @@ mod tests {
|
|
|
2301
2666
|
assert_eq!(rows.len(), 1);
|
|
2302
2667
|
assert_eq!(rows[0].0.schema_key, "schema-a");
|
|
2303
2668
|
assert_eq!(
|
|
2304
|
-
rows[0]
|
|
2669
|
+
rows[0]
|
|
2670
|
+
.0
|
|
2671
|
+
.entity_id
|
|
2672
|
+
.as_single_string_owned()
|
|
2673
|
+
.expect("identity"),
|
|
2305
2674
|
"entity-a"
|
|
2306
2675
|
);
|
|
2307
2676
|
assert_eq!(rows[0].0.file_id.as_deref(), Some("file-a"));
|
|
2308
2677
|
}
|
|
2309
2678
|
|
|
2679
|
+
#[tokio::test]
|
|
2680
|
+
async fn scan_schema_file_prefix_honors_tombstones_and_limit() {
|
|
2681
|
+
let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
|
|
2682
|
+
let tree = TrackedStateTree::new();
|
|
2683
|
+
|
|
2684
|
+
let mut transaction = storage
|
|
2685
|
+
.begin_write_transaction()
|
|
2686
|
+
.await
|
|
2687
|
+
.expect("transaction should open");
|
|
2688
|
+
let result = apply_mutations_for_test(
|
|
2689
|
+
&tree,
|
|
2690
|
+
transaction.as_mut(),
|
|
2691
|
+
None,
|
|
2692
|
+
vec![
|
|
2693
|
+
mutation_owned(
|
|
2694
|
+
key("schema-a", Some("file-a"), "entity-a"),
|
|
2695
|
+
value("c1", Some("{}")),
|
|
2696
|
+
),
|
|
2697
|
+
mutation_owned(
|
|
2698
|
+
key("schema-a", Some("file-a"), "entity-b"),
|
|
2699
|
+
value("c2", None),
|
|
2700
|
+
),
|
|
2701
|
+
mutation_owned(
|
|
2702
|
+
key("schema-a", Some("file-a"), "entity-c"),
|
|
2703
|
+
value("c3", Some("{}")),
|
|
2704
|
+
),
|
|
2705
|
+
mutation_owned(
|
|
2706
|
+
key("schema-a", Some("file-b"), "entity-d"),
|
|
2707
|
+
value("c4", Some("{}")),
|
|
2708
|
+
),
|
|
2709
|
+
],
|
|
2710
|
+
None,
|
|
2711
|
+
)
|
|
2712
|
+
.await
|
|
2713
|
+
.expect("mutations should apply");
|
|
2714
|
+
transaction
|
|
2715
|
+
.commit()
|
|
2716
|
+
.await
|
|
2717
|
+
.expect("transaction should commit");
|
|
2718
|
+
|
|
2719
|
+
let mut store = storage.clone();
|
|
2720
|
+
let rows = tree
|
|
2721
|
+
.scan(
|
|
2722
|
+
&mut store,
|
|
2723
|
+
&result.root_id,
|
|
2724
|
+
&TrackedStateTreeScanRequest {
|
|
2725
|
+
schema_keys: vec!["schema-a".to_string()],
|
|
2726
|
+
file_ids: vec![crate::NullableKeyFilter::Value("file-a".to_string())],
|
|
2727
|
+
include_tombstones: false,
|
|
2728
|
+
limit: Some(2),
|
|
2729
|
+
..Default::default()
|
|
2730
|
+
},
|
|
2731
|
+
)
|
|
2732
|
+
.await
|
|
2733
|
+
.expect("scan should succeed");
|
|
2734
|
+
|
|
2735
|
+
assert_eq!(rows.len(), 2);
|
|
2736
|
+
assert!(rows.iter().all(
|
|
2737
|
+
|(key, _)| key.schema_key == "schema-a" && key.file_id.as_deref() == Some("file-a")
|
|
2738
|
+
));
|
|
2739
|
+
assert_eq!(
|
|
2740
|
+
rows.iter()
|
|
2741
|
+
.map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
|
|
2742
|
+
.collect::<Vec<_>>(),
|
|
2743
|
+
vec!["entity-a", "entity-c"]
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2310
2747
|
#[tokio::test]
|
|
2311
2748
|
async fn applying_to_base_root_reuses_existing_rows_and_overwrites_changed_rows() {
|
|
2312
2749
|
let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
|
|
2313
2750
|
let tree = TrackedStateTree::new();
|
|
2314
2751
|
let unchanged_key = key("schema", None, "unchanged");
|
|
2315
2752
|
let changed_key = key("schema", None, "changed");
|
|
2753
|
+
let unchanged_value = value("c1", Some("{}"));
|
|
2754
|
+
let old_changed_value = value("c2", Some("{\"old\":true}"));
|
|
2755
|
+
let new_changed_value = value("c3", Some("{\"new\":true}"));
|
|
2316
2756
|
|
|
2317
2757
|
let mut transaction = storage
|
|
2318
2758
|
.begin_write_transaction()
|
|
@@ -2323,8 +2763,8 @@ mod tests {
|
|
|
2323
2763
|
transaction.as_mut(),
|
|
2324
2764
|
None,
|
|
2325
2765
|
vec![
|
|
2326
|
-
|
|
2327
|
-
|
|
2766
|
+
mutation(&unchanged_key, &unchanged_value),
|
|
2767
|
+
mutation(&changed_key, &old_changed_value),
|
|
2328
2768
|
],
|
|
2329
2769
|
None,
|
|
2330
2770
|
)
|
|
@@ -2334,10 +2774,7 @@ mod tests {
|
|
|
2334
2774
|
&tree,
|
|
2335
2775
|
transaction.as_mut(),
|
|
2336
2776
|
Some(&base.root_id),
|
|
2337
|
-
vec![
|
|
2338
|
-
changed_key.clone(),
|
|
2339
|
-
value("c3", Some("{\"new\":true}")),
|
|
2340
|
-
)],
|
|
2777
|
+
vec![mutation(&changed_key, &new_changed_value)],
|
|
2341
2778
|
None,
|
|
2342
2779
|
)
|
|
2343
2780
|
.await
|
|
@@ -2353,6 +2790,7 @@ mod tests {
|
|
|
2353
2790
|
.await
|
|
2354
2791
|
.expect("unchanged read")
|
|
2355
2792
|
.expect("unchanged exists")
|
|
2793
|
+
.change_locator
|
|
2356
2794
|
.change_id,
|
|
2357
2795
|
"c1"
|
|
2358
2796
|
);
|
|
@@ -2361,6 +2799,7 @@ mod tests {
|
|
|
2361
2799
|
.await
|
|
2362
2800
|
.expect("changed read")
|
|
2363
2801
|
.expect("changed exists")
|
|
2802
|
+
.change_locator
|
|
2364
2803
|
.change_id,
|
|
2365
2804
|
"c3"
|
|
2366
2805
|
);
|
|
@@ -2373,6 +2812,9 @@ mod tests {
|
|
|
2373
2812
|
let shared_key = key("schema", None, "shared");
|
|
2374
2813
|
let branch_a_key = key("schema", None, "branch-a");
|
|
2375
2814
|
let branch_b_key = key("schema", None, "branch-b");
|
|
2815
|
+
let shared_value = value("shared-change", Some("{\"shared\":true}"));
|
|
2816
|
+
let branch_a_value = value("branch-a-change", Some("{\"branch\":\"a\"}"));
|
|
2817
|
+
let branch_b_value = value("branch-b-change", Some("{\"branch\":\"b\"}"));
|
|
2376
2818
|
|
|
2377
2819
|
let mut transaction = storage
|
|
2378
2820
|
.begin_write_transaction()
|
|
@@ -2382,10 +2824,7 @@ mod tests {
|
|
|
2382
2824
|
&tree,
|
|
2383
2825
|
transaction.as_mut(),
|
|
2384
2826
|
None,
|
|
2385
|
-
vec![
|
|
2386
|
-
shared_key.clone(),
|
|
2387
|
-
value("shared-change", Some("{\"shared\":true}")),
|
|
2388
|
-
)],
|
|
2827
|
+
vec![mutation(&shared_key, &shared_value)],
|
|
2389
2828
|
Some("commit-base"),
|
|
2390
2829
|
)
|
|
2391
2830
|
.await
|
|
@@ -2394,10 +2833,7 @@ mod tests {
|
|
|
2394
2833
|
&tree,
|
|
2395
2834
|
transaction.as_mut(),
|
|
2396
2835
|
Some(&base.root_id),
|
|
2397
|
-
vec![
|
|
2398
|
-
branch_a_key.clone(),
|
|
2399
|
-
value("branch-a-change", Some("{\"branch\":\"a\"}")),
|
|
2400
|
-
)],
|
|
2836
|
+
vec![mutation(&branch_a_key, &branch_a_value)],
|
|
2401
2837
|
Some("commit-a"),
|
|
2402
2838
|
)
|
|
2403
2839
|
.await
|
|
@@ -2406,10 +2842,7 @@ mod tests {
|
|
|
2406
2842
|
&tree,
|
|
2407
2843
|
transaction.as_mut(),
|
|
2408
2844
|
Some(&base.root_id),
|
|
2409
|
-
vec![
|
|
2410
|
-
branch_b_key.clone(),
|
|
2411
|
-
value("branch-b-change", Some("{\"branch\":\"b\"}")),
|
|
2412
|
-
)],
|
|
2845
|
+
vec![mutation(&branch_b_key, &branch_b_value)],
|
|
2413
2846
|
Some("commit-b"),
|
|
2414
2847
|
)
|
|
2415
2848
|
.await
|
|
@@ -2455,7 +2888,7 @@ mod tests {
|
|
|
2455
2888
|
});
|
|
2456
2889
|
let rows = (0..100)
|
|
2457
2890
|
.map(|index| {
|
|
2458
|
-
|
|
2891
|
+
mutation_owned(
|
|
2459
2892
|
key("schema", None, &format!("entity-{index:03}")),
|
|
2460
2893
|
value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
|
|
2461
2894
|
)
|
|
@@ -2475,10 +2908,7 @@ mod tests {
|
|
|
2475
2908
|
&tree,
|
|
2476
2909
|
transaction.as_mut(),
|
|
2477
2910
|
Some(&base.root_id),
|
|
2478
|
-
vec![
|
|
2479
|
-
changed_key.clone(),
|
|
2480
|
-
changed_value.clone(),
|
|
2481
|
-
)],
|
|
2911
|
+
vec![mutation(&changed_key, &changed_value)],
|
|
2482
2912
|
None,
|
|
2483
2913
|
)
|
|
2484
2914
|
.await
|
|
@@ -2513,7 +2943,7 @@ mod tests {
|
|
|
2513
2943
|
});
|
|
2514
2944
|
let rows = (0..100)
|
|
2515
2945
|
.map(|index| {
|
|
2516
|
-
|
|
2946
|
+
mutation_owned(
|
|
2517
2947
|
key("schema", None, &format!("entity-{index:03}")),
|
|
2518
2948
|
value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
|
|
2519
2949
|
)
|
|
@@ -2533,10 +2963,7 @@ mod tests {
|
|
|
2533
2963
|
&tree,
|
|
2534
2964
|
transaction.as_mut(),
|
|
2535
2965
|
Some(&base.root_id),
|
|
2536
|
-
vec![
|
|
2537
|
-
inserted_key.clone(),
|
|
2538
|
-
inserted_value.clone(),
|
|
2539
|
-
)],
|
|
2966
|
+
vec![mutation(&inserted_key, &inserted_value)],
|
|
2540
2967
|
None,
|
|
2541
2968
|
)
|
|
2542
2969
|
.await
|
|
@@ -2574,7 +3001,7 @@ mod tests {
|
|
|
2574
3001
|
});
|
|
2575
3002
|
let rows = (0..100)
|
|
2576
3003
|
.map(|index| {
|
|
2577
|
-
|
|
3004
|
+
mutation_owned(
|
|
2578
3005
|
key("schema", None, &format!("entity-{index:03}")),
|
|
2579
3006
|
value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
|
|
2580
3007
|
)
|
|
@@ -2582,7 +3009,7 @@ mod tests {
|
|
|
2582
3009
|
.collect::<Vec<_>>();
|
|
2583
3010
|
let updates = (10..25)
|
|
2584
3011
|
.map(|index| {
|
|
2585
|
-
|
|
3012
|
+
(
|
|
2586
3013
|
key("schema", None, &format!("entity-{index:03}")),
|
|
2587
3014
|
value(
|
|
2588
3015
|
&format!("changed-{index}"),
|
|
@@ -2603,7 +3030,10 @@ mod tests {
|
|
|
2603
3030
|
&tree,
|
|
2604
3031
|
transaction.as_mut(),
|
|
2605
3032
|
Some(&base.root_id),
|
|
2606
|
-
updates
|
|
3033
|
+
updates
|
|
3034
|
+
.iter()
|
|
3035
|
+
.map(|(key, value)| mutation(key, value))
|
|
3036
|
+
.collect(),
|
|
2607
3037
|
None,
|
|
2608
3038
|
)
|
|
2609
3039
|
.await
|
|
@@ -2612,8 +3042,7 @@ mod tests {
|
|
|
2612
3042
|
.collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
|
|
2613
3043
|
.await
|
|
2614
3044
|
.expect("base entries should collect");
|
|
2615
|
-
for
|
|
2616
|
-
let TrackedStateMutation::Put { key, value } = update;
|
|
3045
|
+
for (key, value) in updates {
|
|
2617
3046
|
let encoded_key = encode_key(&key);
|
|
2618
3047
|
let encoded_value = encode_value(&value);
|
|
2619
3048
|
let index = canonical_entries
|
|
@@ -2638,7 +3067,7 @@ mod tests {
|
|
|
2638
3067
|
});
|
|
2639
3068
|
let rows = (0..100)
|
|
2640
3069
|
.map(|index| {
|
|
2641
|
-
|
|
3070
|
+
mutation_owned(
|
|
2642
3071
|
key("schema", None, &format!("entity-{index:03}")),
|
|
2643
3072
|
value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
|
|
2644
3073
|
)
|
|
@@ -2648,7 +3077,7 @@ mod tests {
|
|
|
2648
3077
|
.into_iter()
|
|
2649
3078
|
.enumerate()
|
|
2650
3079
|
.map(|(index, entity_id)| {
|
|
2651
|
-
|
|
3080
|
+
(
|
|
2652
3081
|
key("schema", None, entity_id),
|
|
2653
3082
|
value(
|
|
2654
3083
|
&format!("inserted-{index}"),
|
|
@@ -2669,7 +3098,10 @@ mod tests {
|
|
|
2669
3098
|
&tree,
|
|
2670
3099
|
transaction.as_mut(),
|
|
2671
3100
|
Some(&base.root_id),
|
|
2672
|
-
inserts
|
|
3101
|
+
inserts
|
|
3102
|
+
.iter()
|
|
3103
|
+
.map(|(key, value)| mutation(key, value))
|
|
3104
|
+
.collect(),
|
|
2673
3105
|
None,
|
|
2674
3106
|
)
|
|
2675
3107
|
.await
|
|
@@ -2678,8 +3110,7 @@ mod tests {
|
|
|
2678
3110
|
.collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
|
|
2679
3111
|
.await
|
|
2680
3112
|
.expect("base entries should collect");
|
|
2681
|
-
for
|
|
2682
|
-
let TrackedStateMutation::Put { key, value } = insert;
|
|
3113
|
+
for (key, value) in inserts {
|
|
2683
3114
|
let encoded_key = encode_key(&key);
|
|
2684
3115
|
let encoded_value = encode_value(&value);
|
|
2685
3116
|
let index = canonical_entries
|
|
@@ -2715,6 +3146,14 @@ mod tests {
|
|
|
2715
3146
|
Ok(result)
|
|
2716
3147
|
}
|
|
2717
3148
|
|
|
3149
|
+
fn mutation(key: &TrackedStateKey, value: &TrackedStateIndexValue) -> TrackedStateMutation {
|
|
3150
|
+
TrackedStateMutation::put_encoded(encode_key(key), encode_value(value))
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
fn mutation_owned(key: TrackedStateKey, value: TrackedStateIndexValue) -> TrackedStateMutation {
|
|
3154
|
+
mutation(&key, &value)
|
|
3155
|
+
}
|
|
3156
|
+
|
|
2718
3157
|
fn key(schema_key: &str, file_id: Option<&str>, entity_id: &str) -> TrackedStateKey {
|
|
2719
3158
|
TrackedStateKey {
|
|
2720
3159
|
schema_key: schema_key.to_string(),
|
|
@@ -2723,22 +3162,26 @@ mod tests {
|
|
|
2723
3162
|
}
|
|
2724
3163
|
}
|
|
2725
3164
|
|
|
2726
|
-
fn value(change_id: &str, snapshot_content: Option<&str>) ->
|
|
2727
|
-
let
|
|
2728
|
-
Some("{\"v\":1}") =>
|
|
2729
|
-
Some("{\"v\":2}") =>
|
|
2730
|
-
Some(_) =>
|
|
2731
|
-
None =>
|
|
3165
|
+
fn value(change_id: &str, snapshot_content: Option<&str>) -> TrackedStateIndexValue {
|
|
3166
|
+
let source_ordinal = match snapshot_content {
|
|
3167
|
+
Some("{\"v\":1}") => 1,
|
|
3168
|
+
Some("{\"v\":2}") => 2,
|
|
3169
|
+
Some(_) => 3,
|
|
3170
|
+
None => 0,
|
|
2732
3171
|
};
|
|
2733
|
-
|
|
2734
|
-
|
|
3172
|
+
TrackedStateIndexValue {
|
|
3173
|
+
change_locator: crate::commit_store::ChangeLocator {
|
|
3174
|
+
source_commit_id: "commit".to_string(),
|
|
3175
|
+
source_pack_id: 0,
|
|
3176
|
+
source_ordinal,
|
|
3177
|
+
change_id: change_id.to_string(),
|
|
3178
|
+
},
|
|
3179
|
+
deleted: snapshot_content.is_none(),
|
|
3180
|
+
snapshot_ref: snapshot_content
|
|
3181
|
+
.map(|content| crate::json_store::JsonRef::for_content(content.as_bytes())),
|
|
2735
3182
|
metadata_ref: None,
|
|
2736
|
-
schema_version: "1".to_string(),
|
|
2737
3183
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2738
3184
|
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
2739
|
-
change_id: change_id.to_string(),
|
|
2740
|
-
commit_id: "commit".to_string(),
|
|
2741
|
-
deleted: snapshot_content.is_none(),
|
|
2742
3185
|
}
|
|
2743
3186
|
}
|
|
2744
3187
|
}
|