@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.
- package/README.md +1 -1
- package/SKILL.md +105 -65
- package/dist/engine-wasm/index.js +4 -4
- package/dist/engine-wasm/wasm/lix_engine.d.ts +30 -6
- package/dist/engine-wasm/wasm/lix_engine.js +187 -117
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +14 -8
- package/dist/generated/builtin-schemas.d.ts +69 -69
- package/dist/generated/builtin-schemas.js +94 -94
- package/dist/open-lix.d.ts +42 -28
- package/dist/open-lix.js +49 -10
- package/dist/sqlite/index.js +86 -30
- package/dist-engine-src/README.md +3 -3
- package/dist-engine-src/src/backend/capabilities.rs +67 -0
- package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
- package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
- package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
- package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
- package/dist-engine-src/src/backend/conformance/model.rs +28 -0
- package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
- package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
- package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
- package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
- package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
- package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
- package/dist-engine-src/src/backend/conformance/write.rs +16 -0
- package/dist-engine-src/src/backend/error.rs +94 -0
- package/dist-engine-src/src/backend/in_memory.rs +670 -0
- package/dist-engine-src/src/backend/mod.rs +36 -9
- package/dist-engine-src/src/backend/predicate.rs +80 -0
- package/dist-engine-src/src/backend/traits.rs +260 -0
- package/dist-engine-src/src/backend/types.rs +224 -81
- package/dist-engine-src/src/binary_cas/context.rs +8 -8
- package/dist-engine-src/src/binary_cas/kv.rs +234 -259
- package/dist-engine-src/src/{version → branch}/context.rs +12 -12
- package/dist-engine-src/src/branch/lifecycle.rs +221 -0
- package/dist-engine-src/src/branch/mod.rs +13 -0
- package/dist-engine-src/src/branch/refs.rs +321 -0
- package/dist-engine-src/src/branch/stage_rows.rs +67 -0
- package/dist-engine-src/src/branch/types.rs +21 -0
- package/dist-engine-src/src/catalog/context.rs +18 -18
- package/dist-engine-src/src/catalog/snapshot.rs +8 -8
- package/dist-engine-src/src/changelog/bench_support.rs +785 -0
- package/dist-engine-src/src/changelog/change.rs +1 -0
- package/dist-engine-src/src/changelog/codec.rs +497 -0
- package/dist-engine-src/src/changelog/commit.rs +1 -0
- package/dist-engine-src/src/changelog/context.rs +1614 -0
- package/dist-engine-src/src/changelog/mod.rs +29 -0
- package/dist-engine-src/src/changelog/store.rs +163 -0
- package/dist-engine-src/src/changelog/test_support.rs +54 -0
- package/dist-engine-src/src/changelog/types.rs +213 -0
- package/dist-engine-src/src/commit_graph/context.rs +317 -274
- package/dist-engine-src/src/commit_graph/mod.rs +2 -4
- package/dist-engine-src/src/commit_graph/types.rs +22 -42
- package/dist-engine-src/src/commit_graph/walker.rs +133 -103
- package/dist-engine-src/src/common/error.rs +52 -18
- package/dist-engine-src/src/common/identity.rs +2 -2
- package/dist-engine-src/src/common/mod.rs +1 -1
- package/dist-engine-src/src/domain.rs +42 -46
- package/dist-engine-src/src/engine.rs +74 -96
- package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
- package/dist-engine-src/src/functions/context.rs +56 -52
- package/dist-engine-src/src/functions/state.rs +51 -52
- package/dist-engine-src/src/init.rs +288 -154
- package/dist-engine-src/src/json_store/context.rs +15 -266
- package/dist-engine-src/src/json_store/mod.rs +26 -0
- package/dist-engine-src/src/json_store/store.rs +103 -718
- package/dist-engine-src/src/json_store/types.rs +4 -9
- package/dist-engine-src/src/lib.rs +49 -19
- package/dist-engine-src/src/live_state/context.rs +654 -790
- package/dist-engine-src/src/live_state/mod.rs +9 -3
- package/dist-engine-src/src/live_state/overlay.rs +4 -4
- package/dist-engine-src/src/live_state/types.rs +30 -21
- package/dist-engine-src/src/live_state/visibility.rs +514 -71
- package/dist-engine-src/src/plugin/install.rs +48 -48
- package/dist-engine-src/src/plugin/manifest.rs +7 -7
- package/dist-engine-src/src/plugin/materializer.rs +0 -275
- package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
- package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
- package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
- package/dist-engine-src/src/schema/compatibility.rs +11 -11
- package/dist-engine-src/src/schema/definition.json +2 -2
- package/dist-engine-src/src/schema/definition.rs +5 -5
- package/dist-engine-src/src/schema/key.rs +3 -3
- package/dist-engine-src/src/schema/mod.rs +1 -1
- package/dist-engine-src/src/schema/tests.rs +18 -18
- package/dist-engine-src/src/session/context.rs +819 -124
- package/dist-engine-src/src/session/create_branch.rs +94 -0
- package/dist-engine-src/src/session/execute.rs +260 -57
- package/dist-engine-src/src/session/merge/analysis.rs +9 -3
- package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
- package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
- package/dist-engine-src/src/session/merge/mod.rs +5 -6
- package/dist-engine-src/src/session/merge/stats.rs +7 -11
- package/dist-engine-src/src/session/mod.rs +19 -16
- package/dist-engine-src/src/session/switch_branch.rs +113 -0
- package/dist-engine-src/src/session/transaction.rs +557 -0
- package/dist-engine-src/src/sql2/bind/classify.rs +102 -0
- package/dist-engine-src/src/sql2/bind/error.rs +5 -0
- package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
- package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
- package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +98 -3
- package/dist-engine-src/src/sql2/bind/read.rs +65 -0
- package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
- package/dist-engine-src/src/sql2/bind/table.rs +273 -0
- package/dist-engine-src/src/sql2/bind/write.rs +86 -0
- package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
- package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
- package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
- package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
- package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
- package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
- package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
- package/dist-engine-src/src/sql2/context.rs +36 -30
- package/dist-engine-src/src/sql2/error.rs +4 -5
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
- package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
- package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
- package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
- package/dist-engine-src/src/sql2/exec/write.rs +661 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
- package/dist-engine-src/src/sql2/history_projection.rs +8 -8
- package/dist-engine-src/src/sql2/history_route.rs +35 -31
- package/dist-engine-src/src/sql2/mod.rs +30 -24
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
- package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
- package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
- package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
- package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
- package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
- package/dist-engine-src/src/sql2/plan/write.rs +147 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
- package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
- package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
- package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
- package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
- package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
- package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
- package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
- package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
- package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
- package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
- package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
- package/dist-engine-src/src/sql2/read_only.rs +2 -2
- package/dist-engine-src/src/sql2/session.rs +47 -96
- package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
- package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
- package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
- package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
- package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
- package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
- package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
- package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
- package/dist-engine-src/src/storage/conformance.rs +399 -0
- package/dist-engine-src/src/storage/context.rs +552 -288
- package/dist-engine-src/src/storage/mod.rs +48 -10
- package/dist-engine-src/src/storage/point.rs +440 -0
- package/dist-engine-src/src/storage/read_scope.rs +43 -64
- package/dist-engine-src/src/storage/reader.rs +867 -0
- package/dist-engine-src/src/storage/scan.rs +784 -0
- package/dist-engine-src/src/storage/spaces.rs +236 -0
- package/dist-engine-src/src/storage/stats.rs +80 -0
- package/dist-engine-src/src/storage/write_set.rs +962 -0
- package/dist-engine-src/src/storage_bench.rs +136 -4828
- package/dist-engine-src/src/test_support.rs +360 -138
- package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
- package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
- package/dist-engine-src/src/tracked_state/context.rs +1927 -993
- package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
- package/dist-engine-src/src/tracked_state/merge.rs +74 -88
- package/dist-engine-src/src/tracked_state/mod.rs +19 -16
- package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
- package/dist-engine-src/src/tracked_state/storage.rs +243 -191
- package/dist-engine-src/src/tracked_state/tree.rs +247 -371
- package/dist-engine-src/src/tracked_state/types.rs +49 -42
- package/dist-engine-src/src/transaction/bench_support.rs +407 -0
- package/dist-engine-src/src/transaction/commit.rs +821 -713
- package/dist-engine-src/src/transaction/context.rs +705 -600
- package/dist-engine-src/src/transaction/mod.rs +13 -2
- package/dist-engine-src/src/transaction/normalization.rs +63 -76
- package/dist-engine-src/src/transaction/prep.rs +13 -13
- package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
- package/dist-engine-src/src/transaction/staging.rs +228 -434
- package/dist-engine-src/src/transaction/types.rs +41 -98
- package/dist-engine-src/src/transaction/validation.rs +382 -446
- package/dist-engine-src/src/untracked_state/codec.rs +337 -29
- package/dist-engine-src/src/untracked_state/context.rs +7 -7
- package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
- package/dist-engine-src/src/untracked_state/mod.rs +1 -1
- package/dist-engine-src/src/untracked_state/storage.rs +659 -157
- package/dist-engine-src/src/untracked_state/types.rs +21 -21
- package/package.json +71 -68
- package/dist-engine-src/src/backend/kv.rs +0 -358
- package/dist-engine-src/src/backend/testing.rs +0 -658
- package/dist-engine-src/src/commit_store/codec.rs +0 -887
- package/dist-engine-src/src/commit_store/context.rs +0 -944
- package/dist-engine-src/src/commit_store/materialization.rs +0 -84
- package/dist-engine-src/src/commit_store/mod.rs +0 -16
- package/dist-engine-src/src/commit_store/storage.rs +0 -600
- package/dist-engine-src/src/commit_store/types.rs +0 -215
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
- package/dist-engine-src/src/session/create_version.rs +0 -88
- package/dist-engine-src/src/session/merge/apply.rs +0 -23
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
- package/dist-engine-src/src/session/switch_version.rs +0 -109
- package/dist-engine-src/src/sql2/classify.rs +0 -182
- package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
- package/dist-engine-src/src/sql2/execute.rs +0 -3440
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
- package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
- package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -166
- package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -25
- package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
- package/dist-engine-src/src/sql2/version_scope.rs +0 -394
- package/dist-engine-src/src/storage/types.rs +0 -501
- package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
- package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
- package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
- package/dist-engine-src/src/version/lifecycle.rs +0 -221
- package/dist-engine-src/src/version/mod.rs +0 -13
- package/dist-engine-src/src/version/refs.rs +0 -330
- package/dist-engine-src/src/version/stage_rows.rs +0 -67
- package/dist-engine-src/src/version/types.rs +0 -21
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
use
|
|
2
|
-
|
|
1
|
+
use bytes::Bytes;
|
|
2
|
+
|
|
3
|
+
use crate::entity_pk::EntityPk;
|
|
4
|
+
use crate::storage::{
|
|
5
|
+
PointReadPlan, ScanPlan, StorageCoreProjection, StorageGetOptions, StorageKey, StoragePrefix,
|
|
6
|
+
StorageProjectedValue, StorageRead, StorageScanOptions, StorageSpace, StorageSpaceId,
|
|
7
|
+
StorageValue, StorageWriteSet,
|
|
8
|
+
};
|
|
3
9
|
use crate::untracked_state::{
|
|
4
10
|
MaterializedUntrackedStateRow, UntrackedMaterializationProjection, UntrackedStateIdentity,
|
|
5
11
|
UntrackedStateIdentityRef, UntrackedStateRow, UntrackedStateRowRef, UntrackedStateRowRequest,
|
|
@@ -7,69 +13,84 @@ use crate::untracked_state::{
|
|
|
7
13
|
};
|
|
8
14
|
use crate::{LixError, NullableKeyFilter};
|
|
9
15
|
|
|
10
|
-
pub(super) const UNTRACKED_STATE_ROW_NAMESPACE: &str = "untracked_state.row";
|
|
16
|
+
pub(super) const UNTRACKED_STATE_ROW_NAMESPACE: &str = "untracked_state.row.v1";
|
|
17
|
+
pub(crate) const UNTRACKED_STATE_ROW_SPACE: StorageSpace =
|
|
18
|
+
StorageSpace::new(StorageSpaceId(0x0001_0002), UNTRACKED_STATE_ROW_NAMESPACE);
|
|
19
|
+
// Durable key bytes:
|
|
20
|
+
// b"LXUK" | branch:u8 |
|
|
21
|
+
// branch_id_len:u32be | branch_id:utf8 |
|
|
22
|
+
// schema_key_len:u32be | schema_key:utf8 |
|
|
23
|
+
// entity_part_count:u32be | {entity_part_len:u32be | entity_part:utf8}* |
|
|
24
|
+
// file_id_tag:u8 | [file_id_len:u32be | file_id:utf8]
|
|
25
|
+
const UNTRACKED_STATE_ROW_KEY_IDENTIFIER: &[u8; 4] = b"LXUK";
|
|
26
|
+
const UNTRACKED_STATE_ROW_KEY_BRANCH_V1: u8 = 1;
|
|
11
27
|
|
|
12
28
|
pub(crate) async fn scan_rows(
|
|
13
|
-
store: &
|
|
29
|
+
store: &impl StorageRead,
|
|
14
30
|
request: &UntrackedStateScanRequest,
|
|
15
31
|
) -> Result<Vec<MaterializedUntrackedStateRow>, LixError> {
|
|
16
|
-
let mut rows = scan_all_canonical_rows(store).await?;
|
|
17
|
-
rows.retain(|row| row_matches_scan(row, request));
|
|
18
|
-
if let Some(limit) = request.limit {
|
|
19
|
-
rows.truncate(limit);
|
|
20
|
-
}
|
|
21
32
|
let projection = UntrackedMaterializationProjection::from_columns(&request.projection.columns);
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
let plans = scan_plans_for_request(request)?;
|
|
34
|
+
let mut materialized = Vec::new();
|
|
35
|
+
|
|
36
|
+
for plan in plans {
|
|
37
|
+
scan_matching_rows(store, request, &projection, &plan, &mut materialized)?;
|
|
38
|
+
if request
|
|
39
|
+
.limit
|
|
40
|
+
.is_some_and(|limit| materialized.len() >= limit)
|
|
41
|
+
{
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
25
44
|
}
|
|
45
|
+
|
|
26
46
|
Ok(materialized)
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
pub(crate) async fn load_row(
|
|
30
|
-
store: &
|
|
50
|
+
store: &impl StorageRead,
|
|
31
51
|
request: &UntrackedStateRowRequest,
|
|
32
52
|
) -> Result<Option<MaterializedUntrackedStateRow>, LixError> {
|
|
33
53
|
let Some(identity) = identity_from_request(request) else {
|
|
34
54
|
return Ok(None);
|
|
35
55
|
};
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
56
|
+
let result = PointReadPlan::new(
|
|
57
|
+
UNTRACKED_STATE_ROW_SPACE,
|
|
58
|
+
&[StorageKey(Bytes::from(encode_untracked_state_row_key(
|
|
59
|
+
&identity,
|
|
60
|
+
)?))],
|
|
61
|
+
)
|
|
62
|
+
.materialize(store, StorageGetOptions::default())?;
|
|
63
|
+
let bytes = result
|
|
64
|
+
.value
|
|
45
65
|
.into_iter()
|
|
46
66
|
.next()
|
|
47
|
-
.
|
|
67
|
+
.flatten()
|
|
68
|
+
.and_then(full_value);
|
|
48
69
|
let Some(bytes) = bytes else {
|
|
49
70
|
return Ok(None);
|
|
50
71
|
};
|
|
51
|
-
let row = crate::untracked_state::codec::
|
|
72
|
+
let row = crate::untracked_state::codec::decode_payload_with_identity(identity, &bytes)?;
|
|
52
73
|
crate::untracked_state::materialize_row(row, &UntrackedMaterializationProjection::full())
|
|
53
74
|
.map(Some)
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
pub(super) async fn existing_identities<'a>(
|
|
57
|
-
store: &
|
|
78
|
+
store: &(impl StorageRead + ?Sized),
|
|
58
79
|
identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
|
|
59
80
|
) -> Result<Vec<UntrackedStateIdentity>, LixError> {
|
|
60
81
|
let mut candidates = identities
|
|
61
82
|
.into_iter()
|
|
62
83
|
.map(|identity| {
|
|
63
84
|
let owned = UntrackedStateIdentity {
|
|
64
|
-
|
|
85
|
+
branch_id: identity.branch_id.to_string(),
|
|
65
86
|
schema_key: identity.schema_key.to_string(),
|
|
66
|
-
|
|
87
|
+
entity_pk: identity.entity_pk.clone(),
|
|
67
88
|
file_id: identity.file_id.map(str::to_string),
|
|
68
89
|
};
|
|
69
|
-
let key = encode_untracked_state_row_key_ref(owned.as_ref())
|
|
70
|
-
(key, owned)
|
|
90
|
+
let key = encode_untracked_state_row_key_ref(owned.as_ref())?;
|
|
91
|
+
Ok((key, owned))
|
|
71
92
|
})
|
|
72
|
-
.collect::<Vec<_>>()
|
|
93
|
+
.collect::<Result<Vec<_>, LixError>>()?;
|
|
73
94
|
candidates.sort_by(|(left, _), (right, _)| left.cmp(right));
|
|
74
95
|
candidates.dedup_by(|(left, _), (right, _)| left == right);
|
|
75
96
|
if candidates.is_empty() {
|
|
@@ -77,29 +98,27 @@ pub(super) async fn existing_identities<'a>(
|
|
|
77
98
|
}
|
|
78
99
|
let keys = candidates
|
|
79
100
|
.iter()
|
|
80
|
-
.map(|(key, _)| key.clone())
|
|
101
|
+
.map(|(key, _)| StorageKey(Bytes::from(key.clone())))
|
|
81
102
|
.collect::<Vec<_>>();
|
|
82
103
|
|
|
83
|
-
let result =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
})?;
|
|
97
|
-
if group.exists.len() != candidates.len() {
|
|
104
|
+
let result = PointReadPlan::from_unique_keys(UNTRACKED_STATE_ROW_SPACE, keys).materialize(
|
|
105
|
+
store,
|
|
106
|
+
StorageGetOptions {
|
|
107
|
+
projection: StorageCoreProjection::KeyOnly,
|
|
108
|
+
..StorageGetOptions::default()
|
|
109
|
+
},
|
|
110
|
+
)?;
|
|
111
|
+
let exists = result
|
|
112
|
+
.value
|
|
113
|
+
.into_iter()
|
|
114
|
+
.map(|value| value.is_some())
|
|
115
|
+
.collect::<Vec<_>>();
|
|
116
|
+
if exists.len() != candidates.len() {
|
|
98
117
|
return Err(LixError::new(
|
|
99
118
|
LixError::CODE_INTERNAL_ERROR,
|
|
100
119
|
format!(
|
|
101
120
|
"untracked identity existence probe returned {} results for {} requested keys",
|
|
102
|
-
|
|
121
|
+
exists.len(),
|
|
103
122
|
candidates.len()
|
|
104
123
|
),
|
|
105
124
|
));
|
|
@@ -107,7 +126,7 @@ pub(super) async fn existing_identities<'a>(
|
|
|
107
126
|
|
|
108
127
|
Ok(candidates
|
|
109
128
|
.into_iter()
|
|
110
|
-
.zip(
|
|
129
|
+
.zip(exists)
|
|
111
130
|
.filter_map(|((_, identity), exists)| exists.then_some(identity))
|
|
112
131
|
.collect())
|
|
113
132
|
}
|
|
@@ -119,55 +138,189 @@ where
|
|
|
119
138
|
for row in rows {
|
|
120
139
|
if row.snapshot_content.is_none() {
|
|
121
140
|
writes.delete(
|
|
122
|
-
|
|
123
|
-
encode_untracked_state_row_key_ref(row.into()),
|
|
141
|
+
UNTRACKED_STATE_ROW_SPACE,
|
|
142
|
+
StorageKey(Bytes::from(encode_untracked_state_row_key_ref(row.into())?)),
|
|
124
143
|
);
|
|
125
144
|
} else {
|
|
126
145
|
writes.put(
|
|
127
|
-
|
|
128
|
-
encode_untracked_state_row_key_ref(row.into()),
|
|
129
|
-
|
|
146
|
+
UNTRACKED_STATE_ROW_SPACE,
|
|
147
|
+
StorageKey(Bytes::from(encode_untracked_state_row_key_ref(row.into())?)),
|
|
148
|
+
StorageValue {
|
|
149
|
+
bytes: Bytes::from(crate::untracked_state::codec::encode_payload_ref(row)?),
|
|
150
|
+
},
|
|
130
151
|
);
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
Ok(())
|
|
134
155
|
}
|
|
135
156
|
|
|
136
|
-
pub(crate) fn stage_delete_rows<'a, I>(
|
|
157
|
+
pub(crate) fn stage_delete_rows<'a, I>(
|
|
158
|
+
writes: &mut StorageWriteSet,
|
|
159
|
+
identities: I,
|
|
160
|
+
) -> Result<(), LixError>
|
|
137
161
|
where
|
|
138
162
|
I: IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
|
|
139
163
|
{
|
|
140
164
|
for identity in identities {
|
|
141
165
|
writes.delete(
|
|
142
|
-
|
|
143
|
-
encode_untracked_state_row_key_ref(identity),
|
|
166
|
+
UNTRACKED_STATE_ROW_SPACE,
|
|
167
|
+
StorageKey(Bytes::from(encode_untracked_state_row_key_ref(identity)?)),
|
|
144
168
|
);
|
|
145
169
|
}
|
|
170
|
+
Ok(())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn scan_matching_rows(
|
|
174
|
+
store: &impl StorageRead,
|
|
175
|
+
request: &UntrackedStateScanRequest,
|
|
176
|
+
projection: &UntrackedMaterializationProjection,
|
|
177
|
+
plan: &ScanPlan,
|
|
178
|
+
materialized: &mut Vec<MaterializedUntrackedStateRow>,
|
|
179
|
+
) -> Result<(), LixError> {
|
|
180
|
+
let mut resume_after = None;
|
|
181
|
+
loop {
|
|
182
|
+
let remaining_limit = request
|
|
183
|
+
.limit
|
|
184
|
+
.map(|limit| limit.saturating_sub(materialized.len()));
|
|
185
|
+
if matches!(remaining_limit, Some(0)) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
let page = plan.collect(
|
|
189
|
+
store,
|
|
190
|
+
StorageScanOptions {
|
|
191
|
+
resume_after: resume_after.as_ref(),
|
|
192
|
+
limit_rows: remaining_limit
|
|
193
|
+
.unwrap_or_else(|| StorageScanOptions::default().limit_rows),
|
|
194
|
+
..StorageScanOptions::default()
|
|
195
|
+
},
|
|
196
|
+
)?;
|
|
197
|
+
resume_after = page.value.entries.last().map(|entry| entry.key.clone());
|
|
198
|
+
|
|
199
|
+
for entry in page.value.entries {
|
|
200
|
+
let Some(bytes) = full_value(entry.value) else {
|
|
201
|
+
continue;
|
|
202
|
+
};
|
|
203
|
+
let identity = decode_untracked_state_row_key_ref(entry.key.0.as_ref())?;
|
|
204
|
+
let row = crate::untracked_state::codec::decode_payload_with_identity(
|
|
205
|
+
identity,
|
|
206
|
+
bytes.as_ref(),
|
|
207
|
+
)?;
|
|
208
|
+
if !row_matches_scan(&row, request) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
materialized.push(crate::untracked_state::materialize_row(row, projection)?);
|
|
212
|
+
if request
|
|
213
|
+
.limit
|
|
214
|
+
.is_some_and(|limit| materialized.len() >= limit)
|
|
215
|
+
{
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if !page.value.has_more || resume_after.is_none() {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
Ok(())
|
|
146
225
|
}
|
|
147
226
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
227
|
+
fn scan_plans_for_request(request: &UntrackedStateScanRequest) -> Result<Vec<ScanPlan>, LixError> {
|
|
228
|
+
let mut prefixes = scan_prefixes_for_filter(&request.filter)?;
|
|
229
|
+
prefixes.sort();
|
|
230
|
+
prefixes.dedup();
|
|
231
|
+
Ok(prefixes
|
|
232
|
+
.into_iter()
|
|
233
|
+
.map(|prefix| {
|
|
234
|
+
ScanPlan::prefix(
|
|
235
|
+
UNTRACKED_STATE_ROW_SPACE,
|
|
236
|
+
StoragePrefix {
|
|
237
|
+
bytes: Bytes::from(prefix),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
157
240
|
})
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
241
|
+
.collect())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn scan_prefixes_for_filter(
|
|
245
|
+
filter: &crate::untracked_state::UntrackedStateFilter,
|
|
246
|
+
) -> Result<Vec<Vec<u8>>, LixError> {
|
|
247
|
+
if filter.branch_ids.is_empty() {
|
|
248
|
+
return Ok(vec![Vec::new()]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let mut prefixes = Vec::new();
|
|
252
|
+
for branch_id in &filter.branch_ids {
|
|
253
|
+
let mut branch_prefix = key_header();
|
|
254
|
+
push_component(&mut branch_prefix, branch_id)?;
|
|
255
|
+
if filter.schema_keys.is_empty() {
|
|
256
|
+
prefixes.push(branch_prefix);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for schema_key in &filter.schema_keys {
|
|
261
|
+
let mut schema_prefix = branch_prefix.clone();
|
|
262
|
+
push_component(&mut schema_prefix, schema_key)?;
|
|
263
|
+
if filter.entity_pks.is_empty() {
|
|
264
|
+
prefixes.push(schema_prefix);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for entity_pk in &filter.entity_pks {
|
|
269
|
+
let mut entity_prefix = schema_prefix.clone();
|
|
270
|
+
push_entity_component(&mut entity_prefix, entity_pk)?;
|
|
271
|
+
append_file_prefixes(&mut prefixes, entity_prefix, &filter.file_ids)?;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
Ok(prefixes)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn push_entity_component(out: &mut Vec<u8>, entity_pk: &EntityPk) -> Result<(), LixError> {
|
|
279
|
+
push_entity_tuple(out, entity_pk)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn append_file_prefixes(
|
|
283
|
+
prefixes: &mut Vec<Vec<u8>>,
|
|
284
|
+
entity_prefix: Vec<u8>,
|
|
285
|
+
file_filters: &[NullableKeyFilter<String>],
|
|
286
|
+
) -> Result<(), LixError> {
|
|
287
|
+
if file_filters.is_empty()
|
|
288
|
+
|| file_filters
|
|
289
|
+
.iter()
|
|
290
|
+
.any(|filter| matches!(filter, NullableKeyFilter::Any))
|
|
291
|
+
{
|
|
292
|
+
prefixes.push(entity_prefix);
|
|
293
|
+
return Ok(());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for filter in file_filters {
|
|
297
|
+
let mut prefix = entity_prefix.clone();
|
|
298
|
+
match filter {
|
|
299
|
+
NullableKeyFilter::Null => prefix.push(0),
|
|
300
|
+
NullableKeyFilter::Value(file_id) => {
|
|
301
|
+
prefix.push(1);
|
|
302
|
+
push_component(&mut prefix, file_id)?;
|
|
303
|
+
}
|
|
304
|
+
NullableKeyFilter::Any => unreachable!("Any handled before exact file prefixes"),
|
|
305
|
+
}
|
|
306
|
+
prefixes.push(prefix);
|
|
307
|
+
}
|
|
308
|
+
Ok(())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn full_value(value: StorageProjectedValue) -> Option<Bytes> {
|
|
312
|
+
match value {
|
|
313
|
+
StorageProjectedValue::FullValue(bytes) => Some(bytes),
|
|
314
|
+
StorageProjectedValue::KeyOnly => None,
|
|
315
|
+
}
|
|
163
316
|
}
|
|
164
317
|
|
|
165
318
|
fn row_matches_scan(row: &UntrackedStateRow, request: &UntrackedStateScanRequest) -> bool {
|
|
166
319
|
(request.filter.schema_keys.is_empty() || request.filter.schema_keys.contains(&row.schema_key))
|
|
167
|
-
&& (request.filter.
|
|
168
|
-
|| request.filter.
|
|
169
|
-
&& (request.filter.
|
|
170
|
-
|| request.filter.
|
|
320
|
+
&& (request.filter.entity_pks.is_empty()
|
|
321
|
+
|| request.filter.entity_pks.contains(&row.entity_pk))
|
|
322
|
+
&& (request.filter.branch_ids.is_empty()
|
|
323
|
+
|| request.filter.branch_ids.contains(&row.branch_id))
|
|
171
324
|
&& nullable_matches_filters(&row.file_id, &request.filter.file_ids)
|
|
172
325
|
}
|
|
173
326
|
|
|
@@ -187,59 +340,370 @@ fn identity_from_request(request: &UntrackedStateRowRequest) -> Option<Untracked
|
|
|
187
340
|
NullableKeyFilter::Any => return None,
|
|
188
341
|
};
|
|
189
342
|
Some(UntrackedStateIdentity {
|
|
190
|
-
|
|
343
|
+
branch_id: request.branch_id.clone(),
|
|
191
344
|
schema_key: request.schema_key.clone(),
|
|
192
|
-
|
|
345
|
+
entity_pk: request.entity_pk.clone(),
|
|
193
346
|
file_id,
|
|
194
347
|
})
|
|
195
348
|
}
|
|
196
349
|
|
|
197
|
-
fn encode_untracked_state_row_key(identity: &UntrackedStateIdentity) -> Vec<u8> {
|
|
350
|
+
fn encode_untracked_state_row_key(identity: &UntrackedStateIdentity) -> Result<Vec<u8>, LixError> {
|
|
198
351
|
encode_untracked_state_row_key_ref(identity.as_ref())
|
|
199
352
|
}
|
|
200
353
|
|
|
201
|
-
pub(
|
|
354
|
+
pub(crate) fn encode_untracked_state_row_key_ref(
|
|
202
355
|
identity: UntrackedStateIdentityRef<'_>,
|
|
203
|
-
) -> Vec<u8> {
|
|
204
|
-
let mut out =
|
|
205
|
-
push_component(&mut out, identity.
|
|
206
|
-
push_component(&mut out, identity.schema_key)
|
|
207
|
-
|
|
208
|
-
.entity_id
|
|
209
|
-
.as_json_array_text()
|
|
210
|
-
.expect("untracked-state identity should project");
|
|
211
|
-
push_component(&mut out, &entity_id);
|
|
356
|
+
) -> Result<Vec<u8>, LixError> {
|
|
357
|
+
let mut out = key_header();
|
|
358
|
+
push_component(&mut out, identity.branch_id)?;
|
|
359
|
+
push_component(&mut out, identity.schema_key)?;
|
|
360
|
+
push_entity_tuple(&mut out, identity.entity_pk)?;
|
|
212
361
|
match identity.file_id {
|
|
213
362
|
Some(file_id) => {
|
|
214
363
|
out.push(1);
|
|
215
|
-
push_component(&mut out, file_id)
|
|
364
|
+
push_component(&mut out, file_id)?;
|
|
216
365
|
}
|
|
217
366
|
None => out.push(0),
|
|
218
367
|
}
|
|
368
|
+
Ok(out)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fn decode_untracked_state_row_key_ref(bytes: &[u8]) -> Result<UntrackedStateIdentity, LixError> {
|
|
372
|
+
if !bytes.starts_with(UNTRACKED_STATE_ROW_KEY_IDENTIFIER) {
|
|
373
|
+
return Err(LixError::new(
|
|
374
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
375
|
+
"failed to decode untracked-state key: invalid key identifier",
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
let mut cursor = UNTRACKED_STATE_ROW_KEY_IDENTIFIER.len();
|
|
379
|
+
let branch = bytes.get(cursor).copied().ok_or_else(|| {
|
|
380
|
+
LixError::new(
|
|
381
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
382
|
+
"failed to decode untracked-state key: missing branch",
|
|
383
|
+
)
|
|
384
|
+
})?;
|
|
385
|
+
cursor += 1;
|
|
386
|
+
if branch != UNTRACKED_STATE_ROW_KEY_BRANCH_V1 {
|
|
387
|
+
return Err(LixError::new(
|
|
388
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
389
|
+
format!("failed to decode untracked-state key: unsupported branch {branch}"),
|
|
390
|
+
));
|
|
391
|
+
}
|
|
392
|
+
let branch_id = read_key_component(bytes, &mut cursor, "branch_id")?;
|
|
393
|
+
let schema_key = read_key_component(bytes, &mut cursor, "schema_key")?;
|
|
394
|
+
let entity_pk = read_entity_tuple(bytes, &mut cursor)?;
|
|
395
|
+
let file_tag = bytes.get(cursor).copied().ok_or_else(|| {
|
|
396
|
+
LixError::new(
|
|
397
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
398
|
+
"failed to decode untracked-state key: missing file_id tag",
|
|
399
|
+
)
|
|
400
|
+
})?;
|
|
401
|
+
cursor += 1;
|
|
402
|
+
let file_id = match file_tag {
|
|
403
|
+
0 => None,
|
|
404
|
+
1 => Some(read_key_component(bytes, &mut cursor, "file_id")?),
|
|
405
|
+
_ => {
|
|
406
|
+
return Err(LixError::new(
|
|
407
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
408
|
+
"failed to decode untracked-state key: invalid file_id tag",
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
if cursor != bytes.len() {
|
|
413
|
+
return Err(LixError::new(
|
|
414
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
415
|
+
"failed to decode untracked-state key: trailing bytes",
|
|
416
|
+
));
|
|
417
|
+
}
|
|
418
|
+
Ok(UntrackedStateIdentity {
|
|
419
|
+
branch_id,
|
|
420
|
+
schema_key,
|
|
421
|
+
entity_pk,
|
|
422
|
+
file_id,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn read_key_component(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<String, LixError> {
|
|
427
|
+
let len_end = cursor.checked_add(4).ok_or_else(|| {
|
|
428
|
+
LixError::new(
|
|
429
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
430
|
+
format!("failed to decode untracked-state key: `{field}` cursor overflow"),
|
|
431
|
+
)
|
|
432
|
+
})?;
|
|
433
|
+
let len_bytes = bytes.get(*cursor..len_end).ok_or_else(|| {
|
|
434
|
+
LixError::new(
|
|
435
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
436
|
+
format!("failed to decode untracked-state key: truncated `{field}` length"),
|
|
437
|
+
)
|
|
438
|
+
})?;
|
|
439
|
+
*cursor = len_end;
|
|
440
|
+
let len = u32::from_be_bytes(len_bytes.try_into().expect("slice length checked")) as usize;
|
|
441
|
+
let value_end = cursor.checked_add(len).ok_or_else(|| {
|
|
442
|
+
LixError::new(
|
|
443
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
444
|
+
format!("failed to decode untracked-state key: `{field}` length overflow"),
|
|
445
|
+
)
|
|
446
|
+
})?;
|
|
447
|
+
let value = bytes.get(*cursor..value_end).ok_or_else(|| {
|
|
448
|
+
LixError::new(
|
|
449
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
450
|
+
format!("failed to decode untracked-state key: truncated `{field}`"),
|
|
451
|
+
)
|
|
452
|
+
})?;
|
|
453
|
+
*cursor = value_end;
|
|
454
|
+
std::str::from_utf8(value)
|
|
455
|
+
.map(str::to_string)
|
|
456
|
+
.map_err(|error| {
|
|
457
|
+
LixError::new(
|
|
458
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
459
|
+
format!(
|
|
460
|
+
"failed to decode untracked-state key: invalid utf-8 for `{field}`: {error}"
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn read_entity_tuple(bytes: &[u8], cursor: &mut usize) -> Result<EntityPk, LixError> {
|
|
467
|
+
let part_count = read_key_u32(bytes, cursor, "entity_part_count")? as usize;
|
|
468
|
+
if part_count == 0 {
|
|
469
|
+
return Err(LixError::new(
|
|
470
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
471
|
+
"failed to decode untracked-state key: entity primary key has no parts",
|
|
472
|
+
));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let mut parts = Vec::with_capacity(part_count);
|
|
476
|
+
for _ in 0..part_count {
|
|
477
|
+
let part = read_key_component(bytes, cursor, "entity_part")?;
|
|
478
|
+
parts.push(part);
|
|
479
|
+
}
|
|
480
|
+
Ok(EntityPk { parts })
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
fn read_key_u32(bytes: &[u8], cursor: &mut usize, field: &str) -> Result<u32, LixError> {
|
|
484
|
+
let len_end = cursor.checked_add(4).ok_or_else(|| {
|
|
485
|
+
LixError::new(
|
|
486
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
487
|
+
format!("failed to decode untracked-state key: `{field}` cursor overflow"),
|
|
488
|
+
)
|
|
489
|
+
})?;
|
|
490
|
+
let len_bytes = bytes.get(*cursor..len_end).ok_or_else(|| {
|
|
491
|
+
LixError::new(
|
|
492
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
493
|
+
format!("failed to decode untracked-state key: truncated `{field}`"),
|
|
494
|
+
)
|
|
495
|
+
})?;
|
|
496
|
+
*cursor = len_end;
|
|
497
|
+
Ok(u32::from_be_bytes(
|
|
498
|
+
len_bytes.try_into().expect("slice length checked"),
|
|
499
|
+
))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn key_header() -> Vec<u8> {
|
|
503
|
+
let mut out = Vec::with_capacity(UNTRACKED_STATE_ROW_KEY_IDENTIFIER.len() + 1);
|
|
504
|
+
out.extend_from_slice(UNTRACKED_STATE_ROW_KEY_IDENTIFIER);
|
|
505
|
+
out.push(UNTRACKED_STATE_ROW_KEY_BRANCH_V1);
|
|
219
506
|
out
|
|
220
507
|
}
|
|
221
508
|
|
|
222
|
-
fn push_component(out: &mut Vec<u8>, value: &str) {
|
|
509
|
+
fn push_component(out: &mut Vec<u8>, value: &str) -> Result<(), LixError> {
|
|
223
510
|
let bytes = value.as_bytes();
|
|
224
|
-
out
|
|
511
|
+
push_bytes_component(out, bytes)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
fn push_entity_tuple(out: &mut Vec<u8>, entity_pk: &EntityPk) -> Result<(), LixError> {
|
|
515
|
+
let part_count = u32::try_from(entity_pk.parts.len()).map_err(|_| {
|
|
516
|
+
LixError::new(
|
|
517
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
518
|
+
"failed to encode untracked-state key: entity primary key part count exceeds u32",
|
|
519
|
+
)
|
|
520
|
+
})?;
|
|
521
|
+
if part_count == 0 {
|
|
522
|
+
return Err(LixError::new(
|
|
523
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
524
|
+
"failed to encode untracked-state key: entity primary key has no parts",
|
|
525
|
+
));
|
|
526
|
+
}
|
|
527
|
+
out.extend_from_slice(&part_count.to_be_bytes());
|
|
528
|
+
for part in &entity_pk.parts {
|
|
529
|
+
push_bytes_component(out, part.as_bytes())?;
|
|
530
|
+
}
|
|
531
|
+
Ok(())
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
fn push_bytes_component(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), LixError> {
|
|
535
|
+
let len = u32::try_from(bytes.len()).map_err(|_| {
|
|
536
|
+
LixError::new(
|
|
537
|
+
LixError::CODE_INTERNAL_ERROR,
|
|
538
|
+
"failed to encode untracked-state key: component length exceeds u32",
|
|
539
|
+
)
|
|
540
|
+
})?;
|
|
541
|
+
out.extend_from_slice(&len.to_be_bytes());
|
|
225
542
|
out.extend_from_slice(bytes);
|
|
543
|
+
Ok(())
|
|
226
544
|
}
|
|
227
545
|
|
|
228
546
|
#[cfg(test)]
|
|
229
547
|
mod tests {
|
|
230
|
-
use std::sync::Arc;
|
|
231
|
-
|
|
232
548
|
use super::*;
|
|
233
|
-
use crate::
|
|
234
|
-
use crate::storage::{
|
|
549
|
+
use crate::storage::StorageContext;
|
|
550
|
+
use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
|
|
235
551
|
use crate::untracked_state::UntrackedStateContext;
|
|
236
552
|
|
|
553
|
+
#[test]
|
|
554
|
+
fn key_v1_roundtrips_null_file_id() {
|
|
555
|
+
let identity = UntrackedStateIdentity {
|
|
556
|
+
branch_id: "branch-1".to_string(),
|
|
557
|
+
schema_key: "schema-1".to_string(),
|
|
558
|
+
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
559
|
+
file_id: None,
|
|
560
|
+
};
|
|
561
|
+
let key = encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
562
|
+
|
|
563
|
+
assert_eq!(&key[..4], b"LXUK");
|
|
564
|
+
assert_eq!(key[4], 1);
|
|
565
|
+
assert_eq!(
|
|
566
|
+
decode_untracked_state_row_key_ref(&key).expect("key should decode"),
|
|
567
|
+
identity
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
fn key_v1_roundtrips_empty_file_id() {
|
|
573
|
+
let identity = UntrackedStateIdentity {
|
|
574
|
+
branch_id: "branch-1".to_string(),
|
|
575
|
+
schema_key: "schema-1".to_string(),
|
|
576
|
+
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
577
|
+
file_id: Some(String::new()),
|
|
578
|
+
};
|
|
579
|
+
let key = encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
580
|
+
|
|
581
|
+
assert_eq!(
|
|
582
|
+
decode_untracked_state_row_key_ref(&key).expect("key should decode"),
|
|
583
|
+
identity
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn key_v1_roundtrips_empty_entity_pk_part() {
|
|
589
|
+
let identity = UntrackedStateIdentity {
|
|
590
|
+
branch_id: "branch-1".to_string(),
|
|
591
|
+
schema_key: "json_pointer".to_string(),
|
|
592
|
+
entity_pk: crate::entity_pk::EntityPk::single(""),
|
|
593
|
+
file_id: Some("file-1".to_string()),
|
|
594
|
+
};
|
|
595
|
+
let key = encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
596
|
+
|
|
597
|
+
assert_eq!(
|
|
598
|
+
decode_untracked_state_row_key_ref(&key).expect("key should decode"),
|
|
599
|
+
identity
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#[test]
|
|
604
|
+
fn key_v1_roundtrips_tuple_and_unicode_identity() {
|
|
605
|
+
let identity = UntrackedStateIdentity {
|
|
606
|
+
branch_id: "branch-東京".to_string(),
|
|
607
|
+
schema_key: "schema-1".to_string(),
|
|
608
|
+
entity_pk: crate::entity_pk::EntityPk::tuple(vec![
|
|
609
|
+
"entity-1".to_string(),
|
|
610
|
+
"ключ".to_string(),
|
|
611
|
+
])
|
|
612
|
+
.expect("entity primary key should build"),
|
|
613
|
+
file_id: Some("file-δ".to_string()),
|
|
614
|
+
};
|
|
615
|
+
let key = encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
616
|
+
|
|
617
|
+
assert_eq!(
|
|
618
|
+
decode_untracked_state_row_key_ref(&key).expect("key should decode"),
|
|
619
|
+
identity
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn key_v1_encodes_entity_as_binary_tuple_not_json_text() {
|
|
625
|
+
let identity = UntrackedStateIdentity {
|
|
626
|
+
branch_id: "branch-1".to_string(),
|
|
627
|
+
schema_key: "schema-1".to_string(),
|
|
628
|
+
entity_pk: crate::entity_pk::EntityPk::tuple(vec![
|
|
629
|
+
"entity/1".to_string(),
|
|
630
|
+
"quote\"part".to_string(),
|
|
631
|
+
])
|
|
632
|
+
.expect("entity primary key should build"),
|
|
633
|
+
file_id: None,
|
|
634
|
+
};
|
|
635
|
+
let key = encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
636
|
+
|
|
637
|
+
assert!(!key
|
|
638
|
+
.windows(2)
|
|
639
|
+
.any(|window| window == br#"[""# || window == br#""]"#));
|
|
640
|
+
assert_eq!(
|
|
641
|
+
decode_untracked_state_row_key_ref(&key).expect("key should decode"),
|
|
642
|
+
identity
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#[test]
|
|
647
|
+
fn key_decode_rejects_invalid_identifier() {
|
|
648
|
+
let error = decode_untracked_state_row_key_ref(b"LXUQ\x01")
|
|
649
|
+
.expect_err("invalid key identifier should fail");
|
|
650
|
+
assert!(error.to_string().contains("invalid key identifier"));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#[test]
|
|
654
|
+
fn key_decode_rejects_unknown_branch() {
|
|
655
|
+
let identity = UntrackedStateIdentity {
|
|
656
|
+
branch_id: "branch-1".to_string(),
|
|
657
|
+
schema_key: "schema-1".to_string(),
|
|
658
|
+
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
659
|
+
file_id: None,
|
|
660
|
+
};
|
|
661
|
+
let mut key =
|
|
662
|
+
encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
663
|
+
key[4] = 2;
|
|
664
|
+
let error =
|
|
665
|
+
decode_untracked_state_row_key_ref(&key).expect_err("unknown key branch should fail");
|
|
666
|
+
assert!(error.to_string().contains("unsupported branch 2"));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#[test]
|
|
670
|
+
fn key_decode_rejects_trailing_bytes() {
|
|
671
|
+
let identity = UntrackedStateIdentity {
|
|
672
|
+
branch_id: "branch-1".to_string(),
|
|
673
|
+
schema_key: "schema-1".to_string(),
|
|
674
|
+
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
675
|
+
file_id: None,
|
|
676
|
+
};
|
|
677
|
+
let mut key =
|
|
678
|
+
encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
679
|
+
key.push(0);
|
|
680
|
+
let error =
|
|
681
|
+
decode_untracked_state_row_key_ref(&key).expect_err("trailing key bytes should fail");
|
|
682
|
+
assert!(error.to_string().contains("trailing bytes"));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[test]
|
|
686
|
+
fn key_decode_rejects_truncated_component() {
|
|
687
|
+
let identity = UntrackedStateIdentity {
|
|
688
|
+
branch_id: "branch-1".to_string(),
|
|
689
|
+
schema_key: "schema-1".to_string(),
|
|
690
|
+
entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
|
|
691
|
+
file_id: None,
|
|
692
|
+
};
|
|
693
|
+
let mut key =
|
|
694
|
+
encode_untracked_state_row_key_ref(identity.as_ref()).expect("key should encode");
|
|
695
|
+
key.truncate(key.len() - 2);
|
|
696
|
+
let error =
|
|
697
|
+
decode_untracked_state_row_key_ref(&key).expect_err("truncated key should fail");
|
|
698
|
+
assert!(error.to_string().contains("truncated"));
|
|
699
|
+
}
|
|
700
|
+
|
|
237
701
|
async fn write_materialized_rows_to_store(
|
|
238
702
|
context: &UntrackedStateContext,
|
|
239
|
-
|
|
703
|
+
storage: &StorageContext,
|
|
240
704
|
rows: &[MaterializedUntrackedStateRow],
|
|
241
705
|
) {
|
|
242
|
-
let mut writes =
|
|
706
|
+
let mut writes = storage.new_write_set();
|
|
243
707
|
let canonical_rows = rows
|
|
244
708
|
.iter()
|
|
245
709
|
.map(|row| crate::test_support::untracked_state_row_from_materialized(&mut writes, row))
|
|
@@ -249,35 +713,29 @@ mod tests {
|
|
|
249
713
|
.writer(&mut writes)
|
|
250
714
|
.stage_rows(canonical_rows.iter().map(|row| row.as_ref()))
|
|
251
715
|
.expect("rows should write");
|
|
252
|
-
|
|
716
|
+
storage
|
|
717
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
718
|
+
.expect("rows should commit");
|
|
253
719
|
}
|
|
254
720
|
|
|
255
721
|
#[tokio::test]
|
|
256
722
|
async fn write_and_load_roundtrips() {
|
|
257
|
-
let
|
|
258
|
-
let storage = StorageContext::new(backend.clone());
|
|
723
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
259
724
|
let context = UntrackedStateContext::new();
|
|
260
725
|
let row = untracked_row("global", "lix_key_value", "ui-tab");
|
|
261
726
|
|
|
262
|
-
|
|
263
|
-
.begin_write_transaction()
|
|
264
|
-
.await
|
|
265
|
-
.expect("transaction should open");
|
|
266
|
-
write_materialized_rows_to_store(
|
|
267
|
-
&context,
|
|
268
|
-
transaction.as_mut(),
|
|
269
|
-
std::slice::from_ref(&row),
|
|
270
|
-
)
|
|
271
|
-
.await;
|
|
272
|
-
transaction.commit().await.expect("commit should succeed");
|
|
727
|
+
write_materialized_rows_to_store(&context, &storage, std::slice::from_ref(&row)).await;
|
|
273
728
|
|
|
274
729
|
let loaded = {
|
|
275
|
-
let
|
|
730
|
+
let read = storage
|
|
731
|
+
.begin_read(StorageReadOptions::default())
|
|
732
|
+
.expect("read should open");
|
|
733
|
+
let mut reader = context.reader(read);
|
|
276
734
|
reader
|
|
277
735
|
.load_row(&UntrackedStateRowRequest {
|
|
278
736
|
schema_key: "lix_key_value".to_string(),
|
|
279
|
-
|
|
280
|
-
|
|
737
|
+
branch_id: "global".to_string(),
|
|
738
|
+
entity_pk: crate::entity_pk::EntityPk::single("ui-tab"),
|
|
281
739
|
file_id: NullableKeyFilter::Null,
|
|
282
740
|
})
|
|
283
741
|
.await
|
|
@@ -287,33 +745,30 @@ mod tests {
|
|
|
287
745
|
}
|
|
288
746
|
|
|
289
747
|
#[tokio::test]
|
|
290
|
-
async fn
|
|
291
|
-
let
|
|
292
|
-
let storage = StorageContext::new(backend.clone());
|
|
748
|
+
async fn scan_filters_by_schema_and_branch() {
|
|
749
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
293
750
|
let context = UntrackedStateContext::new();
|
|
294
|
-
let mut transaction = storage
|
|
295
|
-
.begin_write_transaction()
|
|
296
|
-
.await
|
|
297
|
-
.expect("transaction should open");
|
|
298
751
|
write_materialized_rows_to_store(
|
|
299
752
|
&context,
|
|
300
|
-
|
|
753
|
+
&storage,
|
|
301
754
|
&[
|
|
302
755
|
untracked_row("global", "lix_key_value", "global-ui"),
|
|
303
|
-
untracked_row("
|
|
304
|
-
untracked_row("
|
|
756
|
+
untracked_row("branch-a", "lix_key_value", "branch-ui"),
|
|
757
|
+
untracked_row("branch-a", "other_schema", "other"),
|
|
305
758
|
],
|
|
306
759
|
)
|
|
307
760
|
.await;
|
|
308
|
-
transaction.commit().await.expect("commit should succeed");
|
|
309
761
|
|
|
310
762
|
let rows = {
|
|
311
|
-
let
|
|
763
|
+
let read = storage
|
|
764
|
+
.begin_read(StorageReadOptions::default())
|
|
765
|
+
.expect("read should open");
|
|
766
|
+
let mut reader = context.reader(read);
|
|
312
767
|
reader
|
|
313
768
|
.scan_rows(&UntrackedStateScanRequest {
|
|
314
769
|
filter: crate::untracked_state::UntrackedStateFilter {
|
|
315
770
|
schema_keys: vec!["lix_key_value".to_string()],
|
|
316
|
-
|
|
771
|
+
branch_ids: vec!["branch-a".to_string()],
|
|
317
772
|
..Default::default()
|
|
318
773
|
},
|
|
319
774
|
..Default::default()
|
|
@@ -324,49 +779,43 @@ mod tests {
|
|
|
324
779
|
|
|
325
780
|
assert_eq!(rows.len(), 1);
|
|
326
781
|
assert_eq!(
|
|
327
|
-
rows[0].
|
|
328
|
-
crate::
|
|
782
|
+
rows[0].entity_pk,
|
|
783
|
+
crate::entity_pk::EntityPk::single("branch-ui")
|
|
329
784
|
);
|
|
330
785
|
}
|
|
331
786
|
|
|
332
787
|
#[tokio::test]
|
|
333
788
|
async fn delete_removes_row() {
|
|
334
|
-
let
|
|
335
|
-
let storage = StorageContext::new(backend.clone());
|
|
789
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
336
790
|
let context = UntrackedStateContext::new();
|
|
337
791
|
let row = untracked_row("global", "lix_key_value", "ui-tab");
|
|
338
792
|
let identity = UntrackedStateIdentity {
|
|
339
|
-
|
|
793
|
+
branch_id: row.branch_id.clone(),
|
|
340
794
|
schema_key: row.schema_key.clone(),
|
|
341
|
-
|
|
795
|
+
entity_pk: row.entity_pk.clone(),
|
|
342
796
|
file_id: row.file_id.clone(),
|
|
343
797
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
.expect("transaction should open");
|
|
348
|
-
let mut writes = StorageWriteSet::new();
|
|
349
|
-
let canonical_row =
|
|
350
|
-
crate::test_support::untracked_state_row_from_materialized(&mut writes, &row)
|
|
351
|
-
.expect("row should canonicalize");
|
|
798
|
+
write_materialized_rows_to_store(&context, &storage, std::slice::from_ref(&row)).await;
|
|
799
|
+
|
|
800
|
+
let mut writes = storage.new_write_set();
|
|
352
801
|
let mut writer = context.writer(&mut writes);
|
|
353
802
|
writer
|
|
354
|
-
.
|
|
355
|
-
.expect("
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
.
|
|
359
|
-
.await
|
|
360
|
-
.expect("writes should apply");
|
|
361
|
-
transaction.commit().await.expect("commit should succeed");
|
|
803
|
+
.stage_delete_rows(std::iter::once(identity.as_ref()))
|
|
804
|
+
.expect("delete should stage");
|
|
805
|
+
storage
|
|
806
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
807
|
+
.expect("writes should commit");
|
|
362
808
|
|
|
363
809
|
let loaded = {
|
|
364
|
-
let
|
|
810
|
+
let read = storage
|
|
811
|
+
.begin_read(StorageReadOptions::default())
|
|
812
|
+
.expect("read should open");
|
|
813
|
+
let mut reader = context.reader(read);
|
|
365
814
|
reader
|
|
366
815
|
.load_row(&UntrackedStateRowRequest {
|
|
367
816
|
schema_key: "lix_key_value".to_string(),
|
|
368
|
-
|
|
369
|
-
|
|
817
|
+
branch_id: "global".to_string(),
|
|
818
|
+
entity_pk: crate::entity_pk::EntityPk::single("ui-tab"),
|
|
370
819
|
file_id: NullableKeyFilter::Null,
|
|
371
820
|
})
|
|
372
821
|
.await
|
|
@@ -375,22 +824,75 @@ mod tests {
|
|
|
375
824
|
assert_eq!(loaded, None);
|
|
376
825
|
}
|
|
377
826
|
|
|
827
|
+
#[tokio::test]
|
|
828
|
+
async fn v1_layout_ignores_previous_untracked_row_space() {
|
|
829
|
+
let storage = StorageContext::new(InMemoryStorageBackend::new());
|
|
830
|
+
let context = UntrackedStateContext::new();
|
|
831
|
+
let legacy_space = StorageSpace::new(StorageSpaceId(0x0001_0001), "untracked_state.row");
|
|
832
|
+
let mut writes = storage.new_write_set();
|
|
833
|
+
writes.put(
|
|
834
|
+
legacy_space,
|
|
835
|
+
StorageKey(Bytes::from_static(b"legacy-row-key")),
|
|
836
|
+
StorageValue {
|
|
837
|
+
bytes: Bytes::from_static(b"legacy-row-value"),
|
|
838
|
+
},
|
|
839
|
+
);
|
|
840
|
+
storage
|
|
841
|
+
.commit_write_set(writes, StorageWriteOptions::default())
|
|
842
|
+
.expect("legacy row should commit");
|
|
843
|
+
|
|
844
|
+
let loaded = {
|
|
845
|
+
let read = storage
|
|
846
|
+
.begin_read(StorageReadOptions::default())
|
|
847
|
+
.expect("read should open");
|
|
848
|
+
let mut reader = context.reader(read);
|
|
849
|
+
reader
|
|
850
|
+
.load_row(&UntrackedStateRowRequest {
|
|
851
|
+
schema_key: "lix_key_value".to_string(),
|
|
852
|
+
branch_id: "global".to_string(),
|
|
853
|
+
entity_pk: crate::entity_pk::EntityPk::single("legacy-row-key"),
|
|
854
|
+
file_id: NullableKeyFilter::Null,
|
|
855
|
+
})
|
|
856
|
+
.await
|
|
857
|
+
}
|
|
858
|
+
.expect("load should succeed");
|
|
859
|
+
assert_eq!(loaded, None);
|
|
860
|
+
|
|
861
|
+
let rows = {
|
|
862
|
+
let read = storage
|
|
863
|
+
.begin_read(StorageReadOptions::default())
|
|
864
|
+
.expect("read should open");
|
|
865
|
+
let mut reader = context.reader(read);
|
|
866
|
+
reader
|
|
867
|
+
.scan_rows(&UntrackedStateScanRequest {
|
|
868
|
+
filter: crate::untracked_state::UntrackedStateFilter {
|
|
869
|
+
branch_ids: vec!["global".to_string()],
|
|
870
|
+
..Default::default()
|
|
871
|
+
},
|
|
872
|
+
..Default::default()
|
|
873
|
+
})
|
|
874
|
+
.await
|
|
875
|
+
}
|
|
876
|
+
.expect("scan should succeed");
|
|
877
|
+
assert!(rows.is_empty());
|
|
878
|
+
}
|
|
879
|
+
|
|
378
880
|
fn untracked_row(
|
|
379
|
-
|
|
881
|
+
branch_id: &str,
|
|
380
882
|
schema_key: &str,
|
|
381
|
-
|
|
883
|
+
entity_pk: &str,
|
|
382
884
|
) -> MaterializedUntrackedStateRow {
|
|
383
885
|
MaterializedUntrackedStateRow {
|
|
384
|
-
|
|
886
|
+
entity_pk: crate::entity_pk::EntityPk::single(entity_pk),
|
|
385
887
|
schema_key: schema_key.to_string(),
|
|
386
888
|
file_id: None,
|
|
387
|
-
snapshot_content: Some(format!("{{\"key\":\"{}\",\"value\":\"value\"}}",
|
|
889
|
+
snapshot_content: Some(format!("{{\"key\":\"{}\",\"value\":\"value\"}}", entity_pk)),
|
|
388
890
|
metadata: None,
|
|
389
891
|
deleted: false,
|
|
390
892
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
|
391
893
|
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
|
392
|
-
global:
|
|
393
|
-
|
|
894
|
+
global: branch_id == "global",
|
|
895
|
+
branch_id: branch_id.to_string(),
|
|
394
896
|
}
|
|
395
897
|
}
|
|
396
898
|
}
|