@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKILL.md +65 -64
- package/dist/engine-wasm/index.js +4 -4
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
- package/dist/engine-wasm/wasm/lix_engine.js +130 -118
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
- package/dist/generated/builtin-schemas.d.ts +69 -69
- package/dist/generated/builtin-schemas.js +94 -94
- package/dist/open-lix.d.ts +33 -26
- package/dist/open-lix.js +10 -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 +803 -148
- package/dist-engine-src/src/session/create_branch.rs +94 -0
- package/dist-engine-src/src/session/execute.rs +223 -83
- 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 +15 -12
- package/dist-engine-src/src/session/switch_branch.rs +113 -0
- package/dist-engine-src/src/session/transaction.rs +495 -14
- package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
- 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} +71 -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 +1 -1
- 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 +28 -23
- 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 -110
- package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
- package/dist-engine-src/src/sql2/execute.rs +0 -3533
- 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 -172
- package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
- 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,3533 +0,0 @@
|
|
|
1
|
-
use datafusion::arrow::datatypes::Field;
|
|
2
|
-
use datafusion::arrow::record_batch::RecordBatch;
|
|
3
|
-
use datafusion::common::metadata::{FieldMetadata, ScalarAndMetadata};
|
|
4
|
-
use datafusion::common::{ParamValues, ScalarValue};
|
|
5
|
-
use datafusion::logical_expr::{Expr, LogicalPlan, WriteOp};
|
|
6
|
-
use datafusion::prelude::SessionContext;
|
|
7
|
-
use datafusion::sql::parser::{DFParserBuilder, Statement as DataFusionStatement};
|
|
8
|
-
use datafusion::sql::sqlparser::dialect::GenericDialect;
|
|
9
|
-
use datafusion::sql::sqlparser::tokenizer::{Token, Tokenizer};
|
|
10
|
-
use serde_json::{json, Value as JsonValue};
|
|
11
|
-
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
|
12
|
-
|
|
13
|
-
use crate::schema::schema_key_from_definition;
|
|
14
|
-
use crate::{LixError, LixNotice, SqlQueryResult, Value};
|
|
15
|
-
|
|
16
|
-
use super::predicate_typecheck::validate_json_predicate_expr_with_dfschema;
|
|
17
|
-
use super::result_metadata::{field_is_json, LIX_VALUE_TYPE_JSON, LIX_VALUE_TYPE_METADATA_KEY};
|
|
18
|
-
use super::session::{build_read_session, build_write_session};
|
|
19
|
-
use super::write_normalization::{
|
|
20
|
-
is_binary_type, lix_file_data_type_lix_error, logical_expr_is_binary_or_null,
|
|
21
|
-
};
|
|
22
|
-
use super::{SqlExecutionContext, SqlStatementKind, SqlWriteExecutionContext};
|
|
23
|
-
|
|
24
|
-
#[allow(dead_code)]
|
|
25
|
-
pub(crate) struct SqlLogicalPlan {
|
|
26
|
-
session: SessionContext,
|
|
27
|
-
plan: LogicalPlan,
|
|
28
|
-
kind: SqlStatementKind,
|
|
29
|
-
notices: Vec<LixNotice>,
|
|
30
|
-
strict_binary_params: BTreeSet<usize>,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
impl SqlLogicalPlan {
|
|
34
|
-
#[allow(dead_code)]
|
|
35
|
-
pub(crate) fn kind(&self) -> SqlStatementKind {
|
|
36
|
-
self.kind
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
#[allow(dead_code)]
|
|
40
|
-
pub(crate) fn is_write(&self) -> bool {
|
|
41
|
-
self.kind == SqlStatementKind::Write
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Minimal top-level sql2 entrypoint.
|
|
46
|
-
///
|
|
47
|
-
/// The final implementation will build the DataFusion session from the
|
|
48
|
-
/// execution context and source rows from `live_state()`.
|
|
49
|
-
///
|
|
50
|
-
/// `catalog()` is intentionally omitted from the MVP boundary for now.
|
|
51
|
-
#[allow(dead_code)]
|
|
52
|
-
pub(crate) async fn execute_sql(
|
|
53
|
-
ctx: &dyn SqlExecutionContext,
|
|
54
|
-
sql: &str,
|
|
55
|
-
params: &[Value],
|
|
56
|
-
) -> Result<SqlQueryResult, LixError> {
|
|
57
|
-
let plan = create_logical_plan(ctx, sql).await?;
|
|
58
|
-
execute_logical_plan(plan, params).await
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
pub(crate) async fn create_logical_plan(
|
|
62
|
-
ctx: &dyn SqlExecutionContext,
|
|
63
|
-
sql: &str,
|
|
64
|
-
) -> Result<SqlLogicalPlan, LixError> {
|
|
65
|
-
let statement = parse_statement(sql)?;
|
|
66
|
-
create_logical_plan_from_parsed(ctx, sql, statement).await
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
pub(crate) fn parse_statement(sql: &str) -> Result<DataFusionStatement, LixError> {
|
|
70
|
-
parse_datafusion_statement(sql)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
pub(crate) async fn create_logical_plan_from_parsed(
|
|
74
|
-
ctx: &dyn SqlExecutionContext,
|
|
75
|
-
sql: &str,
|
|
76
|
-
statement: DataFusionStatement,
|
|
77
|
-
) -> Result<SqlLogicalPlan, LixError> {
|
|
78
|
-
validate_public_read_sql_surface(sql)?;
|
|
79
|
-
super::validate_supported_datafusion_statement_ast(&statement)?;
|
|
80
|
-
super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
|
|
81
|
-
let session = build_read_session(ctx).await?;
|
|
82
|
-
let plan = create_logical_plan_from_statement(&session, statement).await?;
|
|
83
|
-
validate_supported_logical_plan(&plan)?;
|
|
84
|
-
validate_json_predicates_in_logical_plan(&plan)?;
|
|
85
|
-
let kind = classify_logical_plan(&plan);
|
|
86
|
-
let notices = history_filter_notices(&plan);
|
|
87
|
-
|
|
88
|
-
Ok(SqlLogicalPlan {
|
|
89
|
-
session,
|
|
90
|
-
plan,
|
|
91
|
-
kind,
|
|
92
|
-
notices,
|
|
93
|
-
strict_binary_params: BTreeSet::new(),
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#[allow(dead_code)]
|
|
98
|
-
pub(crate) async fn create_write_logical_plan(
|
|
99
|
-
ctx: &mut dyn SqlWriteExecutionContext,
|
|
100
|
-
sql: &str,
|
|
101
|
-
) -> Result<SqlLogicalPlan, LixError> {
|
|
102
|
-
let statement = parse_statement(sql)?;
|
|
103
|
-
create_write_logical_plan_from_parsed(ctx, statement).await
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
pub(crate) async fn create_write_logical_plan_from_parsed(
|
|
107
|
-
ctx: &mut dyn SqlWriteExecutionContext,
|
|
108
|
-
statement: DataFusionStatement,
|
|
109
|
-
) -> Result<SqlLogicalPlan, LixError> {
|
|
110
|
-
super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
|
|
111
|
-
let visible_schemas = ctx.list_visible_schemas()?;
|
|
112
|
-
super::public_bind::validate_public_dml_statement(&statement, &visible_schemas)?;
|
|
113
|
-
super::validate_supported_datafusion_statement_ast(&statement)?;
|
|
114
|
-
reject_read_only_history_view_dml_from_statement(&statement, &visible_schemas)?;
|
|
115
|
-
let session = build_write_session(ctx).await?;
|
|
116
|
-
let plan = create_logical_plan_from_statement(&session, statement).await?;
|
|
117
|
-
validate_supported_logical_plan(&plan)?;
|
|
118
|
-
super::public_bind::validate_public_dml_plan(&plan, &visible_schemas)?;
|
|
119
|
-
validate_json_predicates_in_logical_plan(&plan)?;
|
|
120
|
-
let strict_binary_params = validate_strict_lix_file_data_writes(&plan)?;
|
|
121
|
-
let kind = classify_logical_plan(&plan);
|
|
122
|
-
|
|
123
|
-
Ok(SqlLogicalPlan {
|
|
124
|
-
session,
|
|
125
|
-
plan,
|
|
126
|
-
kind,
|
|
127
|
-
notices: Vec::new(),
|
|
128
|
-
strict_binary_params,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
pub(crate) async fn create_transaction_read_logical_plan_from_parsed(
|
|
133
|
-
ctx: &mut dyn SqlWriteExecutionContext,
|
|
134
|
-
sql: &str,
|
|
135
|
-
statement: DataFusionStatement,
|
|
136
|
-
) -> Result<SqlLogicalPlan, LixError> {
|
|
137
|
-
validate_public_read_sql_surface(sql)?;
|
|
138
|
-
super::validate_supported_datafusion_statement_ast(&statement)?;
|
|
139
|
-
super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
|
|
140
|
-
let session = build_write_session(ctx).await?;
|
|
141
|
-
let plan = create_logical_plan_from_statement(&session, statement).await?;
|
|
142
|
-
validate_supported_logical_plan(&plan)?;
|
|
143
|
-
validate_json_predicates_in_logical_plan(&plan)?;
|
|
144
|
-
let kind = classify_logical_plan(&plan);
|
|
145
|
-
let notices = history_filter_notices(&plan);
|
|
146
|
-
|
|
147
|
-
Ok(SqlLogicalPlan {
|
|
148
|
-
session,
|
|
149
|
-
plan,
|
|
150
|
-
kind,
|
|
151
|
-
notices,
|
|
152
|
-
strict_binary_params: BTreeSet::new(),
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
fn validate_public_read_sql_surface(sql: &str) -> Result<(), LixError> {
|
|
157
|
-
let normalized = sql.to_ascii_lowercase();
|
|
158
|
-
if normalized.contains("lower(path)") {
|
|
159
|
-
return Err(LixError::new(
|
|
160
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
161
|
-
"public column 'path' must be compared directly to a literal or parameter",
|
|
162
|
-
));
|
|
163
|
-
}
|
|
164
|
-
if normalized.contains("lixcol_version_id")
|
|
165
|
-
&& (normalized.contains("= lower(") || normalized.contains(" in (lower("))
|
|
166
|
-
{
|
|
167
|
-
return Err(LixError::new(
|
|
168
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
169
|
-
"public column 'lixcol_version_id' must be compared directly to a literal or parameter",
|
|
170
|
-
));
|
|
171
|
-
}
|
|
172
|
-
Ok(())
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
fn parse_datafusion_statement(sql: &str) -> Result<DataFusionStatement, LixError> {
|
|
176
|
-
let dialect = GenericDialect {};
|
|
177
|
-
let mut next_index = 1usize;
|
|
178
|
-
let mut has_anonymous = false;
|
|
179
|
-
let mut explicit_placeholders = Vec::new();
|
|
180
|
-
|
|
181
|
-
let mut tokens = Vec::new();
|
|
182
|
-
Tokenizer::new(&dialect, sql)
|
|
183
|
-
.tokenize_with_location_into_buf_with_mapper(&mut tokens, |mut token_span| {
|
|
184
|
-
if let Token::Placeholder(placeholder) = &token_span.token {
|
|
185
|
-
if placeholder == "?" {
|
|
186
|
-
has_anonymous = true;
|
|
187
|
-
token_span.token = Token::Placeholder(format!("${next_index}"));
|
|
188
|
-
next_index += 1;
|
|
189
|
-
} else {
|
|
190
|
-
explicit_placeholders.push(placeholder.clone());
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
token_span
|
|
194
|
-
})
|
|
195
|
-
.map_err(|error| {
|
|
196
|
-
LixError::new(
|
|
197
|
-
LixError::CODE_PARSE_ERROR,
|
|
198
|
-
format!("sql2 SQL tokenize error: {error}"),
|
|
199
|
-
)
|
|
200
|
-
})?;
|
|
201
|
-
|
|
202
|
-
if has_anonymous && !explicit_placeholders.is_empty() {
|
|
203
|
-
return Err(LixError::new(
|
|
204
|
-
LixError::CODE_PARSE_ERROR,
|
|
205
|
-
"SQL mixes anonymous and explicit parameter placeholders",
|
|
206
|
-
)
|
|
207
|
-
.with_hint("Use either anonymous placeholders like ?, ? or numbered placeholders like $1, $2, but not both.")
|
|
208
|
-
.with_details(json!({
|
|
209
|
-
"operation": "execute",
|
|
210
|
-
"explicit_placeholders": explicit_placeholders,
|
|
211
|
-
})));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
let mut statements = DFParserBuilder::new(tokens)
|
|
215
|
-
.with_dialect(&dialect)
|
|
216
|
-
.build()
|
|
217
|
-
.map_err(datafusion_error_to_lix_error)?
|
|
218
|
-
.parse_statements()
|
|
219
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
220
|
-
|
|
221
|
-
if statements.len() > 1 {
|
|
222
|
-
return Err(LixError::new(
|
|
223
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
224
|
-
"Lix SQL only supports one statement per execute() call",
|
|
225
|
-
));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
statements.pop_front().ok_or_else(|| {
|
|
229
|
-
LixError::new(
|
|
230
|
-
LixError::CODE_PARSE_ERROR,
|
|
231
|
-
"sql2 DataFusion error: No SQL statements were provided in the query string",
|
|
232
|
-
)
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async fn create_logical_plan_from_statement(
|
|
237
|
-
session: &SessionContext,
|
|
238
|
-
statement: DataFusionStatement,
|
|
239
|
-
) -> Result<LogicalPlan, LixError> {
|
|
240
|
-
session
|
|
241
|
-
.state()
|
|
242
|
-
.statement_to_plan(statement)
|
|
243
|
-
.await
|
|
244
|
-
.map_err(datafusion_error_to_lix_error)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
fn validate_json_predicates_in_logical_plan(plan: &LogicalPlan) -> Result<(), LixError> {
|
|
248
|
-
match plan {
|
|
249
|
-
LogicalPlan::Filter(filter) => {
|
|
250
|
-
validate_json_predicate_expr_with_dfschema(filter.input.schema(), &filter.predicate)?;
|
|
251
|
-
}
|
|
252
|
-
LogicalPlan::TableScan(scan) => {
|
|
253
|
-
for filter in &scan.filters {
|
|
254
|
-
validate_json_predicate_expr_with_dfschema(scan.projected_schema.as_ref(), filter)?;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
_ => {}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for input in plan.inputs() {
|
|
261
|
-
validate_json_predicates_in_logical_plan(input)?;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
Ok(())
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
fn validate_strict_lix_file_data_writes(plan: &LogicalPlan) -> Result<BTreeSet<usize>, LixError> {
|
|
268
|
-
let mut strict_binary_params = BTreeSet::new();
|
|
269
|
-
let LogicalPlan::Dml(dml) = plan else {
|
|
270
|
-
return Ok(strict_binary_params);
|
|
271
|
-
};
|
|
272
|
-
if dml.table_name.table() != "lix_file"
|
|
273
|
-
|| !matches!(dml.op, WriteOp::Insert(_) | WriteOp::Update)
|
|
274
|
-
{
|
|
275
|
-
return Ok(strict_binary_params);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
reject_non_binary_lix_file_data_write(&dml.input, &mut strict_binary_params)?;
|
|
279
|
-
Ok(strict_binary_params)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
fn reject_non_binary_lix_file_data_write(
|
|
283
|
-
input: &LogicalPlan,
|
|
284
|
-
strict_binary_params: &mut BTreeSet<usize>,
|
|
285
|
-
) -> Result<(), LixError> {
|
|
286
|
-
let LogicalPlan::Projection(projection) = input else {
|
|
287
|
-
return Ok(());
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
let Some(data_expr) = projection.expr.iter().find_map(|expr| match expr {
|
|
291
|
-
Expr::Alias(alias) if alias.name == "data" => Some(alias.expr.as_ref()),
|
|
292
|
-
_ => None,
|
|
293
|
-
}) else {
|
|
294
|
-
return Ok(());
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
validate_lix_file_data_expr(data_expr, strict_binary_params)?;
|
|
298
|
-
|
|
299
|
-
let Expr::Column(column) = data_expr else {
|
|
300
|
-
return Ok(());
|
|
301
|
-
};
|
|
302
|
-
let LogicalPlan::Values(values) = projection.input.as_ref() else {
|
|
303
|
-
return Ok(());
|
|
304
|
-
};
|
|
305
|
-
let Ok(column_index) = values.schema.index_of_column(column) else {
|
|
306
|
-
return Ok(());
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
for row in &values.values {
|
|
310
|
-
if let Some(value_expr) = row.get(column_index) {
|
|
311
|
-
validate_lix_file_data_expr(value_expr, strict_binary_params)?;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
Ok(())
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
fn validate_lix_file_data_expr(
|
|
319
|
-
expr: &Expr,
|
|
320
|
-
strict_binary_params: &mut BTreeSet<usize>,
|
|
321
|
-
) -> Result<(), LixError> {
|
|
322
|
-
match expr {
|
|
323
|
-
Expr::Cast(cast) if is_binary_type(&cast.data_type) => {
|
|
324
|
-
if collect_placeholder_param(&cast.expr, strict_binary_params)? {
|
|
325
|
-
return Ok(());
|
|
326
|
-
}
|
|
327
|
-
if !logical_expr_is_binary_or_null(&cast.expr) {
|
|
328
|
-
return Err(lix_file_data_type_lix_error());
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
Expr::Placeholder(_) => {
|
|
332
|
-
collect_placeholder_param(expr, strict_binary_params)?;
|
|
333
|
-
}
|
|
334
|
-
Expr::Alias(alias) => validate_lix_file_data_expr(&alias.expr, strict_binary_params)?,
|
|
335
|
-
_ => {}
|
|
336
|
-
}
|
|
337
|
-
Ok(())
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
fn collect_placeholder_param(
|
|
341
|
-
expr: &Expr,
|
|
342
|
-
strict_binary_params: &mut BTreeSet<usize>,
|
|
343
|
-
) -> Result<bool, LixError> {
|
|
344
|
-
match expr {
|
|
345
|
-
Expr::Placeholder(placeholder) => {
|
|
346
|
-
let index = placeholder_index(&placeholder.id)?;
|
|
347
|
-
strict_binary_params.insert(index);
|
|
348
|
-
Ok(true)
|
|
349
|
-
}
|
|
350
|
-
Expr::Alias(alias) => collect_placeholder_param(&alias.expr, strict_binary_params),
|
|
351
|
-
_ => Ok(false),
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
fn placeholder_index(id: &str) -> Result<usize, LixError> {
|
|
356
|
-
id.strip_prefix('$')
|
|
357
|
-
.and_then(|raw| raw.parse::<usize>().ok())
|
|
358
|
-
.filter(|index| *index > 0)
|
|
359
|
-
.ok_or_else(|| {
|
|
360
|
-
LixError::new(
|
|
361
|
-
LixError::CODE_PARSE_ERROR,
|
|
362
|
-
format!("unsupported SQL parameter placeholder '{id}'"),
|
|
363
|
-
)
|
|
364
|
-
.with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
pub(crate) async fn execute_logical_plan(
|
|
369
|
-
plan: SqlLogicalPlan,
|
|
370
|
-
params: &[Value],
|
|
371
|
-
) -> Result<SqlQueryResult, LixError> {
|
|
372
|
-
let SqlLogicalPlan {
|
|
373
|
-
session,
|
|
374
|
-
plan,
|
|
375
|
-
kind: _,
|
|
376
|
-
notices,
|
|
377
|
-
strict_binary_params,
|
|
378
|
-
} = plan;
|
|
379
|
-
validate_parameter_count(&plan, params.len())?;
|
|
380
|
-
validate_strict_binary_params(&strict_binary_params, params)?;
|
|
381
|
-
|
|
382
|
-
let mut dataframe = session
|
|
383
|
-
.execute_logical_plan(plan)
|
|
384
|
-
.await
|
|
385
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
386
|
-
if !params.is_empty() {
|
|
387
|
-
dataframe = dataframe
|
|
388
|
-
.with_param_values(ParamValues::List(
|
|
389
|
-
params.iter().map(scalar_value_from_lix_value).collect(),
|
|
390
|
-
))
|
|
391
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let result_fields = dataframe
|
|
395
|
-
.schema()
|
|
396
|
-
.fields()
|
|
397
|
-
.iter()
|
|
398
|
-
.map(|field| field.as_ref().clone())
|
|
399
|
-
.collect::<Vec<_>>();
|
|
400
|
-
let batches = super::runtime::collect_dataframe(dataframe)
|
|
401
|
-
.await
|
|
402
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
403
|
-
let mut result = query_result_from_batches(&result_fields, &batches)?;
|
|
404
|
-
result.notices = notices;
|
|
405
|
-
Ok(result)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
fn validate_strict_binary_params(
|
|
409
|
-
strict_binary_params: &BTreeSet<usize>,
|
|
410
|
-
params: &[Value],
|
|
411
|
-
) -> Result<(), LixError> {
|
|
412
|
-
for index in strict_binary_params {
|
|
413
|
-
let Some(value) = params.get(index - 1) else {
|
|
414
|
-
continue;
|
|
415
|
-
};
|
|
416
|
-
if !matches!(value, Value::Blob(_)) {
|
|
417
|
-
return Err(lix_file_data_type_lix_error());
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
Ok(())
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
fn validate_parameter_count(plan: &LogicalPlan, param_count: usize) -> Result<(), LixError> {
|
|
424
|
-
let parameter_names = plan
|
|
425
|
-
.get_parameter_names()
|
|
426
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
427
|
-
let expected_count = expected_positional_parameter_count(¶meter_names)?;
|
|
428
|
-
if param_count == expected_count {
|
|
429
|
-
return Ok(());
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
Err(LixError::new(
|
|
433
|
-
LixError::CODE_INVALID_PARAM,
|
|
434
|
-
format!(
|
|
435
|
-
"SQL expected {expected_count} parameter(s), but {param_count} parameter(s) were provided"
|
|
436
|
-
),
|
|
437
|
-
)
|
|
438
|
-
.with_details(json!({
|
|
439
|
-
"operation": "execute",
|
|
440
|
-
"expected_param_count": expected_count,
|
|
441
|
-
"provided_param_count": param_count,
|
|
442
|
-
"placeholders": sorted_parameter_names(¶meter_names),
|
|
443
|
-
})))
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
fn expected_positional_parameter_count(
|
|
447
|
-
parameter_names: &HashSet<String>,
|
|
448
|
-
) -> Result<usize, LixError> {
|
|
449
|
-
let mut max_index = 0usize;
|
|
450
|
-
for name in parameter_names {
|
|
451
|
-
let Some(index) = name
|
|
452
|
-
.strip_prefix('$')
|
|
453
|
-
.and_then(|raw| raw.parse::<usize>().ok())
|
|
454
|
-
else {
|
|
455
|
-
return Err(LixError::new(
|
|
456
|
-
LixError::CODE_PARSE_ERROR,
|
|
457
|
-
format!("unsupported SQL parameter placeholder '{name}'"),
|
|
458
|
-
)
|
|
459
|
-
.with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
|
|
460
|
-
.with_details(json!({
|
|
461
|
-
"operation": "execute",
|
|
462
|
-
"placeholder": name,
|
|
463
|
-
})));
|
|
464
|
-
};
|
|
465
|
-
if index == 0 {
|
|
466
|
-
return Err(LixError::new(
|
|
467
|
-
LixError::CODE_PARSE_ERROR,
|
|
468
|
-
"SQL parameter placeholders are 1-indexed",
|
|
469
|
-
)
|
|
470
|
-
.with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
|
|
471
|
-
.with_details(json!({
|
|
472
|
-
"operation": "execute",
|
|
473
|
-
"placeholder": name,
|
|
474
|
-
})));
|
|
475
|
-
}
|
|
476
|
-
max_index = max_index.max(index);
|
|
477
|
-
}
|
|
478
|
-
Ok(max_index)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
fn sorted_parameter_names(parameter_names: &HashSet<String>) -> Vec<String> {
|
|
482
|
-
let mut names = parameter_names.iter().cloned().collect::<Vec<_>>();
|
|
483
|
-
names.sort();
|
|
484
|
-
names
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
fn reject_read_only_history_view_dml_from_statement(
|
|
488
|
-
statement: &DataFusionStatement,
|
|
489
|
-
visible_schemas: &[JsonValue],
|
|
490
|
-
) -> Result<(), LixError> {
|
|
491
|
-
let target_names = super::datafusion_statement_dml_target_table_names(statement);
|
|
492
|
-
for target_name in target_names {
|
|
493
|
-
if is_history_view_name(&target_name, visible_schemas)? {
|
|
494
|
-
return Err(read_only_history_view_error(&target_name));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
Ok(())
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
fn is_history_view_name(table_name: &str, visible_schemas: &[JsonValue]) -> Result<bool, LixError> {
|
|
501
|
-
if matches!(
|
|
502
|
-
table_name,
|
|
503
|
-
"lix_state_history" | "lix_file_history" | "lix_directory_history"
|
|
504
|
-
) {
|
|
505
|
-
return Ok(true);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
for schema in visible_schemas {
|
|
509
|
-
let schema_key = schema_key_from_definition(schema)?;
|
|
510
|
-
if table_name == format!("{}_history", schema_key.schema_key) {
|
|
511
|
-
return Ok(true);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
Ok(false)
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
fn read_only_history_view_error(view_name: &str) -> LixError {
|
|
519
|
-
LixError::new(
|
|
520
|
-
LixError::CODE_READ_ONLY,
|
|
521
|
-
format!("DML cannot write read-only history view '{view_name}'"),
|
|
522
|
-
)
|
|
523
|
-
.with_hint(
|
|
524
|
-
"History views are query-only; write to the live surface such as lix_state, lix_file, lix_directory, or the typed entity table.",
|
|
525
|
-
)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
fn classify_logical_plan(plan: &LogicalPlan) -> SqlStatementKind {
|
|
529
|
-
match plan {
|
|
530
|
-
LogicalPlan::Dml(_) => SqlStatementKind::Write,
|
|
531
|
-
LogicalPlan::Ddl(_) | LogicalPlan::Statement(_) | LogicalPlan::Copy(_) => {
|
|
532
|
-
SqlStatementKind::Other
|
|
533
|
-
}
|
|
534
|
-
_ => SqlStatementKind::Read,
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
fn validate_supported_logical_plan(plan: &LogicalPlan) -> Result<(), LixError> {
|
|
539
|
-
match plan {
|
|
540
|
-
LogicalPlan::Ddl(_) => {
|
|
541
|
-
return Err(LixError::new(
|
|
542
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
543
|
-
"DDL statements are not supported by Lix SQL",
|
|
544
|
-
)
|
|
545
|
-
.with_hint(
|
|
546
|
-
"Use Lix entity surfaces such as lix_registered_schema, lix_version, lix_file, and lix_key_value instead of CREATE/DROP statements.",
|
|
547
|
-
));
|
|
548
|
-
}
|
|
549
|
-
LogicalPlan::Statement(_) => {
|
|
550
|
-
return Err(LixError::new(
|
|
551
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
552
|
-
"SQL utility statements are not supported by Lix SQL",
|
|
553
|
-
));
|
|
554
|
-
}
|
|
555
|
-
LogicalPlan::Copy(_) => {
|
|
556
|
-
return Err(LixError::new(
|
|
557
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
558
|
-
"COPY statements are not supported by Lix SQL",
|
|
559
|
-
));
|
|
560
|
-
}
|
|
561
|
-
LogicalPlan::RecursiveQuery(_) => {
|
|
562
|
-
return Err(LixError::new(
|
|
563
|
-
LixError::CODE_UNSUPPORTED_SQL,
|
|
564
|
-
"recursive CTEs are not supported by Lix SQL",
|
|
565
|
-
)
|
|
566
|
-
.with_hint(
|
|
567
|
-
"Use explicit commit graph surfaces such as lix_commit, lix_commit_edge, and lix_state_history instead of WITH RECURSIVE.",
|
|
568
|
-
));
|
|
569
|
-
}
|
|
570
|
-
_ => {}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
for input in plan.inputs() {
|
|
574
|
-
validate_supported_logical_plan(input)?;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
Ok(())
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
fn scalar_value_from_lix_value(value: &Value) -> ScalarAndMetadata {
|
|
581
|
-
match value {
|
|
582
|
-
Value::Null => ScalarValue::Null.into(),
|
|
583
|
-
Value::Boolean(value) => ScalarValue::Boolean(Some(*value)).into(),
|
|
584
|
-
Value::Integer(value) => ScalarValue::Int64(Some(*value)).into(),
|
|
585
|
-
Value::Real(value) => ScalarValue::Float64(Some(*value)).into(),
|
|
586
|
-
Value::Text(value) => ScalarValue::Utf8(Some(value.clone())).into(),
|
|
587
|
-
Value::Json(value) => ScalarAndMetadata::new(
|
|
588
|
-
ScalarValue::Utf8(Some(value.to_string())),
|
|
589
|
-
Some(json_field_metadata()),
|
|
590
|
-
),
|
|
591
|
-
Value::Blob(value) => ScalarValue::Binary(Some(value.clone())).into(),
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
fn json_field_metadata() -> FieldMetadata {
|
|
596
|
-
FieldMetadata::new(BTreeMap::from([(
|
|
597
|
-
LIX_VALUE_TYPE_METADATA_KEY.to_string(),
|
|
598
|
-
LIX_VALUE_TYPE_JSON.to_string(),
|
|
599
|
-
)]))
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
fn datafusion_error_to_lix_error(error: datafusion::error::DataFusionError) -> LixError {
|
|
603
|
-
super::error::datafusion_error_to_lix_error(error)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
fn query_result_from_batches(
|
|
607
|
-
result_fields: &[Field],
|
|
608
|
-
batches: &[RecordBatch],
|
|
609
|
-
) -> Result<SqlQueryResult, LixError> {
|
|
610
|
-
let result_columns = result_fields
|
|
611
|
-
.iter()
|
|
612
|
-
.map(|field| field.name().to_string())
|
|
613
|
-
.collect::<Vec<_>>();
|
|
614
|
-
let mut rows = Vec::<Vec<Value>>::new();
|
|
615
|
-
for batch in batches {
|
|
616
|
-
for row_index in 0..batch.num_rows() {
|
|
617
|
-
let mut row = Vec::<Value>::with_capacity(batch.num_columns());
|
|
618
|
-
for (column_index, array) in batch.columns().iter().enumerate() {
|
|
619
|
-
let scalar = ScalarValue::try_from_array(array.as_ref(), row_index)
|
|
620
|
-
.map_err(datafusion_error_to_lix_error)?;
|
|
621
|
-
let field = result_fields.get(column_index);
|
|
622
|
-
row.push(scalar_value_to_lix_value(&scalar, field)?);
|
|
623
|
-
}
|
|
624
|
-
rows.push(row);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
Ok(SqlQueryResult {
|
|
629
|
-
rows,
|
|
630
|
-
columns: result_columns.to_vec(),
|
|
631
|
-
notices: Vec::new(),
|
|
632
|
-
})
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
fn history_filter_notices(plan: &LogicalPlan) -> Vec<LixNotice> {
|
|
636
|
-
let mut observations = Vec::new();
|
|
637
|
-
collect_notice_observations(plan, &Vec::new(), &mut observations);
|
|
638
|
-
|
|
639
|
-
let mut notices = Vec::new();
|
|
640
|
-
let mut emitted_codes = HashSet::<String>::new();
|
|
641
|
-
for observation in observations {
|
|
642
|
-
for rule in HISTORY_NOTICE_RULES {
|
|
643
|
-
if observation.table_name != rule.table_name {
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
if !observation.references_any(rule.payload_columns)
|
|
647
|
-
|| observation.references_any(rule.identity_columns)
|
|
648
|
-
{
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
let code = format!("LIX_HISTORY_NON_IDENTITY_FILTER:{}", rule.table_name);
|
|
653
|
-
if emitted_codes.insert(code) {
|
|
654
|
-
notices.push(history_non_identity_filter_notice(rule.table_name));
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
notices
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
#[derive(Debug)]
|
|
662
|
-
struct NoticeObservation {
|
|
663
|
-
table_name: String,
|
|
664
|
-
filter_columns: HashSet<String>,
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
impl NoticeObservation {
|
|
668
|
-
fn references_any(&self, columns: &[&str]) -> bool {
|
|
669
|
-
columns
|
|
670
|
-
.iter()
|
|
671
|
-
.any(|column| self.filter_columns.contains(*column))
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
struct HistoryNoticeRule {
|
|
676
|
-
table_name: &'static str,
|
|
677
|
-
payload_columns: &'static [&'static str],
|
|
678
|
-
identity_columns: &'static [&'static str],
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const HISTORY_NOTICE_RULES: &[HistoryNoticeRule] = &[
|
|
682
|
-
HistoryNoticeRule {
|
|
683
|
-
table_name: "lix_file_history",
|
|
684
|
-
payload_columns: &["path", "directory_id", "name", "hidden", "data"],
|
|
685
|
-
identity_columns: &["id", "lixcol_entity_id"],
|
|
686
|
-
},
|
|
687
|
-
HistoryNoticeRule {
|
|
688
|
-
table_name: "lix_directory_history",
|
|
689
|
-
payload_columns: &["path", "parent_id", "name", "hidden"],
|
|
690
|
-
identity_columns: &["id", "lixcol_entity_id"],
|
|
691
|
-
},
|
|
692
|
-
];
|
|
693
|
-
|
|
694
|
-
fn collect_notice_observations(
|
|
695
|
-
plan: &LogicalPlan,
|
|
696
|
-
active_filter_columns: &Vec<HashSet<String>>,
|
|
697
|
-
observations: &mut Vec<NoticeObservation>,
|
|
698
|
-
) {
|
|
699
|
-
match plan {
|
|
700
|
-
LogicalPlan::Filter(filter) => {
|
|
701
|
-
let mut next_filters = active_filter_columns.clone();
|
|
702
|
-
next_filters.push(expr_column_names(&filter.predicate));
|
|
703
|
-
collect_notice_observations(&filter.input, &next_filters, observations);
|
|
704
|
-
}
|
|
705
|
-
LogicalPlan::TableScan(scan) => {
|
|
706
|
-
let mut filter_columns = HashSet::new();
|
|
707
|
-
for columns in active_filter_columns {
|
|
708
|
-
filter_columns.extend(columns.iter().cloned());
|
|
709
|
-
}
|
|
710
|
-
for filter in &scan.filters {
|
|
711
|
-
filter_columns.extend(expr_column_names(filter));
|
|
712
|
-
}
|
|
713
|
-
if !filter_columns.is_empty() {
|
|
714
|
-
observations.push(NoticeObservation {
|
|
715
|
-
table_name: table_reference_name(&scan.table_name),
|
|
716
|
-
filter_columns,
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
other => {
|
|
721
|
-
for input in other.inputs() {
|
|
722
|
-
collect_notice_observations(input, active_filter_columns, observations);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
fn expr_column_names(expr: &Expr) -> HashSet<String> {
|
|
729
|
-
expr.column_refs()
|
|
730
|
-
.iter()
|
|
731
|
-
.map(|column| column.name.clone())
|
|
732
|
-
.collect()
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
fn table_reference_name(table: &datafusion::common::TableReference) -> String {
|
|
736
|
-
match table {
|
|
737
|
-
datafusion::common::TableReference::Bare { table } => table.to_string(),
|
|
738
|
-
datafusion::common::TableReference::Partial { table, .. } => table.to_string(),
|
|
739
|
-
datafusion::common::TableReference::Full { table, .. } => table.to_string(),
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
fn history_non_identity_filter_notice(view_name: &str) -> LixNotice {
|
|
744
|
-
LixNotice {
|
|
745
|
-
code: "LIX_HISTORY_NON_IDENTITY_FILTER".to_string(),
|
|
746
|
-
message: format!("{view_name} was filtered without an identity predicate."),
|
|
747
|
-
hint: Some(
|
|
748
|
-
"Filter by id or lixcol_entity_id to include tombstones and renamed history."
|
|
749
|
-
.to_string(),
|
|
750
|
-
),
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
fn scalar_value_to_lix_value(
|
|
755
|
-
value: &ScalarValue,
|
|
756
|
-
field: Option<&Field>,
|
|
757
|
-
) -> Result<Value, LixError> {
|
|
758
|
-
match value {
|
|
759
|
-
ScalarValue::Null => Ok(Value::Null),
|
|
760
|
-
ScalarValue::Boolean(Some(value)) => Ok(Value::Boolean(*value)),
|
|
761
|
-
ScalarValue::Boolean(None) => Ok(Value::Null),
|
|
762
|
-
ScalarValue::Int8(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
763
|
-
ScalarValue::Int8(None) => Ok(Value::Null),
|
|
764
|
-
ScalarValue::Int16(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
765
|
-
ScalarValue::Int16(None) => Ok(Value::Null),
|
|
766
|
-
ScalarValue::Int32(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
767
|
-
ScalarValue::Int32(None) => Ok(Value::Null),
|
|
768
|
-
ScalarValue::Int64(Some(value)) => Ok(Value::Integer(*value)),
|
|
769
|
-
ScalarValue::Int64(None) => Ok(Value::Null),
|
|
770
|
-
ScalarValue::UInt8(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
771
|
-
ScalarValue::UInt8(None) => Ok(Value::Null),
|
|
772
|
-
ScalarValue::UInt16(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
773
|
-
ScalarValue::UInt16(None) => Ok(Value::Null),
|
|
774
|
-
ScalarValue::UInt32(Some(value)) => Ok(Value::Integer(i64::from(*value))),
|
|
775
|
-
ScalarValue::UInt32(None) => Ok(Value::Null),
|
|
776
|
-
ScalarValue::UInt64(Some(value)) => match i64::try_from(*value) {
|
|
777
|
-
Ok(value) => Ok(Value::Integer(value)),
|
|
778
|
-
Err(_) => Ok(Value::Text(value.to_string())),
|
|
779
|
-
},
|
|
780
|
-
ScalarValue::UInt64(None) => Ok(Value::Null),
|
|
781
|
-
ScalarValue::Float32(Some(value)) => Ok(Value::Real(f64::from(*value))),
|
|
782
|
-
ScalarValue::Float32(None) => Ok(Value::Null),
|
|
783
|
-
ScalarValue::Float64(Some(value)) => Ok(Value::Real(*value)),
|
|
784
|
-
ScalarValue::Float64(None) => Ok(Value::Null),
|
|
785
|
-
ScalarValue::Utf8(Some(value))
|
|
786
|
-
| ScalarValue::Utf8View(Some(value))
|
|
787
|
-
| ScalarValue::LargeUtf8(Some(value)) => string_scalar_to_lix_value(value, field),
|
|
788
|
-
ScalarValue::Utf8(None) | ScalarValue::Utf8View(None) | ScalarValue::LargeUtf8(None) => {
|
|
789
|
-
Ok(Value::Null)
|
|
790
|
-
}
|
|
791
|
-
ScalarValue::Binary(Some(value)) | ScalarValue::LargeBinary(Some(value)) => {
|
|
792
|
-
Ok(Value::Blob(value.clone()))
|
|
793
|
-
}
|
|
794
|
-
ScalarValue::Binary(None) | ScalarValue::LargeBinary(None) => Ok(Value::Null),
|
|
795
|
-
other => Ok(Value::Text(other.to_string())),
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
fn string_scalar_to_lix_value(value: &str, field: Option<&Field>) -> Result<Value, LixError> {
|
|
800
|
-
if field.is_some_and(field_is_json) {
|
|
801
|
-
return serde_json::from_str::<serde_json::Value>(value)
|
|
802
|
-
.map(Value::Json)
|
|
803
|
-
.map_err(|error| {
|
|
804
|
-
LixError::new(
|
|
805
|
-
"LIX_ERROR_INVALID_JSON",
|
|
806
|
-
format!(
|
|
807
|
-
"column '{}' is marked as JSON but contains invalid JSON: {error}",
|
|
808
|
-
field
|
|
809
|
-
.map(|field| field.name().as_str())
|
|
810
|
-
.unwrap_or("<unknown>")
|
|
811
|
-
),
|
|
812
|
-
)
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
Ok(Value::Text(value.to_string()))
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
#[cfg(test)]
|
|
819
|
-
mod tests {
|
|
820
|
-
use std::sync::{Arc, Mutex};
|
|
821
|
-
|
|
822
|
-
use async_trait::async_trait;
|
|
823
|
-
use serde_json::json;
|
|
824
|
-
use serde_json::Value as JsonValue;
|
|
825
|
-
|
|
826
|
-
use super::{
|
|
827
|
-
create_write_logical_plan, execute_logical_plan, execute_sql, SqlExecutionContext,
|
|
828
|
-
SqlWriteExecutionContext,
|
|
829
|
-
};
|
|
830
|
-
use crate::binary_cas::BlobDataReader;
|
|
831
|
-
use crate::commit_graph::{
|
|
832
|
-
CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
|
|
833
|
-
CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
|
|
834
|
-
};
|
|
835
|
-
use crate::commit_store::CommitStoreContext;
|
|
836
|
-
use crate::functions::{
|
|
837
|
-
FunctionProvider, FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
|
|
838
|
-
};
|
|
839
|
-
use crate::json_store::JsonStoreContext;
|
|
840
|
-
use crate::live_state::{
|
|
841
|
-
LiveStateContext, LiveStateReader, LiveStateRowRequest, LiveStateScanRequest,
|
|
842
|
-
MaterializedLiveStateRow,
|
|
843
|
-
};
|
|
844
|
-
use crate::sql2::{CommitStoreQuerySource, SqlCommitStoreQuerySource};
|
|
845
|
-
use crate::storage::{
|
|
846
|
-
KvEntryPage, KvExistsBatch, KvGetRequest, KvKeyPage, KvScanRequest, KvValueBatch,
|
|
847
|
-
KvValuePage, StorageContext, StorageReadScope, StorageReadTransaction, StorageReader,
|
|
848
|
-
StorageWriteSet,
|
|
849
|
-
};
|
|
850
|
-
use crate::tracked_state::TrackedStateContext;
|
|
851
|
-
use crate::transaction::prepare_version_ref_row;
|
|
852
|
-
use crate::transaction::types::{
|
|
853
|
-
TransactionWrite, TransactionWriteOutcome, TransactionWriteRow,
|
|
854
|
-
};
|
|
855
|
-
use crate::untracked_state::UntrackedStateContext;
|
|
856
|
-
use crate::version::VersionRefReader;
|
|
857
|
-
use crate::{Engine, ExecuteResult, SessionContext};
|
|
858
|
-
use crate::{LixError, Value};
|
|
859
|
-
|
|
860
|
-
struct DummyBlobReader;
|
|
861
|
-
struct DummyLiveStateReader;
|
|
862
|
-
struct RowsLiveStateReader {
|
|
863
|
-
rows: Vec<MaterializedLiveStateRow>,
|
|
864
|
-
}
|
|
865
|
-
struct BackendBlobReader(StorageContext);
|
|
866
|
-
struct DummyCommitGraphReader;
|
|
867
|
-
struct DummyVersionRefReader;
|
|
868
|
-
struct TestReadTransaction(StorageContext);
|
|
869
|
-
|
|
870
|
-
fn test_read_scope(
|
|
871
|
-
storage: StorageContext,
|
|
872
|
-
) -> StorageReadScope<Box<dyn StorageReadTransaction + Send + Sync + 'static>> {
|
|
873
|
-
StorageReadScope::new(Box::new(TestReadTransaction(storage)))
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
#[async_trait]
|
|
877
|
-
impl StorageReader for TestReadTransaction {
|
|
878
|
-
async fn get_values(&mut self, request: KvGetRequest) -> Result<KvValueBatch, LixError> {
|
|
879
|
-
self.0.get_values(request).await
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
async fn exists_many(&mut self, request: KvGetRequest) -> Result<KvExistsBatch, LixError> {
|
|
883
|
-
self.0.exists_many(request).await
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
async fn scan_keys(&mut self, request: KvScanRequest) -> Result<KvKeyPage, LixError> {
|
|
887
|
-
self.0.scan_keys(request).await
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
async fn scan_values(&mut self, request: KvScanRequest) -> Result<KvValuePage, LixError> {
|
|
891
|
-
self.0.scan_values(request).await
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
async fn scan_entries(&mut self, request: KvScanRequest) -> Result<KvEntryPage, LixError> {
|
|
895
|
-
self.0.scan_entries(request).await
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
#[async_trait]
|
|
900
|
-
impl StorageReadTransaction for TestReadTransaction {
|
|
901
|
-
async fn rollback(self: Box<Self>) -> Result<(), LixError> {
|
|
902
|
-
Ok(())
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
#[allow(dead_code)]
|
|
907
|
-
fn test_functions() -> FunctionProviderHandle {
|
|
908
|
-
SharedFunctionProvider::new(
|
|
909
|
-
Box::new(SystemFunctionProvider) as Box<dyn FunctionProvider + Send>
|
|
910
|
-
)
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
#[derive(Default)]
|
|
914
|
-
struct CapturingStagedWrites {
|
|
915
|
-
deltas: Vec<CapturedStageWrite>,
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
#[derive(Clone)]
|
|
919
|
-
struct CapturedStageWrite {
|
|
920
|
-
rows: Vec<TransactionWriteRow>,
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
impl CapturedStageWrite {
|
|
924
|
-
fn pending_write_overlay(&self) -> Result<CapturedStageOverlay, LixError> {
|
|
925
|
-
Ok(CapturedStageOverlay {
|
|
926
|
-
rows: self.rows.clone(),
|
|
927
|
-
})
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
struct CapturedStageOverlay {
|
|
932
|
-
rows: Vec<TransactionWriteRow>,
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
impl CapturedStageOverlay {
|
|
936
|
-
fn visible_semantic_rows(
|
|
937
|
-
&self,
|
|
938
|
-
include_tombstones: bool,
|
|
939
|
-
schema_key: &str,
|
|
940
|
-
) -> Vec<CapturedStageRow> {
|
|
941
|
-
self.visible_all_semantic_rows()
|
|
942
|
-
.into_iter()
|
|
943
|
-
.filter(|row| row.schema_key == schema_key)
|
|
944
|
-
.filter(|row| include_tombstones || !row.tombstone)
|
|
945
|
-
.collect()
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
fn visible_all_semantic_rows(&self) -> Vec<CapturedStageRow> {
|
|
949
|
-
self.rows
|
|
950
|
-
.iter()
|
|
951
|
-
.cloned()
|
|
952
|
-
.map(CapturedStageRow::from)
|
|
953
|
-
.collect()
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
struct CapturedStageRow {
|
|
958
|
-
entity_id: String,
|
|
959
|
-
schema_key: String,
|
|
960
|
-
version_id: String,
|
|
961
|
-
file_id: Option<String>,
|
|
962
|
-
snapshot_content: Option<String>,
|
|
963
|
-
metadata: Option<String>,
|
|
964
|
-
global: bool,
|
|
965
|
-
untracked: bool,
|
|
966
|
-
tombstone: bool,
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
impl From<TransactionWriteRow> for CapturedStageRow {
|
|
970
|
-
fn from(row: TransactionWriteRow) -> Self {
|
|
971
|
-
Self {
|
|
972
|
-
entity_id: row
|
|
973
|
-
.entity_id
|
|
974
|
-
.expect("captured staged row should carry entity_id")
|
|
975
|
-
.as_json_array_text()
|
|
976
|
-
.expect("captured staged row should project entity_id"),
|
|
977
|
-
schema_key: row.schema_key,
|
|
978
|
-
version_id: row.version_id,
|
|
979
|
-
file_id: row.file_id,
|
|
980
|
-
global: row.global,
|
|
981
|
-
untracked: row.untracked,
|
|
982
|
-
tombstone: row.snapshot.is_none(),
|
|
983
|
-
snapshot_content: row.snapshot.map(|snapshot| snapshot.to_string()),
|
|
984
|
-
metadata: row.metadata.map(|metadata| metadata.to_string()),
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
struct DummySqlExecutionContext<'a> {
|
|
990
|
-
active_version_id: &'a str,
|
|
991
|
-
blob_reader: Arc<dyn BlobDataReader>,
|
|
992
|
-
live_state: Arc<dyn LiveStateReader>,
|
|
993
|
-
schema_definitions: Vec<JsonValue>,
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
impl<'a> SqlExecutionContext for DummySqlExecutionContext<'a> {
|
|
997
|
-
fn active_version_id(&self) -> &str {
|
|
998
|
-
self.active_version_id
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
fn live_state(&self) -> Arc<dyn LiveStateReader> {
|
|
1002
|
-
Arc::clone(&self.live_state)
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
fn functions(&self) -> FunctionProviderHandle {
|
|
1006
|
-
test_functions()
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
|
|
1010
|
-
Arc::clone(&self.blob_reader)
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
|
|
1014
|
-
let base_scope = test_read_scope(StorageContext::new(Arc::new(
|
|
1015
|
-
crate::backend::testing::UnitTestBackend::new(),
|
|
1016
|
-
)));
|
|
1017
|
-
let read_scope = StorageReadScope::new(base_scope.store());
|
|
1018
|
-
CommitStoreQuerySource {
|
|
1019
|
-
commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
|
|
1020
|
-
json_reader: JsonStoreContext::new().reader(read_scope.store()),
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
|
|
1025
|
-
Box::new(DummyCommitGraphReader)
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
fn version_ref(&self) -> Arc<dyn VersionRefReader> {
|
|
1029
|
-
Arc::new(DummyVersionRefReader)
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
|
|
1033
|
-
Ok(self.schema_definitions.clone())
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
struct DummySqlWriteExecutionContext<'a> {
|
|
1038
|
-
active_version_id: &'a str,
|
|
1039
|
-
blob_reader: Arc<dyn BlobDataReader>,
|
|
1040
|
-
live_state: Arc<dyn LiveStateReader>,
|
|
1041
|
-
staged_writes: Arc<Mutex<CapturingStagedWrites>>,
|
|
1042
|
-
schema_definitions: Vec<JsonValue>,
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
#[async_trait]
|
|
1046
|
-
impl SqlWriteExecutionContext for DummySqlWriteExecutionContext<'_> {
|
|
1047
|
-
fn active_version_id(&self) -> &str {
|
|
1048
|
-
self.active_version_id
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
fn functions(&self) -> FunctionProviderHandle {
|
|
1052
|
-
test_functions()
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
|
|
1056
|
-
Ok(self.schema_definitions.clone())
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
async fn load_bytes_many(
|
|
1060
|
-
&mut self,
|
|
1061
|
-
hashes: &[crate::binary_cas::BlobHash],
|
|
1062
|
-
) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
|
|
1063
|
-
self.blob_reader.load_bytes_many(hashes).await
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
async fn scan_live_state(
|
|
1067
|
-
&mut self,
|
|
1068
|
-
request: &LiveStateScanRequest,
|
|
1069
|
-
) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
|
|
1070
|
-
self.live_state.scan_rows(request).await
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
async fn load_version_head(
|
|
1074
|
-
&mut self,
|
|
1075
|
-
version_id: &str,
|
|
1076
|
-
) -> Result<Option<String>, LixError> {
|
|
1077
|
-
Ok(Some(format!("commit-{version_id}")))
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
async fn stage_write(
|
|
1081
|
-
&mut self,
|
|
1082
|
-
write: TransactionWrite,
|
|
1083
|
-
) -> Result<TransactionWriteOutcome, LixError> {
|
|
1084
|
-
let count = match &write {
|
|
1085
|
-
TransactionWrite::Rows { rows, .. } => rows.len() as u64,
|
|
1086
|
-
TransactionWrite::RowsWithFileData { count, .. } => *count,
|
|
1087
|
-
TransactionWrite::AdoptedChanges { changes } => changes.len() as u64,
|
|
1088
|
-
};
|
|
1089
|
-
let rows = match write {
|
|
1090
|
-
TransactionWrite::Rows { rows, .. } => rows,
|
|
1091
|
-
TransactionWrite::RowsWithFileData { rows, .. } => rows,
|
|
1092
|
-
TransactionWrite::AdoptedChanges { .. } => Vec::new(),
|
|
1093
|
-
};
|
|
1094
|
-
self.staged_writes
|
|
1095
|
-
.lock()
|
|
1096
|
-
.expect("staged writes lock")
|
|
1097
|
-
.deltas
|
|
1098
|
-
.push(CapturedStageWrite { rows });
|
|
1099
|
-
Ok(TransactionWriteOutcome { count })
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
async fn execute_write_sql(
|
|
1104
|
-
ctx: &mut dyn SqlWriteExecutionContext,
|
|
1105
|
-
sql: &str,
|
|
1106
|
-
params: &[Value],
|
|
1107
|
-
) -> Result<crate::SqlQueryResult, LixError> {
|
|
1108
|
-
let plan = create_write_logical_plan(ctx, sql).await?;
|
|
1109
|
-
execute_logical_plan(plan, params).await
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
#[async_trait]
|
|
1113
|
-
impl VersionRefReader for DummyVersionRefReader {
|
|
1114
|
-
async fn load_head(
|
|
1115
|
-
&self,
|
|
1116
|
-
_version_id: &str,
|
|
1117
|
-
) -> Result<Option<crate::version::VersionHead>, LixError> {
|
|
1118
|
-
Ok(None)
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async fn scan_heads(&self) -> Result<Vec<crate::version::VersionHead>, LixError> {
|
|
1122
|
-
Ok(Vec::new())
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
#[async_trait]
|
|
1127
|
-
impl CommitGraphReader for DummyCommitGraphReader {
|
|
1128
|
-
async fn load_commit(
|
|
1129
|
-
&mut self,
|
|
1130
|
-
_commit_id: &str,
|
|
1131
|
-
) -> Result<Option<CommitGraphCommit>, LixError> {
|
|
1132
|
-
Ok(None)
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
1136
|
-
Ok(Vec::new())
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
async fn reachable_commits(
|
|
1140
|
-
&mut self,
|
|
1141
|
-
_head_commit_id: &str,
|
|
1142
|
-
) -> Result<Vec<ReachableCommitGraphCommit>, LixError> {
|
|
1143
|
-
Ok(Vec::new())
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
async fn best_common_ancestors(
|
|
1147
|
-
&mut self,
|
|
1148
|
-
_left_commit_id: &str,
|
|
1149
|
-
_right_commit_id: &str,
|
|
1150
|
-
) -> Result<Vec<CommitGraphCommit>, LixError> {
|
|
1151
|
-
Ok(Vec::new())
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
async fn merge_base(
|
|
1155
|
-
&mut self,
|
|
1156
|
-
_left_commit_id: &str,
|
|
1157
|
-
_right_commit_id: &str,
|
|
1158
|
-
) -> Result<CommitGraphCommit, LixError> {
|
|
1159
|
-
Err(LixError::new(
|
|
1160
|
-
"LIX_ERROR_UNKNOWN",
|
|
1161
|
-
"dummy commit graph reader cannot resolve merge base",
|
|
1162
|
-
))
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
fn commit_edges(&self, _commits: &[CommitGraphCommit]) -> Vec<CommitGraphEdge> {
|
|
1166
|
-
Vec::new()
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
async fn change_history_from_commit(
|
|
1170
|
-
&mut self,
|
|
1171
|
-
_start_commit_id: &str,
|
|
1172
|
-
_request: &CommitGraphChangeHistoryRequest,
|
|
1173
|
-
) -> Result<Vec<CommitGraphChangeHistoryEntry>, LixError> {
|
|
1174
|
-
Ok(Vec::new())
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
#[async_trait]
|
|
1179
|
-
impl LiveStateReader for DummyLiveStateReader {
|
|
1180
|
-
async fn scan_rows(
|
|
1181
|
-
&self,
|
|
1182
|
-
_request: &LiveStateScanRequest,
|
|
1183
|
-
) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
|
|
1184
|
-
Ok(vec![])
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
async fn load_row(
|
|
1188
|
-
&self,
|
|
1189
|
-
_request: &LiveStateRowRequest,
|
|
1190
|
-
) -> Result<Option<MaterializedLiveStateRow>, LixError> {
|
|
1191
|
-
Ok(None)
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
#[async_trait]
|
|
1196
|
-
impl LiveStateReader for RowsLiveStateReader {
|
|
1197
|
-
async fn scan_rows(
|
|
1198
|
-
&self,
|
|
1199
|
-
_request: &LiveStateScanRequest,
|
|
1200
|
-
) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
|
|
1201
|
-
Ok(self.rows.clone())
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
async fn load_row(
|
|
1205
|
-
&self,
|
|
1206
|
-
_request: &LiveStateRowRequest,
|
|
1207
|
-
) -> Result<Option<MaterializedLiveStateRow>, LixError> {
|
|
1208
|
-
Ok(None)
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
#[async_trait]
|
|
1213
|
-
impl BlobDataReader for DummyBlobReader {
|
|
1214
|
-
async fn load_bytes_many(
|
|
1215
|
-
&self,
|
|
1216
|
-
hashes: &[crate::binary_cas::BlobHash],
|
|
1217
|
-
) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
|
|
1218
|
-
Ok(crate::binary_cas::BlobBytesBatch::new(vec![
|
|
1219
|
-
None;
|
|
1220
|
-
hashes.len()
|
|
1221
|
-
]))
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
#[async_trait]
|
|
1226
|
-
impl BlobDataReader for BackendBlobReader {
|
|
1227
|
-
async fn load_bytes_many(
|
|
1228
|
-
&self,
|
|
1229
|
-
hashes: &[crate::binary_cas::BlobHash],
|
|
1230
|
-
) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
|
|
1231
|
-
let binary_cas = crate::binary_cas::BinaryCasContext::new();
|
|
1232
|
-
let reader = binary_cas.reader(self.0.clone());
|
|
1233
|
-
reader.load_bytes_many(hashes).await
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
fn live_lix_state_row(entity_id: &str, metadata: Option<&str>) -> MaterializedLiveStateRow {
|
|
1238
|
-
MaterializedLiveStateRow {
|
|
1239
|
-
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
1240
|
-
schema_key: "lix_key_value".to_string(),
|
|
1241
|
-
file_id: None,
|
|
1242
|
-
snapshot_content: Some("{\"key\":\"hello\",\"value\":\"world\"}".to_string()),
|
|
1243
|
-
metadata: metadata.map(str::to_string),
|
|
1244
|
-
deleted: false,
|
|
1245
|
-
version_id: "version-a".to_string(),
|
|
1246
|
-
change_id: Some(format!("change-{entity_id}")),
|
|
1247
|
-
commit_id: Some(format!("commit-{entity_id}")),
|
|
1248
|
-
global: false,
|
|
1249
|
-
untracked: false,
|
|
1250
|
-
created_at: "2026-04-23T00:00:00Z".to_string(),
|
|
1251
|
-
updated_at: "2026-04-23T01:00:00Z".to_string(),
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
fn live_entity_row(entity_id: &str, version_id: &str, value: &str) -> MaterializedLiveStateRow {
|
|
1256
|
-
MaterializedLiveStateRow {
|
|
1257
|
-
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
1258
|
-
schema_key: "test_state_schema".to_string(),
|
|
1259
|
-
file_id: None,
|
|
1260
|
-
snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
|
|
1261
|
-
metadata: Some(json!({ "source": entity_id }).to_string()),
|
|
1262
|
-
deleted: false,
|
|
1263
|
-
version_id: version_id.to_string(),
|
|
1264
|
-
change_id: Some(format!("change-{entity_id}")),
|
|
1265
|
-
commit_id: Some(format!("commit-{entity_id}")),
|
|
1266
|
-
global: false,
|
|
1267
|
-
untracked: false,
|
|
1268
|
-
created_at: "2026-04-23T00:00:00Z".to_string(),
|
|
1269
|
-
updated_at: "2026-04-23T01:00:00Z".to_string(),
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
fn live_directory_row(
|
|
1274
|
-
entity_id: &str,
|
|
1275
|
-
version_id: &str,
|
|
1276
|
-
parent_id: Option<&str>,
|
|
1277
|
-
name: &str,
|
|
1278
|
-
hidden: bool,
|
|
1279
|
-
) -> MaterializedLiveStateRow {
|
|
1280
|
-
MaterializedLiveStateRow {
|
|
1281
|
-
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
1282
|
-
schema_key: "lix_directory_descriptor".to_string(),
|
|
1283
|
-
file_id: None,
|
|
1284
|
-
snapshot_content: Some(
|
|
1285
|
-
json!({
|
|
1286
|
-
"id": entity_id,
|
|
1287
|
-
"parent_id": parent_id,
|
|
1288
|
-
"name": name,
|
|
1289
|
-
"hidden": hidden
|
|
1290
|
-
})
|
|
1291
|
-
.to_string(),
|
|
1292
|
-
),
|
|
1293
|
-
metadata: Some(json!({ "source": entity_id }).to_string()),
|
|
1294
|
-
deleted: false,
|
|
1295
|
-
version_id: version_id.to_string(),
|
|
1296
|
-
change_id: Some(format!("change-{entity_id}")),
|
|
1297
|
-
commit_id: Some(format!("commit-{entity_id}")),
|
|
1298
|
-
global: false,
|
|
1299
|
-
untracked: false,
|
|
1300
|
-
created_at: "2026-04-23T00:00:00Z".to_string(),
|
|
1301
|
-
updated_at: "2026-04-23T01:00:00Z".to_string(),
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
fn live_file_row(
|
|
1306
|
-
entity_id: &str,
|
|
1307
|
-
version_id: &str,
|
|
1308
|
-
directory_id: Option<&str>,
|
|
1309
|
-
name: &str,
|
|
1310
|
-
hidden: bool,
|
|
1311
|
-
) -> MaterializedLiveStateRow {
|
|
1312
|
-
MaterializedLiveStateRow {
|
|
1313
|
-
entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
|
|
1314
|
-
schema_key: "lix_file_descriptor".to_string(),
|
|
1315
|
-
file_id: None,
|
|
1316
|
-
snapshot_content: Some(
|
|
1317
|
-
json!({
|
|
1318
|
-
"id": entity_id,
|
|
1319
|
-
"directory_id": directory_id,
|
|
1320
|
-
"name": name,
|
|
1321
|
-
"hidden": hidden
|
|
1322
|
-
})
|
|
1323
|
-
.to_string(),
|
|
1324
|
-
),
|
|
1325
|
-
metadata: Some(json!({ "source": entity_id }).to_string()),
|
|
1326
|
-
deleted: false,
|
|
1327
|
-
version_id: version_id.to_string(),
|
|
1328
|
-
change_id: Some(format!("change-{entity_id}")),
|
|
1329
|
-
commit_id: Some(format!("commit-{entity_id}")),
|
|
1330
|
-
global: false,
|
|
1331
|
-
untracked: false,
|
|
1332
|
-
created_at: "2026-04-23T00:00:00Z".to_string(),
|
|
1333
|
-
updated_at: "2026-04-23T01:00:00Z".to_string(),
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
#[tokio::test]
|
|
1338
|
-
async fn sql_execution_context_exposes_live_state_and_blob_reader() {
|
|
1339
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1340
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1341
|
-
let ctx = DummySqlExecutionContext {
|
|
1342
|
-
active_version_id: "version-a",
|
|
1343
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
1344
|
-
live_state: Arc::clone(&live_state) as Arc<dyn LiveStateReader>,
|
|
1345
|
-
schema_definitions: vec![],
|
|
1346
|
-
};
|
|
1347
|
-
|
|
1348
|
-
let actual = ctx.live_state();
|
|
1349
|
-
let expected = live_state as Arc<dyn LiveStateReader>;
|
|
1350
|
-
assert_eq!(ctx.active_version_id(), "version-a");
|
|
1351
|
-
assert!(Arc::ptr_eq(&actual, &expected));
|
|
1352
|
-
assert!(Arc::ptr_eq(&ctx.blob_reader(), &blob_reader));
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
#[tokio::test]
|
|
1356
|
-
async fn execute_sql_uses_execution_context_boundary() {
|
|
1357
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1358
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1359
|
-
let ctx = DummySqlExecutionContext {
|
|
1360
|
-
active_version_id: "version-a",
|
|
1361
|
-
blob_reader,
|
|
1362
|
-
live_state,
|
|
1363
|
-
schema_definitions: vec![],
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
|
-
let result = execute_sql(&ctx, "SELECT 1", &[])
|
|
1367
|
-
.await
|
|
1368
|
-
.expect("sql2 execute should support literal-only queries");
|
|
1369
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
#[tokio::test]
|
|
1373
|
-
async fn execute_sql_collects_union_all_partitions() {
|
|
1374
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1375
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1376
|
-
let ctx = DummySqlExecutionContext {
|
|
1377
|
-
active_version_id: "version-a",
|
|
1378
|
-
blob_reader,
|
|
1379
|
-
live_state,
|
|
1380
|
-
schema_definitions: vec![],
|
|
1381
|
-
};
|
|
1382
|
-
|
|
1383
|
-
let result = execute_sql(&ctx, "SELECT 1 UNION ALL SELECT 2", &[])
|
|
1384
|
-
.await
|
|
1385
|
-
.expect("sql2 execute should collect UNION ALL partitions");
|
|
1386
|
-
assert_eq!(
|
|
1387
|
-
result.rows,
|
|
1388
|
-
vec![vec![Value::Integer(1)], vec![Value::Integer(2)]]
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
#[tokio::test]
|
|
1393
|
-
async fn execute_sql_rejects_extra_parameters() {
|
|
1394
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1395
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1396
|
-
let ctx = DummySqlExecutionContext {
|
|
1397
|
-
active_version_id: "version-a",
|
|
1398
|
-
blob_reader,
|
|
1399
|
-
live_state,
|
|
1400
|
-
schema_definitions: vec![],
|
|
1401
|
-
};
|
|
1402
|
-
|
|
1403
|
-
let error = execute_sql(
|
|
1404
|
-
&ctx,
|
|
1405
|
-
"SELECT $1 AS value",
|
|
1406
|
-
&[Value::Integer(1), Value::Integer(2)],
|
|
1407
|
-
)
|
|
1408
|
-
.await
|
|
1409
|
-
.expect_err("extra params should fail instead of being ignored");
|
|
1410
|
-
|
|
1411
|
-
assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
|
|
1412
|
-
assert_eq!(
|
|
1413
|
-
error.message,
|
|
1414
|
-
"SQL expected 1 parameter(s), but 2 parameter(s) were provided"
|
|
1415
|
-
);
|
|
1416
|
-
assert_eq!(
|
|
1417
|
-
error.details,
|
|
1418
|
-
Some(json!({
|
|
1419
|
-
"operation": "execute",
|
|
1420
|
-
"expected_param_count": 1,
|
|
1421
|
-
"provided_param_count": 2,
|
|
1422
|
-
"placeholders": ["$1"],
|
|
1423
|
-
}))
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
#[tokio::test]
|
|
1428
|
-
async fn execute_sql_exposes_datafusion_information_schema() {
|
|
1429
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1430
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1431
|
-
let ctx = DummySqlExecutionContext {
|
|
1432
|
-
active_version_id: "version-a",
|
|
1433
|
-
blob_reader,
|
|
1434
|
-
live_state,
|
|
1435
|
-
schema_definitions: vec![],
|
|
1436
|
-
};
|
|
1437
|
-
|
|
1438
|
-
let information_schema_result = execute_sql(
|
|
1439
|
-
&ctx,
|
|
1440
|
-
"SELECT table_name FROM information_schema.tables WHERE table_name = 'lix_state'",
|
|
1441
|
-
&[],
|
|
1442
|
-
)
|
|
1443
|
-
.await
|
|
1444
|
-
.expect("information_schema.tables should be enabled");
|
|
1445
|
-
assert_eq!(
|
|
1446
|
-
information_schema_result.rows,
|
|
1447
|
-
vec![vec![Value::Text("lix_state".to_string())]]
|
|
1448
|
-
);
|
|
1449
|
-
|
|
1450
|
-
let tables_result = execute_sql(
|
|
1451
|
-
&ctx,
|
|
1452
|
-
"SELECT table_name FROM information_schema.tables",
|
|
1453
|
-
&[],
|
|
1454
|
-
)
|
|
1455
|
-
.await
|
|
1456
|
-
.expect("information_schema.tables should list registered tables");
|
|
1457
|
-
assert!(tables_result.rows.iter().any(|row| {
|
|
1458
|
-
row.iter()
|
|
1459
|
-
.any(|value| matches!(value, Value::Text(value) if value == "lix_state"))
|
|
1460
|
-
}));
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
async fn setup_engine_history_fixture() -> Result<(SessionContext, String), LixError> {
|
|
1464
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
1465
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone())).await?;
|
|
1466
|
-
let engine = Engine::new(Box::new(backend)).await?;
|
|
1467
|
-
let session = engine.open_session(init_receipt.main_version_id).await?;
|
|
1468
|
-
|
|
1469
|
-
session
|
|
1470
|
-
.execute(
|
|
1471
|
-
"INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
|
|
1472
|
-
VALUES (\
|
|
1473
|
-
lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}},\"required\":[\"value\",\"count\"],\"additionalProperties\":false}'),\
|
|
1474
|
-
false,\
|
|
1475
|
-
false\
|
|
1476
|
-
)",
|
|
1477
|
-
&[],
|
|
1478
|
-
)
|
|
1479
|
-
.await?;
|
|
1480
|
-
session
|
|
1481
|
-
.execute(
|
|
1482
|
-
"INSERT INTO test_state_schema \
|
|
1483
|
-
(lixcol_entity_id, value, count, lixcol_metadata, lixcol_untracked) \
|
|
1484
|
-
VALUES (lix_json('[\"entity-history\"]'), 'A', 7, '{\"source\":\"history\"}', false)",
|
|
1485
|
-
&[],
|
|
1486
|
-
)
|
|
1487
|
-
.await?;
|
|
1488
|
-
session
|
|
1489
|
-
.execute(
|
|
1490
|
-
"INSERT INTO lix_directory (id, path, hidden) \
|
|
1491
|
-
VALUES ('dir-docs', '/docs/', false)",
|
|
1492
|
-
&[],
|
|
1493
|
-
)
|
|
1494
|
-
.await?;
|
|
1495
|
-
session
|
|
1496
|
-
.execute(
|
|
1497
|
-
"INSERT INTO lix_file (id, path, data, hidden) \
|
|
1498
|
-
VALUES ('file-a', '/docs/readme.md', X'68656C6C6F', false)",
|
|
1499
|
-
&[],
|
|
1500
|
-
)
|
|
1501
|
-
.await?;
|
|
1502
|
-
|
|
1503
|
-
let active_version_id = session.active_version_id().await?;
|
|
1504
|
-
let head_commit_id = engine
|
|
1505
|
-
.load_version_head_commit_id(&active_version_id)
|
|
1506
|
-
.await?
|
|
1507
|
-
.ok_or_else(|| {
|
|
1508
|
-
LixError::new(
|
|
1509
|
-
"LIX_ERROR_UNKNOWN",
|
|
1510
|
-
"history fixture expected the session version to have a head commit",
|
|
1511
|
-
)
|
|
1512
|
-
})?;
|
|
1513
|
-
Ok((session, head_commit_id))
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
#[tokio::test]
|
|
1517
|
-
async fn lix_file_path_predicates_canonicalize_bound_values_like_writes() {
|
|
1518
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
1519
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone()))
|
|
1520
|
-
.await
|
|
1521
|
-
.expect("engine should initialize");
|
|
1522
|
-
let engine = Engine::new(Box::new(backend))
|
|
1523
|
-
.await
|
|
1524
|
-
.expect("engine should open");
|
|
1525
|
-
let session = engine
|
|
1526
|
-
.open_session(init_receipt.main_version_id)
|
|
1527
|
-
.await
|
|
1528
|
-
.expect("session should open");
|
|
1529
|
-
|
|
1530
|
-
session
|
|
1531
|
-
.execute(
|
|
1532
|
-
"INSERT INTO lix_file (id, path, data) VALUES ('file-nfc', $1, X'41')",
|
|
1533
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1534
|
-
)
|
|
1535
|
-
.await
|
|
1536
|
-
.expect("NFD path insert should canonicalize");
|
|
1537
|
-
|
|
1538
|
-
let nfd_result = session
|
|
1539
|
-
.execute(
|
|
1540
|
-
"SELECT id FROM lix_file WHERE path = $1",
|
|
1541
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1542
|
-
)
|
|
1543
|
-
.await
|
|
1544
|
-
.expect("NFD path predicate should canonicalize");
|
|
1545
|
-
assert_eq!(
|
|
1546
|
-
rows_from_execute_result(nfd_result).1,
|
|
1547
|
-
vec![vec![Value::Text("file-nfc".to_string())]]
|
|
1548
|
-
);
|
|
1549
|
-
|
|
1550
|
-
let percent_result = session
|
|
1551
|
-
.execute(
|
|
1552
|
-
"SELECT id FROM lix_file WHERE path = '/%43afe%CC%81.txt'",
|
|
1553
|
-
&[],
|
|
1554
|
-
)
|
|
1555
|
-
.await
|
|
1556
|
-
.expect("percent-encoded path predicate should canonicalize");
|
|
1557
|
-
assert_eq!(
|
|
1558
|
-
rows_from_execute_result(percent_result).1,
|
|
1559
|
-
vec![vec![Value::Text("file-nfc".to_string())]]
|
|
1560
|
-
);
|
|
1561
|
-
|
|
1562
|
-
let reversed_result = session
|
|
1563
|
-
.execute(
|
|
1564
|
-
"SELECT id FROM lix_file WHERE $1 = path",
|
|
1565
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1566
|
-
)
|
|
1567
|
-
.await
|
|
1568
|
-
.expect("reversed path predicate should canonicalize");
|
|
1569
|
-
assert_eq!(
|
|
1570
|
-
rows_from_execute_result(reversed_result).1,
|
|
1571
|
-
vec![vec![Value::Text("file-nfc".to_string())]]
|
|
1572
|
-
);
|
|
1573
|
-
|
|
1574
|
-
let or_result = session
|
|
1575
|
-
.execute(
|
|
1576
|
-
"SELECT id FROM lix_file WHERE path = $1 OR id = 'missing'",
|
|
1577
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1578
|
-
)
|
|
1579
|
-
.await
|
|
1580
|
-
.expect("OR path predicate should canonicalize");
|
|
1581
|
-
assert_eq!(
|
|
1582
|
-
rows_from_execute_result(or_result).1,
|
|
1583
|
-
vec![vec![Value::Text("file-nfc".to_string())]]
|
|
1584
|
-
);
|
|
1585
|
-
|
|
1586
|
-
let not_result = session
|
|
1587
|
-
.execute(
|
|
1588
|
-
"SELECT id FROM lix_file WHERE NOT (path = $1)",
|
|
1589
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1590
|
-
)
|
|
1591
|
-
.await
|
|
1592
|
-
.expect("NOT path predicate should canonicalize");
|
|
1593
|
-
assert!(rows_from_execute_result(not_result).1.is_empty());
|
|
1594
|
-
|
|
1595
|
-
let not_in_result = session
|
|
1596
|
-
.execute(
|
|
1597
|
-
"SELECT id FROM lix_file WHERE path NOT IN ($1)",
|
|
1598
|
-
&[Value::Text("/%43afe%CC%81.txt".to_string())],
|
|
1599
|
-
)
|
|
1600
|
-
.await
|
|
1601
|
-
.expect("NOT IN path predicate should canonicalize");
|
|
1602
|
-
assert!(rows_from_execute_result(not_in_result).1.is_empty());
|
|
1603
|
-
|
|
1604
|
-
let update_result = session
|
|
1605
|
-
.execute(
|
|
1606
|
-
"UPDATE lix_file SET hidden = true WHERE path = $1 OR id = 'missing'",
|
|
1607
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1608
|
-
)
|
|
1609
|
-
.await
|
|
1610
|
-
.expect("update predicate should canonicalize through OR");
|
|
1611
|
-
assert_eq!(update_result.rows_affected(), 1);
|
|
1612
|
-
|
|
1613
|
-
let delete_result = session
|
|
1614
|
-
.execute(
|
|
1615
|
-
"DELETE FROM lix_file WHERE path = $1",
|
|
1616
|
-
&[Value::Text("/%43afe%CC%81.txt".to_string())],
|
|
1617
|
-
)
|
|
1618
|
-
.await
|
|
1619
|
-
.expect("delete predicate should canonicalize");
|
|
1620
|
-
assert_eq!(delete_result.rows_affected(), 1);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
#[tokio::test]
|
|
1624
|
-
async fn lix_file_path_predicates_reject_non_literal_path_values() {
|
|
1625
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
1626
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone()))
|
|
1627
|
-
.await
|
|
1628
|
-
.expect("engine should initialize");
|
|
1629
|
-
let engine = Engine::new(Box::new(backend))
|
|
1630
|
-
.await
|
|
1631
|
-
.expect("engine should open");
|
|
1632
|
-
let session = engine
|
|
1633
|
-
.open_session(init_receipt.main_version_id)
|
|
1634
|
-
.await
|
|
1635
|
-
.expect("session should open");
|
|
1636
|
-
|
|
1637
|
-
session
|
|
1638
|
-
.execute(
|
|
1639
|
-
"INSERT INTO lix_file (id, path, data) VALUES ('file-nfc', $1, X'41')",
|
|
1640
|
-
&[Value::Text("/Cafe\u{301}.txt".to_string())],
|
|
1641
|
-
)
|
|
1642
|
-
.await
|
|
1643
|
-
.expect("NFD path insert should canonicalize");
|
|
1644
|
-
|
|
1645
|
-
let error = session
|
|
1646
|
-
.execute("SELECT id FROM lix_file WHERE path = id", &[])
|
|
1647
|
-
.await
|
|
1648
|
-
.expect_err("computed path predicate values should be rejected");
|
|
1649
|
-
assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
|
|
1650
|
-
assert!(
|
|
1651
|
-
error
|
|
1652
|
-
.message
|
|
1653
|
-
.contains("filesystem path predicates only support literal path values"),
|
|
1654
|
-
"{error:?}"
|
|
1655
|
-
);
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
#[tokio::test]
|
|
1659
|
-
async fn lix_directory_path_predicates_canonicalize_bound_values_like_writes() {
|
|
1660
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
1661
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone()))
|
|
1662
|
-
.await
|
|
1663
|
-
.expect("engine should initialize");
|
|
1664
|
-
let engine = Engine::new(Box::new(backend))
|
|
1665
|
-
.await
|
|
1666
|
-
.expect("engine should open");
|
|
1667
|
-
let session = engine
|
|
1668
|
-
.open_session(init_receipt.main_version_id)
|
|
1669
|
-
.await
|
|
1670
|
-
.expect("session should open");
|
|
1671
|
-
|
|
1672
|
-
session
|
|
1673
|
-
.execute(
|
|
1674
|
-
"INSERT INTO lix_directory (id, path) VALUES ('dir-nfc', $1)",
|
|
1675
|
-
&[Value::Text("/Cafe\u{301}/".to_string())],
|
|
1676
|
-
)
|
|
1677
|
-
.await
|
|
1678
|
-
.expect("NFD directory path insert should canonicalize");
|
|
1679
|
-
|
|
1680
|
-
let result = session
|
|
1681
|
-
.execute(
|
|
1682
|
-
"SELECT id FROM lix_directory WHERE path IN ($1)",
|
|
1683
|
-
&[Value::Text("/%43afe%CC%81/".to_string())],
|
|
1684
|
-
)
|
|
1685
|
-
.await
|
|
1686
|
-
.expect("directory path predicate should canonicalize");
|
|
1687
|
-
assert_eq!(
|
|
1688
|
-
rows_from_execute_result(result).1,
|
|
1689
|
-
vec![vec![Value::Text("dir-nfc".to_string())]]
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
let or_result = session
|
|
1693
|
-
.execute(
|
|
1694
|
-
"SELECT id FROM lix_directory WHERE id = 'missing' OR path = $1",
|
|
1695
|
-
&[Value::Text("/Cafe\u{301}/".to_string())],
|
|
1696
|
-
)
|
|
1697
|
-
.await
|
|
1698
|
-
.expect("directory OR path predicate should canonicalize");
|
|
1699
|
-
assert_eq!(
|
|
1700
|
-
rows_from_execute_result(or_result).1,
|
|
1701
|
-
vec![vec![Value::Text("dir-nfc".to_string())]]
|
|
1702
|
-
);
|
|
1703
|
-
|
|
1704
|
-
let not_in_result = session
|
|
1705
|
-
.execute(
|
|
1706
|
-
"SELECT id FROM lix_directory WHERE path NOT IN ($1)",
|
|
1707
|
-
&[Value::Text("/%43afe%CC%81/".to_string())],
|
|
1708
|
-
)
|
|
1709
|
-
.await
|
|
1710
|
-
.expect("directory NOT IN path predicate should canonicalize");
|
|
1711
|
-
assert!(rows_from_execute_result(not_in_result).1.is_empty());
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
#[tokio::test]
|
|
1715
|
-
async fn lix_directory_path_predicates_reject_non_literal_path_values() {
|
|
1716
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
1717
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone()))
|
|
1718
|
-
.await
|
|
1719
|
-
.expect("engine should initialize");
|
|
1720
|
-
let engine = Engine::new(Box::new(backend))
|
|
1721
|
-
.await
|
|
1722
|
-
.expect("engine should open");
|
|
1723
|
-
let session = engine
|
|
1724
|
-
.open_session(init_receipt.main_version_id)
|
|
1725
|
-
.await
|
|
1726
|
-
.expect("session should open");
|
|
1727
|
-
|
|
1728
|
-
session
|
|
1729
|
-
.execute(
|
|
1730
|
-
"INSERT INTO lix_directory (id, path) VALUES ('dir-nfc', $1)",
|
|
1731
|
-
&[Value::Text("/Cafe\u{301}/".to_string())],
|
|
1732
|
-
)
|
|
1733
|
-
.await
|
|
1734
|
-
.expect("NFD directory path insert should canonicalize");
|
|
1735
|
-
|
|
1736
|
-
let error = session
|
|
1737
|
-
.execute("SELECT id FROM lix_directory WHERE path IN (id)", &[])
|
|
1738
|
-
.await
|
|
1739
|
-
.expect_err("computed directory path predicate values should be rejected");
|
|
1740
|
-
assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
|
|
1741
|
-
assert!(
|
|
1742
|
-
error
|
|
1743
|
-
.message
|
|
1744
|
-
.contains("filesystem path predicates only support literal path values"),
|
|
1745
|
-
"{error:?}"
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
fn rows_from_execute_result(result: ExecuteResult) -> (Vec<String>, Vec<Vec<Value>>) {
|
|
1750
|
-
let rows = result;
|
|
1751
|
-
(
|
|
1752
|
-
rows.columns().to_vec(),
|
|
1753
|
-
rows.rows()
|
|
1754
|
-
.iter()
|
|
1755
|
-
.map(|row| row.values().to_vec())
|
|
1756
|
-
.collect(),
|
|
1757
|
-
)
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
#[tokio::test]
|
|
1761
|
-
async fn execute_sql_reads_lix_state_history_from_history_context() {
|
|
1762
|
-
let (session, head_commit_id) = setup_engine_history_fixture()
|
|
1763
|
-
.await
|
|
1764
|
-
.expect("history fixture should initialize");
|
|
1765
|
-
let result = session
|
|
1766
|
-
.execute(
|
|
1767
|
-
&format!(
|
|
1768
|
-
"SELECT entity_id, snapshot_content, metadata, depth, start_commit_id \
|
|
1769
|
-
FROM lix_state_history \
|
|
1770
|
-
WHERE schema_key = 'test_state_schema' \
|
|
1771
|
-
AND entity_id = lix_json('[\"entity-history\"]') \
|
|
1772
|
-
AND start_commit_id = '{head_commit_id}' \
|
|
1773
|
-
AND depth >= 0"
|
|
1774
|
-
),
|
|
1775
|
-
&[],
|
|
1776
|
-
)
|
|
1777
|
-
.await
|
|
1778
|
-
.expect("sql2 execute should read lix_state_history through real engine context");
|
|
1779
|
-
let (columns, rows) = rows_from_execute_result(result);
|
|
1780
|
-
|
|
1781
|
-
assert_eq!(
|
|
1782
|
-
columns,
|
|
1783
|
-
vec![
|
|
1784
|
-
"entity_id",
|
|
1785
|
-
"snapshot_content",
|
|
1786
|
-
"metadata",
|
|
1787
|
-
"depth",
|
|
1788
|
-
"start_commit_id"
|
|
1789
|
-
]
|
|
1790
|
-
);
|
|
1791
|
-
assert_eq!(rows.len(), 1);
|
|
1792
|
-
assert_eq!(rows[0][0], Value::Json(json!(["entity-history"])));
|
|
1793
|
-
assert_eq!(rows[0][1], Value::Json(json!({"count": 7, "value": "A"})));
|
|
1794
|
-
assert_eq!(rows[0][2], Value::Json(json!({"source": "history"})));
|
|
1795
|
-
assert!(matches!(rows[0][3], Value::Integer(_)));
|
|
1796
|
-
assert_eq!(rows[0][4], Value::Text(head_commit_id.clone()));
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
#[tokio::test]
|
|
1800
|
-
async fn execute_sql_reads_entity_history_view_from_history_context() {
|
|
1801
|
-
let (session, head_commit_id) = setup_engine_history_fixture()
|
|
1802
|
-
.await
|
|
1803
|
-
.expect("history fixture should initialize");
|
|
1804
|
-
let result = session
|
|
1805
|
-
.execute(
|
|
1806
|
-
&format!(
|
|
1807
|
-
"SELECT value, count, lixcol_entity_id, lixcol_start_commit_id, lixcol_depth \
|
|
1808
|
-
FROM test_state_schema_history \
|
|
1809
|
-
WHERE lixcol_start_commit_id = '{head_commit_id}' \
|
|
1810
|
-
AND lixcol_entity_id = lix_json('[\"entity-history\"]')"
|
|
1811
|
-
),
|
|
1812
|
-
&[],
|
|
1813
|
-
)
|
|
1814
|
-
.await
|
|
1815
|
-
.expect("sql2 execute should read entity history through real engine context");
|
|
1816
|
-
let (columns, rows) = rows_from_execute_result(result);
|
|
1817
|
-
|
|
1818
|
-
assert_eq!(
|
|
1819
|
-
columns,
|
|
1820
|
-
vec![
|
|
1821
|
-
"value",
|
|
1822
|
-
"count",
|
|
1823
|
-
"lixcol_entity_id",
|
|
1824
|
-
"lixcol_start_commit_id",
|
|
1825
|
-
"lixcol_depth",
|
|
1826
|
-
]
|
|
1827
|
-
);
|
|
1828
|
-
assert_eq!(rows.len(), 1);
|
|
1829
|
-
assert_eq!(rows[0][0], Value::Text("A".to_string()));
|
|
1830
|
-
assert_eq!(rows[0][1], Value::Integer(7));
|
|
1831
|
-
assert_eq!(rows[0][2], Value::Json(json!(["entity-history"])));
|
|
1832
|
-
assert_eq!(rows[0][3], Value::Text(head_commit_id));
|
|
1833
|
-
assert!(matches!(rows[0][4], Value::Integer(_)));
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
#[tokio::test]
|
|
1837
|
-
async fn execute_sql_reads_directory_history_view_from_history_context() {
|
|
1838
|
-
let (session, head_commit_id) = setup_engine_history_fixture()
|
|
1839
|
-
.await
|
|
1840
|
-
.expect("history fixture should initialize");
|
|
1841
|
-
let result = session
|
|
1842
|
-
.execute(
|
|
1843
|
-
&format!(
|
|
1844
|
-
"SELECT id, parent_id, name, path, hidden, lixcol_start_commit_id, lixcol_depth \
|
|
1845
|
-
FROM lix_directory_history \
|
|
1846
|
-
WHERE id = 'dir-docs' AND lixcol_start_commit_id = '{head_commit_id}'"
|
|
1847
|
-
),
|
|
1848
|
-
&[],
|
|
1849
|
-
)
|
|
1850
|
-
.await
|
|
1851
|
-
.expect("sql2 execute should read directory history through real engine context");
|
|
1852
|
-
assert!(
|
|
1853
|
-
result.notices().is_empty(),
|
|
1854
|
-
"identity-filtered directory history should not emit soft notices"
|
|
1855
|
-
);
|
|
1856
|
-
let (columns, rows) = rows_from_execute_result(result);
|
|
1857
|
-
|
|
1858
|
-
assert_eq!(
|
|
1859
|
-
columns,
|
|
1860
|
-
vec![
|
|
1861
|
-
"id",
|
|
1862
|
-
"parent_id",
|
|
1863
|
-
"name",
|
|
1864
|
-
"path",
|
|
1865
|
-
"hidden",
|
|
1866
|
-
"lixcol_start_commit_id",
|
|
1867
|
-
"lixcol_depth",
|
|
1868
|
-
]
|
|
1869
|
-
);
|
|
1870
|
-
assert_eq!(rows.len(), 1);
|
|
1871
|
-
assert_eq!(rows[0][0], Value::Text("dir-docs".to_string()));
|
|
1872
|
-
assert_eq!(rows[0][1], Value::Null);
|
|
1873
|
-
assert_eq!(rows[0][2], Value::Text("docs".to_string()));
|
|
1874
|
-
assert_eq!(rows[0][3], Value::Text("/docs/".to_string()));
|
|
1875
|
-
assert_eq!(rows[0][4], Value::Boolean(false));
|
|
1876
|
-
assert_eq!(rows[0][5], Value::Text(head_commit_id.clone()));
|
|
1877
|
-
assert!(matches!(rows[0][6], Value::Integer(_)));
|
|
1878
|
-
|
|
1879
|
-
let name_filtered_result = session
|
|
1880
|
-
.execute(
|
|
1881
|
-
&format!(
|
|
1882
|
-
"SELECT id \
|
|
1883
|
-
FROM lix_directory_history \
|
|
1884
|
-
WHERE name = 'docs' \
|
|
1885
|
-
AND lixcol_start_commit_id = '{head_commit_id}'"
|
|
1886
|
-
),
|
|
1887
|
-
&[],
|
|
1888
|
-
)
|
|
1889
|
-
.await
|
|
1890
|
-
.expect("sql2 execute should attach notices to name-filtered directory history reads");
|
|
1891
|
-
assert_eq!(name_filtered_result.notices().len(), 1);
|
|
1892
|
-
assert_eq!(
|
|
1893
|
-
name_filtered_result.notices()[0].code,
|
|
1894
|
-
"LIX_HISTORY_NON_IDENTITY_FILTER"
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
#[tokio::test]
|
|
1899
|
-
async fn execute_sql_reads_file_history_view_from_history_context() {
|
|
1900
|
-
let (session, head_commit_id) = setup_engine_history_fixture()
|
|
1901
|
-
.await
|
|
1902
|
-
.expect("history fixture should initialize");
|
|
1903
|
-
let result = session
|
|
1904
|
-
.execute(
|
|
1905
|
-
&format!(
|
|
1906
|
-
"SELECT id, path, data, hidden, lixcol_start_commit_id, lixcol_depth \
|
|
1907
|
-
FROM lix_file_history \
|
|
1908
|
-
WHERE id = 'file-a' \
|
|
1909
|
-
AND lixcol_start_commit_id = '{head_commit_id}' \
|
|
1910
|
-
AND data IS NOT NULL \
|
|
1911
|
-
ORDER BY lixcol_depth",
|
|
1912
|
-
),
|
|
1913
|
-
&[],
|
|
1914
|
-
)
|
|
1915
|
-
.await
|
|
1916
|
-
.expect("sql2 execute should read file history through real engine context");
|
|
1917
|
-
assert!(
|
|
1918
|
-
result.notices().is_empty(),
|
|
1919
|
-
"identity-filtered file history should not emit soft notices"
|
|
1920
|
-
);
|
|
1921
|
-
let (columns, rows) = rows_from_execute_result(result);
|
|
1922
|
-
|
|
1923
|
-
assert_eq!(
|
|
1924
|
-
columns,
|
|
1925
|
-
vec![
|
|
1926
|
-
"id",
|
|
1927
|
-
"path",
|
|
1928
|
-
"data",
|
|
1929
|
-
"hidden",
|
|
1930
|
-
"lixcol_start_commit_id",
|
|
1931
|
-
"lixcol_depth",
|
|
1932
|
-
]
|
|
1933
|
-
);
|
|
1934
|
-
assert_eq!(rows.len(), 1);
|
|
1935
|
-
assert_eq!(rows[0][0], Value::Text("file-a".to_string()));
|
|
1936
|
-
assert_eq!(rows[0][1], Value::Text("/docs/readme.md".to_string()));
|
|
1937
|
-
assert_eq!(rows[0][2], Value::Blob(b"hello".to_vec()));
|
|
1938
|
-
assert_eq!(rows[0][3], Value::Boolean(false));
|
|
1939
|
-
assert_eq!(rows[0][4], Value::Text(head_commit_id.clone()));
|
|
1940
|
-
assert!(matches!(rows[0][5], Value::Integer(_)));
|
|
1941
|
-
|
|
1942
|
-
let path_filtered_result = session
|
|
1943
|
-
.execute(
|
|
1944
|
-
&format!(
|
|
1945
|
-
"SELECT id \
|
|
1946
|
-
FROM lix_file_history \
|
|
1947
|
-
WHERE path = '/docs/readme.md' \
|
|
1948
|
-
AND lixcol_start_commit_id = '{head_commit_id}'"
|
|
1949
|
-
),
|
|
1950
|
-
&[],
|
|
1951
|
-
)
|
|
1952
|
-
.await
|
|
1953
|
-
.expect("sql2 execute should attach notices to path-filtered file history reads");
|
|
1954
|
-
assert_eq!(path_filtered_result.notices().len(), 1);
|
|
1955
|
-
assert_eq!(
|
|
1956
|
-
path_filtered_result.notices()[0].code,
|
|
1957
|
-
"LIX_HISTORY_NON_IDENTITY_FILTER"
|
|
1958
|
-
);
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
#[tokio::test]
|
|
1962
|
-
async fn execute_sql_rejects_writes_to_history_views_before_planning() {
|
|
1963
|
-
for sql in [
|
|
1964
|
-
"DELETE FROM lix_state_history",
|
|
1965
|
-
"DELETE FROM LIX_STATE_HISTORY",
|
|
1966
|
-
"DELETE FROM main.LIX_STATE_HISTORY",
|
|
1967
|
-
] {
|
|
1968
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1969
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1970
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
1971
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
1972
|
-
active_version_id: "version-a",
|
|
1973
|
-
blob_reader,
|
|
1974
|
-
live_state,
|
|
1975
|
-
staged_writes,
|
|
1976
|
-
schema_definitions: vec![],
|
|
1977
|
-
};
|
|
1978
|
-
|
|
1979
|
-
let error = execute_write_sql(&mut ctx, sql, &[])
|
|
1980
|
-
.await
|
|
1981
|
-
.expect_err("history views are read-only");
|
|
1982
|
-
|
|
1983
|
-
assert_eq!(error.code, LixError::CODE_READ_ONLY, "{sql}");
|
|
1984
|
-
assert_eq!(
|
|
1985
|
-
error.message, "DML cannot write read-only history view 'lix_state_history'",
|
|
1986
|
-
"{sql}"
|
|
1987
|
-
);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
#[tokio::test]
|
|
1992
|
-
async fn execute_sql_insert_into_lix_state_values_stages_write() {
|
|
1993
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
1994
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
1995
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
1996
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
1997
|
-
active_version_id: "version-a",
|
|
1998
|
-
blob_reader,
|
|
1999
|
-
live_state,
|
|
2000
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2001
|
-
schema_definitions: vec![],
|
|
2002
|
-
};
|
|
2003
|
-
|
|
2004
|
-
let result = execute_write_sql(
|
|
2005
|
-
&mut ctx,
|
|
2006
|
-
"INSERT INTO lix_state (\
|
|
2007
|
-
entity_id, schema_key, file_id, snapshot_content, metadata, global, untracked\
|
|
2008
|
-
) VALUES (\
|
|
2009
|
-
lix_json('[\"entity-1\"]'), 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"world\"}', '{\"source\":\"sql\"}', false, false\
|
|
2010
|
-
)",
|
|
2011
|
-
&[],
|
|
2012
|
-
)
|
|
2013
|
-
.await
|
|
2014
|
-
.expect("INSERT INTO lix_state VALUES should stage write");
|
|
2015
|
-
|
|
2016
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2017
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2018
|
-
|
|
2019
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2020
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2021
|
-
let overlay = staged_writes.deltas[0]
|
|
2022
|
-
.pending_write_overlay()
|
|
2023
|
-
.expect("staged delta should expose pending overlay");
|
|
2024
|
-
let rows = overlay.visible_semantic_rows(false, "lix_key_value");
|
|
2025
|
-
assert_eq!(rows.len(), 1);
|
|
2026
|
-
assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
|
|
2027
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2028
|
-
assert!(!rows[0].global);
|
|
2029
|
-
assert!(!rows[0].untracked);
|
|
2030
|
-
assert_eq!(
|
|
2031
|
-
rows[0].snapshot_content.as_deref(),
|
|
2032
|
-
Some("{\"key\":\"hello\",\"value\":\"world\"}")
|
|
2033
|
-
);
|
|
2034
|
-
assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"sql\"}"));
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
#[tokio::test]
|
|
2038
|
-
async fn execute_sql_insert_into_lix_state_defaults_global_and_untracked_to_false() {
|
|
2039
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2040
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2041
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2042
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2043
|
-
active_version_id: "version-a",
|
|
2044
|
-
blob_reader,
|
|
2045
|
-
live_state,
|
|
2046
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2047
|
-
schema_definitions: vec![],
|
|
2048
|
-
};
|
|
2049
|
-
|
|
2050
|
-
let result = execute_write_sql(
|
|
2051
|
-
&mut ctx,
|
|
2052
|
-
"INSERT INTO lix_state (\
|
|
2053
|
-
entity_id, schema_key, file_id, snapshot_content, metadata\
|
|
2054
|
-
) VALUES (\
|
|
2055
|
-
lix_json('[\"entity-defaults\"]'), 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"defaults\"}', NULL\
|
|
2056
|
-
)",
|
|
2057
|
-
&[],
|
|
2058
|
-
)
|
|
2059
|
-
.await
|
|
2060
|
-
.expect("INSERT INTO lix_state should default bookkeeping flags");
|
|
2061
|
-
|
|
2062
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2063
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2064
|
-
|
|
2065
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2066
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2067
|
-
let overlay = staged_writes.deltas[0]
|
|
2068
|
-
.pending_write_overlay()
|
|
2069
|
-
.expect("staged delta should expose pending overlay");
|
|
2070
|
-
let rows = overlay.visible_semantic_rows(false, "lix_key_value");
|
|
2071
|
-
assert_eq!(rows.len(), 1);
|
|
2072
|
-
assert_eq!(rows[0].entity_id, "[\"entity-defaults\"]");
|
|
2073
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2074
|
-
assert!(!rows[0].global);
|
|
2075
|
-
assert!(!rows[0].untracked);
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
#[tokio::test]
|
|
2079
|
-
async fn execute_sql_insert_into_lix_state_select_stages_write() {
|
|
2080
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2081
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2082
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2083
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2084
|
-
active_version_id: "version-a",
|
|
2085
|
-
blob_reader,
|
|
2086
|
-
live_state,
|
|
2087
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2088
|
-
schema_definitions: vec![],
|
|
2089
|
-
};
|
|
2090
|
-
|
|
2091
|
-
let result = execute_write_sql(
|
|
2092
|
-
&mut ctx,
|
|
2093
|
-
"INSERT INTO lix_state (\
|
|
2094
|
-
entity_id, schema_key, file_id, snapshot_content, metadata, global, untracked\
|
|
2095
|
-
) \
|
|
2096
|
-
SELECT \
|
|
2097
|
-
lix_json('[\"entity-from-select\"]') AS entity_id, \
|
|
2098
|
-
'lix_key_value' AS schema_key, \
|
|
2099
|
-
NULL AS file_id, \
|
|
2100
|
-
'{\"key\":\"hello\",\"value\":\"from-select\"}' AS snapshot_content, \
|
|
2101
|
-
'{\"source\":\"select\"}' AS metadata, \
|
|
2102
|
-
false AS global, \
|
|
2103
|
-
false AS untracked",
|
|
2104
|
-
&[],
|
|
2105
|
-
)
|
|
2106
|
-
.await
|
|
2107
|
-
.expect("INSERT INTO lix_state SELECT should stage write");
|
|
2108
|
-
|
|
2109
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2110
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2111
|
-
|
|
2112
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2113
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2114
|
-
let overlay = staged_writes.deltas[0]
|
|
2115
|
-
.pending_write_overlay()
|
|
2116
|
-
.expect("staged delta should expose pending overlay");
|
|
2117
|
-
let rows = overlay.visible_semantic_rows(false, "lix_key_value");
|
|
2118
|
-
assert_eq!(rows.len(), 1);
|
|
2119
|
-
assert_eq!(rows[0].entity_id, "[\"entity-from-select\"]");
|
|
2120
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2121
|
-
assert_eq!(
|
|
2122
|
-
rows[0].snapshot_content.as_deref(),
|
|
2123
|
-
Some("{\"key\":\"hello\",\"value\":\"from-select\"}")
|
|
2124
|
-
);
|
|
2125
|
-
assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"select\"}"));
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
#[tokio::test]
|
|
2129
|
-
async fn execute_sql_insert_into_entity_by_version_stages_write() {
|
|
2130
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2131
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2132
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2133
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2134
|
-
active_version_id: "version-a",
|
|
2135
|
-
blob_reader,
|
|
2136
|
-
live_state,
|
|
2137
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2138
|
-
schema_definitions: vec![json!({
|
|
2139
|
-
"x-lix-key": "test_state_schema",
|
|
2140
|
-
"type": "object",
|
|
2141
|
-
"properties": {
|
|
2142
|
-
"value": { "type": "string" }
|
|
2143
|
-
}
|
|
2144
|
-
})],
|
|
2145
|
-
};
|
|
2146
|
-
|
|
2147
|
-
let result = execute_write_sql(
|
|
2148
|
-
&mut ctx,
|
|
2149
|
-
"INSERT INTO test_state_schema_by_version (\
|
|
2150
|
-
lixcol_entity_id, lixcol_version_id, value\
|
|
2151
|
-
) VALUES (lix_json('[\"entity-c\"]'), 'version-b', 'C')",
|
|
2152
|
-
&[],
|
|
2153
|
-
)
|
|
2154
|
-
.await
|
|
2155
|
-
.expect("INSERT INTO entity by-version surface should stage write");
|
|
2156
|
-
|
|
2157
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2158
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2159
|
-
|
|
2160
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2161
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2162
|
-
let overlay = staged_writes.deltas[0]
|
|
2163
|
-
.pending_write_overlay()
|
|
2164
|
-
.expect("staged delta should expose pending overlay");
|
|
2165
|
-
let rows = overlay.visible_semantic_rows(false, "test_state_schema");
|
|
2166
|
-
assert_eq!(rows.len(), 1);
|
|
2167
|
-
assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
|
|
2168
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2169
|
-
assert!(!rows[0].global);
|
|
2170
|
-
assert!(!rows[0].untracked);
|
|
2171
|
-
assert_eq!(
|
|
2172
|
-
rows[0].snapshot_content.as_deref(),
|
|
2173
|
-
Some("{\"value\":\"C\"}")
|
|
2174
|
-
);
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
#[tokio::test]
|
|
2178
|
-
async fn execute_sql_insert_into_active_entity_defaults_active_version() {
|
|
2179
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2180
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2181
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2182
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2183
|
-
active_version_id: "version-a",
|
|
2184
|
-
blob_reader,
|
|
2185
|
-
live_state,
|
|
2186
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2187
|
-
schema_definitions: vec![json!({
|
|
2188
|
-
"x-lix-key": "test_state_schema",
|
|
2189
|
-
"type": "object",
|
|
2190
|
-
"properties": {
|
|
2191
|
-
"value": { "type": "string" }
|
|
2192
|
-
}
|
|
2193
|
-
})],
|
|
2194
|
-
};
|
|
2195
|
-
|
|
2196
|
-
let result = execute_write_sql(
|
|
2197
|
-
&mut ctx,
|
|
2198
|
-
"INSERT INTO test_state_schema (lixcol_entity_id, value) \
|
|
2199
|
-
VALUES (lix_json('[\"entity-c\"]'), 'C')",
|
|
2200
|
-
&[],
|
|
2201
|
-
)
|
|
2202
|
-
.await
|
|
2203
|
-
.expect("INSERT INTO active entity surface should stage write");
|
|
2204
|
-
|
|
2205
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2206
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2207
|
-
|
|
2208
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2209
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2210
|
-
let overlay = staged_writes.deltas[0]
|
|
2211
|
-
.pending_write_overlay()
|
|
2212
|
-
.expect("staged delta should expose pending overlay");
|
|
2213
|
-
let rows = overlay.visible_semantic_rows(false, "test_state_schema");
|
|
2214
|
-
assert_eq!(rows.len(), 1);
|
|
2215
|
-
assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
|
|
2216
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2217
|
-
assert!(!rows[0].global);
|
|
2218
|
-
assert!(!rows[0].untracked);
|
|
2219
|
-
assert_eq!(
|
|
2220
|
-
rows[0].snapshot_content.as_deref(),
|
|
2221
|
-
Some("{\"value\":\"C\"}")
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
#[tokio::test]
|
|
2226
|
-
async fn execute_sql_insert_into_directory_by_version_stages_write() {
|
|
2227
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2228
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2229
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2230
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2231
|
-
active_version_id: "version-a",
|
|
2232
|
-
blob_reader,
|
|
2233
|
-
live_state,
|
|
2234
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2235
|
-
schema_definitions: vec![],
|
|
2236
|
-
};
|
|
2237
|
-
|
|
2238
|
-
let result = execute_write_sql(
|
|
2239
|
-
&mut ctx,
|
|
2240
|
-
"INSERT INTO lix_directory_by_version (\
|
|
2241
|
-
id, parent_id, name, hidden, lixcol_version_id\
|
|
2242
|
-
) VALUES ('dir-docs', NULL, 'docs', false, 'version-b')",
|
|
2243
|
-
&[],
|
|
2244
|
-
)
|
|
2245
|
-
.await
|
|
2246
|
-
.expect("INSERT INTO lix_directory_by_version should stage write");
|
|
2247
|
-
|
|
2248
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2249
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2250
|
-
|
|
2251
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2252
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2253
|
-
let overlay = staged_writes.deltas[0]
|
|
2254
|
-
.pending_write_overlay()
|
|
2255
|
-
.expect("staged delta should expose pending overlay");
|
|
2256
|
-
let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
|
|
2257
|
-
assert_eq!(rows.len(), 1);
|
|
2258
|
-
assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
|
|
2259
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2260
|
-
assert!(!rows[0].global);
|
|
2261
|
-
assert!(!rows[0].untracked);
|
|
2262
|
-
assert_eq!(
|
|
2263
|
-
rows[0].snapshot_content.as_deref(),
|
|
2264
|
-
Some("{\"hidden\":false,\"id\":\"dir-docs\",\"name\":\"docs\",\"parent_id\":null}")
|
|
2265
|
-
);
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
#[tokio::test]
|
|
2269
|
-
async fn execute_sql_insert_into_active_directory_defaults_active_version() {
|
|
2270
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2271
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2272
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2273
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2274
|
-
active_version_id: "version-a",
|
|
2275
|
-
blob_reader,
|
|
2276
|
-
live_state,
|
|
2277
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2278
|
-
schema_definitions: vec![],
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
let result = execute_write_sql(
|
|
2282
|
-
&mut ctx,
|
|
2283
|
-
"INSERT INTO lix_directory (id, parent_id, name, hidden) \
|
|
2284
|
-
VALUES ('dir-docs', NULL, 'docs', false)",
|
|
2285
|
-
&[],
|
|
2286
|
-
)
|
|
2287
|
-
.await
|
|
2288
|
-
.expect("INSERT INTO lix_directory should stage write");
|
|
2289
|
-
|
|
2290
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2291
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2292
|
-
|
|
2293
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2294
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2295
|
-
let overlay = staged_writes.deltas[0]
|
|
2296
|
-
.pending_write_overlay()
|
|
2297
|
-
.expect("staged delta should expose pending overlay");
|
|
2298
|
-
let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
|
|
2299
|
-
assert_eq!(rows.len(), 1);
|
|
2300
|
-
assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
|
|
2301
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2302
|
-
assert!(!rows[0].global);
|
|
2303
|
-
assert!(!rows[0].untracked);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
#[tokio::test]
|
|
2307
|
-
async fn execute_sql_update_directory_stages_rewritten_descriptor() {
|
|
2308
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2309
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2310
|
-
rows: vec![
|
|
2311
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2312
|
-
live_directory_row("dir-guides", "version-a", Some("dir-docs"), "guides", false),
|
|
2313
|
-
],
|
|
2314
|
-
});
|
|
2315
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2316
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2317
|
-
active_version_id: "version-a",
|
|
2318
|
-
blob_reader,
|
|
2319
|
-
live_state,
|
|
2320
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2321
|
-
schema_definitions: vec![],
|
|
2322
|
-
};
|
|
2323
|
-
|
|
2324
|
-
let result = execute_write_sql(
|
|
2325
|
-
&mut ctx,
|
|
2326
|
-
"UPDATE lix_directory \
|
|
2327
|
-
SET hidden = true, lixcol_metadata = '{\"source\":\"directory-update\"}' \
|
|
2328
|
-
WHERE id = 'dir-docs'",
|
|
2329
|
-
&[],
|
|
2330
|
-
)
|
|
2331
|
-
.await
|
|
2332
|
-
.expect("UPDATE lix_directory should stage rewritten descriptor");
|
|
2333
|
-
|
|
2334
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2335
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2336
|
-
|
|
2337
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2338
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2339
|
-
let overlay = staged_writes.deltas[0]
|
|
2340
|
-
.pending_write_overlay()
|
|
2341
|
-
.expect("staged delta should expose pending overlay");
|
|
2342
|
-
let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
|
|
2343
|
-
assert_eq!(rows.len(), 1);
|
|
2344
|
-
assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
|
|
2345
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2346
|
-
assert_eq!(
|
|
2347
|
-
rows[0].snapshot_content.as_deref(),
|
|
2348
|
-
Some("{\"hidden\":true,\"id\":\"dir-docs\",\"name\":\"docs\",\"parent_id\":null}")
|
|
2349
|
-
);
|
|
2350
|
-
assert_eq!(
|
|
2351
|
-
rows[0].metadata.as_deref(),
|
|
2352
|
-
Some("{\"source\":\"directory-update\"}")
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
#[tokio::test]
|
|
2357
|
-
async fn execute_sql_update_directory_rejects_path_assignment() {
|
|
2358
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2359
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2360
|
-
rows: vec![live_directory_row(
|
|
2361
|
-
"dir-docs",
|
|
2362
|
-
"version-a",
|
|
2363
|
-
None,
|
|
2364
|
-
"docs",
|
|
2365
|
-
false,
|
|
2366
|
-
)],
|
|
2367
|
-
});
|
|
2368
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2369
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2370
|
-
active_version_id: "version-a",
|
|
2371
|
-
blob_reader,
|
|
2372
|
-
live_state,
|
|
2373
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2374
|
-
schema_definitions: vec![],
|
|
2375
|
-
};
|
|
2376
|
-
|
|
2377
|
-
let error = execute_write_sql(
|
|
2378
|
-
&mut ctx,
|
|
2379
|
-
"UPDATE lix_directory SET path = '/renamed/' WHERE id = 'dir-docs'",
|
|
2380
|
-
&[],
|
|
2381
|
-
)
|
|
2382
|
-
.await
|
|
2383
|
-
.expect_err("path should remain read-only");
|
|
2384
|
-
|
|
2385
|
-
assert!(
|
|
2386
|
-
error.message.contains("read-only column 'path'"),
|
|
2387
|
-
"unexpected error: {error:?}"
|
|
2388
|
-
);
|
|
2389
|
-
assert!(staged_writes
|
|
2390
|
-
.lock()
|
|
2391
|
-
.expect("staged writes lock")
|
|
2392
|
-
.deltas
|
|
2393
|
-
.is_empty());
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
#[tokio::test]
|
|
2397
|
-
async fn execute_sql_delete_directory_by_version_stages_tombstone() {
|
|
2398
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2399
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2400
|
-
rows: vec![
|
|
2401
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2402
|
-
live_directory_row("dir-guides", "version-b", Some("dir-docs"), "guides", false),
|
|
2403
|
-
],
|
|
2404
|
-
});
|
|
2405
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2406
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2407
|
-
active_version_id: "version-a",
|
|
2408
|
-
blob_reader,
|
|
2409
|
-
live_state,
|
|
2410
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2411
|
-
schema_definitions: vec![],
|
|
2412
|
-
};
|
|
2413
|
-
|
|
2414
|
-
let result = execute_write_sql(
|
|
2415
|
-
&mut ctx,
|
|
2416
|
-
"DELETE FROM lix_directory_by_version \
|
|
2417
|
-
WHERE id = 'dir-guides' AND lixcol_version_id = 'version-b'",
|
|
2418
|
-
&[],
|
|
2419
|
-
)
|
|
2420
|
-
.await
|
|
2421
|
-
.expect("DELETE lix_directory_by_version should stage tombstone");
|
|
2422
|
-
|
|
2423
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2424
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2425
|
-
|
|
2426
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2427
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2428
|
-
let overlay = staged_writes.deltas[0]
|
|
2429
|
-
.pending_write_overlay()
|
|
2430
|
-
.expect("staged delta should expose pending overlay");
|
|
2431
|
-
let rows = overlay.visible_all_semantic_rows();
|
|
2432
|
-
assert_eq!(rows.len(), 1);
|
|
2433
|
-
assert_eq!(rows[0].entity_id, "[\"dir-guides\"]");
|
|
2434
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2435
|
-
assert!(rows[0].tombstone);
|
|
2436
|
-
assert_eq!(rows[0].snapshot_content, None);
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
#[tokio::test]
|
|
2440
|
-
async fn execute_sql_insert_into_file_by_version_stages_descriptor_write() {
|
|
2441
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2442
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2443
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2444
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2445
|
-
active_version_id: "version-a",
|
|
2446
|
-
blob_reader,
|
|
2447
|
-
live_state,
|
|
2448
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2449
|
-
schema_definitions: vec![],
|
|
2450
|
-
};
|
|
2451
|
-
|
|
2452
|
-
let result = execute_write_sql(
|
|
2453
|
-
&mut ctx,
|
|
2454
|
-
"INSERT INTO lix_file_by_version (\
|
|
2455
|
-
id, directory_id, name, hidden, lixcol_version_id\
|
|
2456
|
-
) VALUES ('file-readme', 'dir-docs', 'readme.md', false, 'version-b')",
|
|
2457
|
-
&[],
|
|
2458
|
-
)
|
|
2459
|
-
.await
|
|
2460
|
-
.expect("INSERT INTO lix_file_by_version should stage descriptor write");
|
|
2461
|
-
|
|
2462
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2463
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2464
|
-
|
|
2465
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2466
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2467
|
-
let overlay = staged_writes.deltas[0]
|
|
2468
|
-
.pending_write_overlay()
|
|
2469
|
-
.expect("staged delta should expose pending overlay");
|
|
2470
|
-
let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
|
|
2471
|
-
assert_eq!(rows.len(), 1);
|
|
2472
|
-
assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
|
|
2473
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2474
|
-
assert!(!rows[0].global);
|
|
2475
|
-
assert!(!rows[0].untracked);
|
|
2476
|
-
let snapshot: JsonValue =
|
|
2477
|
-
serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
|
|
2478
|
-
.expect("descriptor snapshot JSON");
|
|
2479
|
-
assert_eq!(snapshot["id"], "file-readme");
|
|
2480
|
-
assert_eq!(snapshot["directory_id"], "dir-docs");
|
|
2481
|
-
assert_eq!(snapshot["name"], "readme.md");
|
|
2482
|
-
assert_eq!(snapshot["hidden"], false);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
#[tokio::test]
|
|
2486
|
-
async fn execute_sql_insert_into_active_file_defaults_active_version() {
|
|
2487
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2488
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2489
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2490
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2491
|
-
active_version_id: "version-a",
|
|
2492
|
-
blob_reader,
|
|
2493
|
-
live_state,
|
|
2494
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2495
|
-
schema_definitions: vec![],
|
|
2496
|
-
};
|
|
2497
|
-
|
|
2498
|
-
let result = execute_write_sql(
|
|
2499
|
-
&mut ctx,
|
|
2500
|
-
"INSERT INTO lix_file (id, directory_id, name, hidden) \
|
|
2501
|
-
VALUES ('file-readme', 'dir-docs', 'readme.md', false)",
|
|
2502
|
-
&[],
|
|
2503
|
-
)
|
|
2504
|
-
.await
|
|
2505
|
-
.expect("INSERT INTO lix_file should stage descriptor write");
|
|
2506
|
-
|
|
2507
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2508
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2509
|
-
|
|
2510
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2511
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2512
|
-
let overlay = staged_writes.deltas[0]
|
|
2513
|
-
.pending_write_overlay()
|
|
2514
|
-
.expect("staged delta should expose pending overlay");
|
|
2515
|
-
let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
|
|
2516
|
-
assert_eq!(rows.len(), 1);
|
|
2517
|
-
assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
|
|
2518
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2519
|
-
assert!(!rows[0].global);
|
|
2520
|
-
assert!(!rows[0].untracked);
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
#[tokio::test]
|
|
2524
|
-
async fn execute_sql_insert_into_file_with_data_stages_blob_ref() {
|
|
2525
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2526
|
-
let live_state = Arc::new(DummyLiveStateReader);
|
|
2527
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2528
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2529
|
-
active_version_id: "version-a",
|
|
2530
|
-
blob_reader,
|
|
2531
|
-
live_state,
|
|
2532
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2533
|
-
schema_definitions: vec![],
|
|
2534
|
-
};
|
|
2535
|
-
|
|
2536
|
-
let result = execute_write_sql(
|
|
2537
|
-
&mut ctx,
|
|
2538
|
-
"INSERT INTO lix_file_by_version (\
|
|
2539
|
-
id, directory_id, name, hidden, data, lixcol_version_id\
|
|
2540
|
-
) VALUES ('file-readme', 'dir-docs', 'readme.md', false, X'4142', 'version-b')",
|
|
2541
|
-
&[],
|
|
2542
|
-
)
|
|
2543
|
-
.await
|
|
2544
|
-
.expect("INSERT INTO lix_file_by_version should stage descriptor and data writes");
|
|
2545
|
-
|
|
2546
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2547
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2548
|
-
|
|
2549
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2550
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2551
|
-
let overlay = staged_writes.deltas[0]
|
|
2552
|
-
.pending_write_overlay()
|
|
2553
|
-
.expect("staged delta should expose pending overlay");
|
|
2554
|
-
let descriptor_rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
|
|
2555
|
-
assert_eq!(descriptor_rows.len(), 1);
|
|
2556
|
-
assert_eq!(descriptor_rows[0].entity_id, "[\"file-readme\"]");
|
|
2557
|
-
let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
|
|
2558
|
-
assert_eq!(blob_ref_rows.len(), 1);
|
|
2559
|
-
assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
|
|
2560
|
-
assert_eq!(blob_ref_rows[0].file_id.as_deref(), Some("file-readme"));
|
|
2561
|
-
assert_eq!(blob_ref_rows[0].version_id, "version-b");
|
|
2562
|
-
let snapshot: JsonValue =
|
|
2563
|
-
serde_json::from_str(blob_ref_rows[0].snapshot_content.as_deref().unwrap())
|
|
2564
|
-
.expect("blob ref snapshot JSON");
|
|
2565
|
-
assert_eq!(snapshot["id"], "file-readme");
|
|
2566
|
-
assert_eq!(snapshot["size_bytes"], 2);
|
|
2567
|
-
assert!(snapshot["blob_hash"]
|
|
2568
|
-
.as_str()
|
|
2569
|
-
.is_some_and(|value| !value.is_empty()));
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
#[tokio::test]
|
|
2573
|
-
async fn execute_sql_update_file_stages_rewritten_descriptor() {
|
|
2574
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2575
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2576
|
-
rows: vec![
|
|
2577
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2578
|
-
live_file_row(
|
|
2579
|
-
"file-readme",
|
|
2580
|
-
"version-a",
|
|
2581
|
-
Some("dir-docs"),
|
|
2582
|
-
"readme.md",
|
|
2583
|
-
false,
|
|
2584
|
-
),
|
|
2585
|
-
live_file_row(
|
|
2586
|
-
"file-guide",
|
|
2587
|
-
"version-a",
|
|
2588
|
-
Some("dir-docs"),
|
|
2589
|
-
"guide.md",
|
|
2590
|
-
false,
|
|
2591
|
-
),
|
|
2592
|
-
],
|
|
2593
|
-
});
|
|
2594
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2595
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2596
|
-
active_version_id: "version-a",
|
|
2597
|
-
blob_reader,
|
|
2598
|
-
live_state,
|
|
2599
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2600
|
-
schema_definitions: vec![],
|
|
2601
|
-
};
|
|
2602
|
-
|
|
2603
|
-
let result = execute_write_sql(
|
|
2604
|
-
&mut ctx,
|
|
2605
|
-
"UPDATE lix_file \
|
|
2606
|
-
SET name = 'readme-updated.txt', hidden = true, lixcol_metadata = '{\"source\":\"file-update\"}' \
|
|
2607
|
-
WHERE id = 'file-readme'",
|
|
2608
|
-
&[],
|
|
2609
|
-
)
|
|
2610
|
-
.await
|
|
2611
|
-
.expect("UPDATE lix_file should stage rewritten descriptor");
|
|
2612
|
-
|
|
2613
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2614
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2615
|
-
|
|
2616
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2617
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2618
|
-
let overlay = staged_writes.deltas[0]
|
|
2619
|
-
.pending_write_overlay()
|
|
2620
|
-
.expect("staged delta should expose pending overlay");
|
|
2621
|
-
let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
|
|
2622
|
-
assert_eq!(rows.len(), 1);
|
|
2623
|
-
assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
|
|
2624
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2625
|
-
let snapshot: JsonValue =
|
|
2626
|
-
serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
|
|
2627
|
-
.expect("descriptor snapshot JSON");
|
|
2628
|
-
assert_eq!(snapshot["id"], "file-readme");
|
|
2629
|
-
assert_eq!(snapshot["directory_id"], "dir-docs");
|
|
2630
|
-
assert_eq!(snapshot["name"], "readme-updated.txt");
|
|
2631
|
-
assert_eq!(snapshot["hidden"], true);
|
|
2632
|
-
assert_eq!(
|
|
2633
|
-
rows[0].metadata.as_deref(),
|
|
2634
|
-
Some("{\"source\":\"file-update\"}")
|
|
2635
|
-
);
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
#[tokio::test]
|
|
2639
|
-
async fn execute_sql_update_file_stages_data_blob_ref() {
|
|
2640
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2641
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2642
|
-
rows: vec![
|
|
2643
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2644
|
-
live_file_row(
|
|
2645
|
-
"file-readme",
|
|
2646
|
-
"version-a",
|
|
2647
|
-
Some("dir-docs"),
|
|
2648
|
-
"readme.md",
|
|
2649
|
-
false,
|
|
2650
|
-
),
|
|
2651
|
-
],
|
|
2652
|
-
});
|
|
2653
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2654
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2655
|
-
active_version_id: "version-a",
|
|
2656
|
-
blob_reader,
|
|
2657
|
-
live_state,
|
|
2658
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2659
|
-
schema_definitions: vec![],
|
|
2660
|
-
};
|
|
2661
|
-
|
|
2662
|
-
let result = execute_write_sql(
|
|
2663
|
-
&mut ctx,
|
|
2664
|
-
"UPDATE lix_file SET data = X'4142' WHERE id = 'file-readme'",
|
|
2665
|
-
&[],
|
|
2666
|
-
)
|
|
2667
|
-
.await
|
|
2668
|
-
.expect("UPDATE lix_file should stage data write");
|
|
2669
|
-
|
|
2670
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2671
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2672
|
-
|
|
2673
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2674
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2675
|
-
let overlay = staged_writes.deltas[0]
|
|
2676
|
-
.pending_write_overlay()
|
|
2677
|
-
.expect("staged delta should expose pending overlay");
|
|
2678
|
-
assert!(overlay
|
|
2679
|
-
.visible_semantic_rows(false, "lix_file_descriptor")
|
|
2680
|
-
.is_empty());
|
|
2681
|
-
let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
|
|
2682
|
-
assert_eq!(blob_ref_rows.len(), 1);
|
|
2683
|
-
assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
|
|
2684
|
-
let snapshot: JsonValue =
|
|
2685
|
-
serde_json::from_str(blob_ref_rows[0].snapshot_content.as_deref().unwrap())
|
|
2686
|
-
.expect("blob ref snapshot JSON");
|
|
2687
|
-
assert_eq!(snapshot["id"], "file-readme");
|
|
2688
|
-
assert_eq!(snapshot["size_bytes"], 2);
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
#[tokio::test]
|
|
2692
|
-
async fn execute_sql_update_file_stages_path_assignment() {
|
|
2693
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2694
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2695
|
-
rows: vec![
|
|
2696
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2697
|
-
live_file_row(
|
|
2698
|
-
"file-readme",
|
|
2699
|
-
"version-a",
|
|
2700
|
-
Some("dir-docs"),
|
|
2701
|
-
"readme.md",
|
|
2702
|
-
false,
|
|
2703
|
-
),
|
|
2704
|
-
],
|
|
2705
|
-
});
|
|
2706
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2707
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2708
|
-
active_version_id: "version-a",
|
|
2709
|
-
blob_reader,
|
|
2710
|
-
live_state,
|
|
2711
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2712
|
-
schema_definitions: vec![],
|
|
2713
|
-
};
|
|
2714
|
-
|
|
2715
|
-
let result = execute_write_sql(
|
|
2716
|
-
&mut ctx,
|
|
2717
|
-
"UPDATE lix_file SET path = '/docs/renamed.md' WHERE id = 'file-readme'",
|
|
2718
|
-
&[],
|
|
2719
|
-
)
|
|
2720
|
-
.await
|
|
2721
|
-
.expect("path update should stage descriptor rewrite");
|
|
2722
|
-
|
|
2723
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2724
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2725
|
-
|
|
2726
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2727
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2728
|
-
let overlay = staged_writes.deltas[0]
|
|
2729
|
-
.pending_write_overlay()
|
|
2730
|
-
.expect("staged delta should expose pending overlay");
|
|
2731
|
-
let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
|
|
2732
|
-
assert_eq!(rows.len(), 1);
|
|
2733
|
-
let snapshot: JsonValue =
|
|
2734
|
-
serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
|
|
2735
|
-
.expect("descriptor snapshot JSON");
|
|
2736
|
-
assert_eq!(snapshot["directory_id"], "dir-docs");
|
|
2737
|
-
assert_eq!(snapshot["name"], "renamed.md");
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
#[tokio::test]
|
|
2741
|
-
async fn execute_sql_delete_file_by_version_stages_descriptor_tombstone() {
|
|
2742
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2743
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2744
|
-
rows: vec![
|
|
2745
|
-
live_directory_row("dir-docs", "version-a", None, "docs", false),
|
|
2746
|
-
live_directory_row("dir-docs", "version-b", None, "docs", false),
|
|
2747
|
-
live_file_row(
|
|
2748
|
-
"file-readme",
|
|
2749
|
-
"version-a",
|
|
2750
|
-
Some("dir-docs"),
|
|
2751
|
-
"readme.md",
|
|
2752
|
-
false,
|
|
2753
|
-
),
|
|
2754
|
-
live_file_row(
|
|
2755
|
-
"file-guide",
|
|
2756
|
-
"version-b",
|
|
2757
|
-
Some("dir-docs"),
|
|
2758
|
-
"guide.md",
|
|
2759
|
-
false,
|
|
2760
|
-
),
|
|
2761
|
-
],
|
|
2762
|
-
});
|
|
2763
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2764
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2765
|
-
active_version_id: "version-a",
|
|
2766
|
-
blob_reader,
|
|
2767
|
-
live_state,
|
|
2768
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2769
|
-
schema_definitions: vec![],
|
|
2770
|
-
};
|
|
2771
|
-
|
|
2772
|
-
let result = execute_write_sql(
|
|
2773
|
-
&mut ctx,
|
|
2774
|
-
"DELETE FROM lix_file_by_version \
|
|
2775
|
-
WHERE id = 'file-guide' AND lixcol_version_id = 'version-b'",
|
|
2776
|
-
&[],
|
|
2777
|
-
)
|
|
2778
|
-
.await
|
|
2779
|
-
.expect("DELETE lix_file_by_version should stage descriptor tombstone");
|
|
2780
|
-
|
|
2781
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2782
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2783
|
-
|
|
2784
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2785
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2786
|
-
let overlay = staged_writes.deltas[0]
|
|
2787
|
-
.pending_write_overlay()
|
|
2788
|
-
.expect("staged delta should expose pending overlay");
|
|
2789
|
-
let rows = overlay.visible_all_semantic_rows();
|
|
2790
|
-
assert_eq!(rows.len(), 1);
|
|
2791
|
-
assert_eq!(rows[0].entity_id, "[\"file-guide\"]");
|
|
2792
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2793
|
-
assert!(rows[0].tombstone);
|
|
2794
|
-
assert_eq!(rows[0].snapshot_content, None);
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
#[tokio::test]
|
|
2798
|
-
async fn execute_sql_update_entity_surface_stages_rewritten_snapshot() {
|
|
2799
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2800
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2801
|
-
rows: vec![
|
|
2802
|
-
live_entity_row("entity-a", "version-a", "A"),
|
|
2803
|
-
live_entity_row("entity-b", "version-a", "B"),
|
|
2804
|
-
],
|
|
2805
|
-
});
|
|
2806
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2807
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2808
|
-
active_version_id: "version-a",
|
|
2809
|
-
blob_reader,
|
|
2810
|
-
live_state,
|
|
2811
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2812
|
-
schema_definitions: vec![json!({
|
|
2813
|
-
"x-lix-key": "test_state_schema",
|
|
2814
|
-
"type": "object",
|
|
2815
|
-
"properties": {
|
|
2816
|
-
"value": { "type": "string" }
|
|
2817
|
-
}
|
|
2818
|
-
})],
|
|
2819
|
-
};
|
|
2820
|
-
|
|
2821
|
-
let result = execute_write_sql(
|
|
2822
|
-
&mut ctx,
|
|
2823
|
-
"UPDATE test_state_schema \
|
|
2824
|
-
SET value = 'updated', lixcol_metadata = '{\"source\":\"entity-update\"}' \
|
|
2825
|
-
WHERE value = 'A'",
|
|
2826
|
-
&[],
|
|
2827
|
-
)
|
|
2828
|
-
.await
|
|
2829
|
-
.expect("UPDATE entity surface should stage rewritten row");
|
|
2830
|
-
|
|
2831
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2832
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2833
|
-
|
|
2834
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2835
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2836
|
-
let overlay = staged_writes.deltas[0]
|
|
2837
|
-
.pending_write_overlay()
|
|
2838
|
-
.expect("staged delta should expose pending overlay");
|
|
2839
|
-
let rows = overlay.visible_semantic_rows(false, "test_state_schema");
|
|
2840
|
-
assert_eq!(rows.len(), 1);
|
|
2841
|
-
assert_eq!(rows[0].entity_id, "[\"entity-a\"]");
|
|
2842
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2843
|
-
assert_eq!(
|
|
2844
|
-
rows[0].snapshot_content.as_deref(),
|
|
2845
|
-
Some("{\"value\":\"updated\"}")
|
|
2846
|
-
);
|
|
2847
|
-
assert_eq!(
|
|
2848
|
-
rows[0].metadata.as_deref(),
|
|
2849
|
-
Some("{\"source\":\"entity-update\"}")
|
|
2850
|
-
);
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
#[tokio::test]
|
|
2854
|
-
async fn execute_sql_delete_entity_by_version_stages_tombstone() {
|
|
2855
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2856
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2857
|
-
rows: vec![
|
|
2858
|
-
live_entity_row("entity-a", "version-a", "A"),
|
|
2859
|
-
live_entity_row("entity-b", "version-b", "B"),
|
|
2860
|
-
],
|
|
2861
|
-
});
|
|
2862
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2863
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2864
|
-
active_version_id: "version-a",
|
|
2865
|
-
blob_reader,
|
|
2866
|
-
live_state,
|
|
2867
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2868
|
-
schema_definitions: vec![json!({
|
|
2869
|
-
"x-lix-key": "test_state_schema",
|
|
2870
|
-
"type": "object",
|
|
2871
|
-
"properties": {
|
|
2872
|
-
"value": { "type": "string" }
|
|
2873
|
-
}
|
|
2874
|
-
})],
|
|
2875
|
-
};
|
|
2876
|
-
|
|
2877
|
-
let result = execute_write_sql(
|
|
2878
|
-
&mut ctx,
|
|
2879
|
-
"DELETE FROM test_state_schema_by_version \
|
|
2880
|
-
WHERE lixcol_version_id = 'version-b'",
|
|
2881
|
-
&[],
|
|
2882
|
-
)
|
|
2883
|
-
.await
|
|
2884
|
-
.expect("DELETE entity by-version surface should stage tombstone");
|
|
2885
|
-
|
|
2886
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2887
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2888
|
-
|
|
2889
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2890
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2891
|
-
let overlay = staged_writes.deltas[0]
|
|
2892
|
-
.pending_write_overlay()
|
|
2893
|
-
.expect("staged delta should expose pending overlay");
|
|
2894
|
-
let rows = overlay.visible_all_semantic_rows();
|
|
2895
|
-
assert_eq!(rows.len(), 1);
|
|
2896
|
-
assert_eq!(rows[0].entity_id, "[\"entity-b\"]");
|
|
2897
|
-
assert_eq!(rows[0].version_id, "version-b");
|
|
2898
|
-
assert!(rows[0].tombstone);
|
|
2899
|
-
assert_eq!(rows[0].snapshot_content, None);
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
#[tokio::test]
|
|
2903
|
-
async fn execute_sql_update_lix_state_stages_rewritten_rows() {
|
|
2904
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2905
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2906
|
-
rows: vec![
|
|
2907
|
-
live_lix_state_row("entity-1", Some("{\"source\":\"match\"}")),
|
|
2908
|
-
live_lix_state_row("entity-2", Some("{\"source\":\"skip\"}")),
|
|
2909
|
-
],
|
|
2910
|
-
});
|
|
2911
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2912
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2913
|
-
active_version_id: "version-a",
|
|
2914
|
-
blob_reader,
|
|
2915
|
-
live_state,
|
|
2916
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2917
|
-
schema_definitions: vec![],
|
|
2918
|
-
};
|
|
2919
|
-
|
|
2920
|
-
let result = execute_write_sql(
|
|
2921
|
-
&mut ctx,
|
|
2922
|
-
"UPDATE lix_state \
|
|
2923
|
-
SET snapshot_content = '{\"key\":\"hello\",\"value\":\"updated\"}', \
|
|
2924
|
-
metadata = '{\"schema_key\":\"lix_key_value\"}' \
|
|
2925
|
-
WHERE metadata = lix_json('{\"source\":\"match\"}')",
|
|
2926
|
-
&[],
|
|
2927
|
-
)
|
|
2928
|
-
.await
|
|
2929
|
-
.expect("UPDATE lix_state should stage rewritten rows");
|
|
2930
|
-
|
|
2931
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2932
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
|
|
2933
|
-
|
|
2934
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2935
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2936
|
-
let overlay = staged_writes.deltas[0]
|
|
2937
|
-
.pending_write_overlay()
|
|
2938
|
-
.expect("staged delta should expose pending overlay");
|
|
2939
|
-
let rows = overlay.visible_semantic_rows(false, "lix_key_value");
|
|
2940
|
-
assert_eq!(rows.len(), 1);
|
|
2941
|
-
assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
|
|
2942
|
-
assert_eq!(rows[0].version_id, "version-a");
|
|
2943
|
-
assert_eq!(
|
|
2944
|
-
rows[0].snapshot_content.as_deref(),
|
|
2945
|
-
Some("{\"key\":\"hello\",\"value\":\"updated\"}")
|
|
2946
|
-
);
|
|
2947
|
-
assert_eq!(
|
|
2948
|
-
rows[0].metadata.as_deref(),
|
|
2949
|
-
Some("{\"schema_key\":\"lix_key_value\"}")
|
|
2950
|
-
);
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
#[tokio::test]
|
|
2954
|
-
async fn execute_sql_delete_lix_state_without_where_stages_all_rows() {
|
|
2955
|
-
let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
|
|
2956
|
-
let live_state = Arc::new(RowsLiveStateReader {
|
|
2957
|
-
rows: vec![
|
|
2958
|
-
live_lix_state_row("entity-1", Some("{\"source\":\"one\"}")),
|
|
2959
|
-
live_lix_state_row("entity-2", Some("{\"source\":\"two\"}")),
|
|
2960
|
-
],
|
|
2961
|
-
});
|
|
2962
|
-
let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
|
|
2963
|
-
let mut ctx = DummySqlWriteExecutionContext {
|
|
2964
|
-
active_version_id: "version-a",
|
|
2965
|
-
blob_reader,
|
|
2966
|
-
live_state,
|
|
2967
|
-
staged_writes: Arc::clone(&staged_writes),
|
|
2968
|
-
schema_definitions: vec![],
|
|
2969
|
-
};
|
|
2970
|
-
|
|
2971
|
-
let result = execute_write_sql(&mut ctx, "DELETE FROM lix_state", &[])
|
|
2972
|
-
.await
|
|
2973
|
-
.expect("DELETE FROM lix_state should follow DataFusion delete-all semantics");
|
|
2974
|
-
|
|
2975
|
-
assert_eq!(result.columns, vec!["count"]);
|
|
2976
|
-
assert_eq!(result.rows, vec![vec![Value::Integer(2)]]);
|
|
2977
|
-
|
|
2978
|
-
let staged_writes = staged_writes.lock().expect("staged writes lock");
|
|
2979
|
-
assert_eq!(staged_writes.deltas.len(), 1);
|
|
2980
|
-
let overlay = staged_writes.deltas[0]
|
|
2981
|
-
.pending_write_overlay()
|
|
2982
|
-
.expect("staged delta should expose pending overlay");
|
|
2983
|
-
let rows = overlay.visible_all_semantic_rows();
|
|
2984
|
-
assert_eq!(rows.len(), 2);
|
|
2985
|
-
assert!(rows.iter().all(|row| row.tombstone));
|
|
2986
|
-
assert!(rows.iter().all(|row| row.snapshot_content.is_none()));
|
|
2987
|
-
assert!(rows.iter().any(|row| row.entity_id == "[\"entity-1\"]"));
|
|
2988
|
-
assert!(rows.iter().any(|row| row.entity_id == "[\"entity-2\"]"));
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
struct BackendSqlExecutionContext<'a> {
|
|
2992
|
-
active_version_id: &'a str,
|
|
2993
|
-
storage: StorageContext,
|
|
2994
|
-
blob_reader: Arc<dyn BlobDataReader>,
|
|
2995
|
-
live_state: Arc<dyn LiveStateReader>,
|
|
2996
|
-
schema_definitions: Vec<JsonValue>,
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
impl SqlExecutionContext for BackendSqlExecutionContext<'_> {
|
|
3000
|
-
fn active_version_id(&self) -> &str {
|
|
3001
|
-
self.active_version_id
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
fn live_state(&self) -> Arc<dyn LiveStateReader> {
|
|
3005
|
-
Arc::clone(&self.live_state)
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
fn functions(&self) -> FunctionProviderHandle {
|
|
3009
|
-
test_functions()
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
|
|
3013
|
-
Arc::clone(&self.blob_reader)
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
|
|
3017
|
-
let base_scope = test_read_scope(self.storage.clone());
|
|
3018
|
-
let read_scope = StorageReadScope::new(base_scope.store());
|
|
3019
|
-
CommitStoreQuerySource {
|
|
3020
|
-
commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
|
|
3021
|
-
json_reader: JsonStoreContext::new().reader(read_scope.store()),
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
|
|
3026
|
-
Box::new(DummyCommitGraphReader)
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
fn version_ref(&self) -> Arc<dyn VersionRefReader> {
|
|
3030
|
-
Arc::new(
|
|
3031
|
-
crate::version::VersionContext::new(Arc::new(UntrackedStateContext::new()))
|
|
3032
|
-
.ref_reader(self.storage.clone()),
|
|
3033
|
-
)
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
|
|
3037
|
-
Ok(self.schema_definitions.clone())
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
async fn setup_sql2_state_fixture(
|
|
3042
|
-
) -> Result<(crate::backend::testing::UnitTestBackend, JsonValue), crate::LixError> {
|
|
3043
|
-
let backend = crate::backend::testing::UnitTestBackend::new();
|
|
3044
|
-
let init_receipt = Engine::initialize(Box::new(backend.clone())).await?;
|
|
3045
|
-
let storage = crate::storage::StorageContext::new(std::sync::Arc::new(backend.clone()));
|
|
3046
|
-
{
|
|
3047
|
-
let mut transaction = storage.begin_write_transaction().await?;
|
|
3048
|
-
let version_ctx = crate::version::VersionContext::new(Arc::new(
|
|
3049
|
-
crate::untracked_state::UntrackedStateContext::new(),
|
|
3050
|
-
));
|
|
3051
|
-
let mut writes = StorageWriteSet::new();
|
|
3052
|
-
let canonical_rows = vec![
|
|
3053
|
-
prepare_version_ref_row(
|
|
3054
|
-
"version-a",
|
|
3055
|
-
&init_receipt.initial_commit_id,
|
|
3056
|
-
"1970-01-01T00:00:00.000Z",
|
|
3057
|
-
)?,
|
|
3058
|
-
prepare_version_ref_row(
|
|
3059
|
-
"version-b",
|
|
3060
|
-
&init_receipt.initial_commit_id,
|
|
3061
|
-
"1970-01-01T00:00:00.000Z",
|
|
3062
|
-
)?,
|
|
3063
|
-
];
|
|
3064
|
-
let rows = canonical_rows
|
|
3065
|
-
.into_iter()
|
|
3066
|
-
.map(|prepared| prepared.row)
|
|
3067
|
-
.collect::<Vec<_>>();
|
|
3068
|
-
version_ctx.stage_canonical_ref_rows(&mut writes, &rows)?;
|
|
3069
|
-
writes.apply(&mut transaction.as_mut()).await?;
|
|
3070
|
-
transaction.commit().await?;
|
|
3071
|
-
}
|
|
3072
|
-
let engine = Engine::new(Box::new(backend.clone())).await?;
|
|
3073
|
-
let session_a = engine.open_session("version-a").await?;
|
|
3074
|
-
let session_b = engine.open_session("version-b").await?;
|
|
3075
|
-
let schema_definition = json!({
|
|
3076
|
-
"x-lix-key": "test_state_schema",
|
|
3077
|
-
"type": "object",
|
|
3078
|
-
"properties": {
|
|
3079
|
-
"value": { "type": "string" }
|
|
3080
|
-
},
|
|
3081
|
-
"required": ["value"],
|
|
3082
|
-
"additionalProperties": false
|
|
3083
|
-
});
|
|
3084
|
-
session_a
|
|
3085
|
-
.execute(
|
|
3086
|
-
"INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
|
|
3087
|
-
VALUES (\
|
|
3088
|
-
lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
|
|
3089
|
-
false,\
|
|
3090
|
-
false\
|
|
3091
|
-
)",
|
|
3092
|
-
&[],
|
|
3093
|
-
)
|
|
3094
|
-
.await?;
|
|
3095
|
-
session_b
|
|
3096
|
-
.execute(
|
|
3097
|
-
"INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
|
|
3098
|
-
VALUES (\
|
|
3099
|
-
lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
|
|
3100
|
-
false,\
|
|
3101
|
-
false\
|
|
3102
|
-
)",
|
|
3103
|
-
&[],
|
|
3104
|
-
)
|
|
3105
|
-
.await?;
|
|
3106
|
-
session_a
|
|
3107
|
-
.execute(
|
|
3108
|
-
"INSERT INTO lix_state (\
|
|
3109
|
-
entity_id, schema_key, file_id, snapshot_content, global, untracked\
|
|
3110
|
-
) VALUES (\
|
|
3111
|
-
lix_json('[\"entity-a\"]'), 'test_state_schema', NULL, '{\"value\":\"A\"}', false, false\
|
|
3112
|
-
)",
|
|
3113
|
-
&[],
|
|
3114
|
-
)
|
|
3115
|
-
.await?;
|
|
3116
|
-
session_b
|
|
3117
|
-
.execute(
|
|
3118
|
-
"INSERT INTO lix_state (\
|
|
3119
|
-
entity_id, schema_key, file_id, snapshot_content, global, untracked\
|
|
3120
|
-
) VALUES (\
|
|
3121
|
-
lix_json('[\"entity-b\"]'), 'test_state_schema', NULL, '{\"value\":\"B\"}', false, false\
|
|
3122
|
-
)",
|
|
3123
|
-
&[],
|
|
3124
|
-
)
|
|
3125
|
-
.await?;
|
|
3126
|
-
session_a
|
|
3127
|
-
.execute(
|
|
3128
|
-
"INSERT INTO lix_state (\
|
|
3129
|
-
entity_id, schema_key, file_id, snapshot_content, global, untracked\
|
|
3130
|
-
) VALUES (\
|
|
3131
|
-
lix_json('[\"dir-docs\"]'), 'lix_directory_descriptor', NULL, '{\"id\":\"dir-docs\",\"parent_id\":null,\"name\":\"docs\",\"hidden\":false}', false, false\
|
|
3132
|
-
)",
|
|
3133
|
-
&[],
|
|
3134
|
-
)
|
|
3135
|
-
.await?;
|
|
3136
|
-
session_a
|
|
3137
|
-
.execute(
|
|
3138
|
-
"INSERT INTO lix_file (id, path, data) \
|
|
3139
|
-
VALUES ('file-a', '/docs/readme.md', X'4142')",
|
|
3140
|
-
&[],
|
|
3141
|
-
)
|
|
3142
|
-
.await?;
|
|
3143
|
-
Ok((backend, schema_definition))
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
fn test_live_state_context() -> LiveStateContext {
|
|
3147
|
-
LiveStateContext::new(
|
|
3148
|
-
TrackedStateContext::new(),
|
|
3149
|
-
UntrackedStateContext::new(),
|
|
3150
|
-
crate::commit_graph::CommitGraphContext::new(),
|
|
3151
|
-
)
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
fn run_async_test_with_large_stack(
|
|
3155
|
-
test: impl FnOnce() -> futures_util::future::LocalBoxFuture<'static, ()> + Send + 'static,
|
|
3156
|
-
) {
|
|
3157
|
-
std::thread::Builder::new()
|
|
3158
|
-
.name("sql2-execute-test".to_string())
|
|
3159
|
-
.stack_size(32 * 1024 * 1024)
|
|
3160
|
-
.spawn(move || {
|
|
3161
|
-
tokio::runtime::Builder::new_current_thread()
|
|
3162
|
-
.enable_all()
|
|
3163
|
-
.build()
|
|
3164
|
-
.expect("test runtime should build")
|
|
3165
|
-
.block_on(test());
|
|
3166
|
-
})
|
|
3167
|
-
.expect("test thread should spawn")
|
|
3168
|
-
.join()
|
|
3169
|
-
.expect("test thread should join");
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
#[test]
|
|
3173
|
-
fn execute_sql_reads_lix_state_by_version() {
|
|
3174
|
-
run_async_test_with_large_stack(|| {
|
|
3175
|
-
Box::pin(async move {
|
|
3176
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3177
|
-
.await
|
|
3178
|
-
.expect("fixture should initialize");
|
|
3179
|
-
let backend = Arc::new(backend);
|
|
3180
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3181
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3182
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3183
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3184
|
-
let ctx = BackendSqlExecutionContext {
|
|
3185
|
-
active_version_id: "version-a",
|
|
3186
|
-
storage: storage.clone(),
|
|
3187
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3188
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3189
|
-
schema_definitions: vec![schema_definition],
|
|
3190
|
-
};
|
|
3191
|
-
|
|
3192
|
-
let result = execute_sql(
|
|
3193
|
-
&ctx,
|
|
3194
|
-
"SELECT entity_id, version_id, snapshot_content, commit_id \
|
|
3195
|
-
FROM lix_state_by_version \
|
|
3196
|
-
WHERE version_id = 'version-b' AND schema_key = 'test_state_schema'",
|
|
3197
|
-
&[],
|
|
3198
|
-
)
|
|
3199
|
-
.await
|
|
3200
|
-
.expect("sql2 execute should read lix_state_by_version");
|
|
3201
|
-
|
|
3202
|
-
assert_eq!(
|
|
3203
|
-
result.columns,
|
|
3204
|
-
vec!["entity_id", "version_id", "snapshot_content", "commit_id"]
|
|
3205
|
-
);
|
|
3206
|
-
assert_eq!(result.rows.len(), 1);
|
|
3207
|
-
assert_eq!(result.rows[0][0], Value::Json(json!(["entity-b"])));
|
|
3208
|
-
assert_eq!(result.rows[0][1], Value::Text("version-b".to_string()));
|
|
3209
|
-
assert_eq!(result.rows[0][2], Value::Json(json!({"value": "B"})));
|
|
3210
|
-
match &result.rows[0][3] {
|
|
3211
|
-
Value::Text(commit_id) => assert!(!commit_id.is_empty()),
|
|
3212
|
-
other => panic!("expected non-null commit_id text, got {other:?}"),
|
|
3213
|
-
}
|
|
3214
|
-
})
|
|
3215
|
-
});
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
#[test]
|
|
3219
|
-
fn execute_sql_supports_broad_lix_state_by_version_reads() {
|
|
3220
|
-
run_async_test_with_large_stack(|| {
|
|
3221
|
-
Box::pin(async move {
|
|
3222
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3223
|
-
.await
|
|
3224
|
-
.expect("fixture should initialize");
|
|
3225
|
-
let backend = Arc::new(backend);
|
|
3226
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3227
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3228
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3229
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3230
|
-
let ctx = BackendSqlExecutionContext {
|
|
3231
|
-
active_version_id: "version-a",
|
|
3232
|
-
storage: storage.clone(),
|
|
3233
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3234
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3235
|
-
schema_definitions: vec![schema_definition],
|
|
3236
|
-
};
|
|
3237
|
-
|
|
3238
|
-
let result = execute_sql(
|
|
3239
|
-
&ctx,
|
|
3240
|
-
"SELECT entity_id FROM lix_state_by_version WHERE schema_key = 'test_state_schema'",
|
|
3241
|
-
&[],
|
|
3242
|
-
)
|
|
3243
|
-
.await
|
|
3244
|
-
.expect("broad by-version read should succeed");
|
|
3245
|
-
|
|
3246
|
-
assert!(
|
|
3247
|
-
result.rows.iter().any(|row| row[0] == Value::Json(json!(["entity-a"])))
|
|
3248
|
-
&& result.rows.iter().any(|row| row[0] == Value::Json(json!(["entity-b"]))),
|
|
3249
|
-
"expected broad by-version read to include rows from multiple visible versions: {:?}",
|
|
3250
|
-
result.rows
|
|
3251
|
-
);
|
|
3252
|
-
})
|
|
3253
|
-
});
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
#[test]
|
|
3257
|
-
fn execute_sql_reads_lix_state_from_active_version() {
|
|
3258
|
-
run_async_test_with_large_stack(|| {
|
|
3259
|
-
Box::pin(async move {
|
|
3260
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3261
|
-
.await
|
|
3262
|
-
.expect("fixture should initialize");
|
|
3263
|
-
let backend = Arc::new(backend);
|
|
3264
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3265
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3266
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3267
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3268
|
-
let ctx = BackendSqlExecutionContext {
|
|
3269
|
-
active_version_id: "version-a",
|
|
3270
|
-
storage: storage.clone(),
|
|
3271
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3272
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3273
|
-
schema_definitions: vec![schema_definition],
|
|
3274
|
-
};
|
|
3275
|
-
|
|
3276
|
-
let result = execute_sql(
|
|
3277
|
-
&ctx,
|
|
3278
|
-
"SELECT entity_id, snapshot_content \
|
|
3279
|
-
FROM lix_state \
|
|
3280
|
-
WHERE schema_key = 'test_state_schema'",
|
|
3281
|
-
&[],
|
|
3282
|
-
)
|
|
3283
|
-
.await
|
|
3284
|
-
.expect("sql2 execute should read lix_state");
|
|
3285
|
-
|
|
3286
|
-
assert_eq!(result.columns, vec!["entity_id", "snapshot_content"]);
|
|
3287
|
-
assert_eq!(result.rows.len(), 1);
|
|
3288
|
-
assert_eq!(result.rows[0][0], Value::Json(json!(["entity-a"])));
|
|
3289
|
-
assert_eq!(result.rows[0][1], Value::Json(json!({"value": "A"})));
|
|
3290
|
-
})
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
#[test]
|
|
3295
|
-
fn execute_sql_reads_entity_view_from_active_version() {
|
|
3296
|
-
run_async_test_with_large_stack(|| {
|
|
3297
|
-
Box::pin(async move {
|
|
3298
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3299
|
-
.await
|
|
3300
|
-
.expect("fixture should initialize");
|
|
3301
|
-
let backend = Arc::new(backend);
|
|
3302
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3303
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3304
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3305
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3306
|
-
let ctx = BackendSqlExecutionContext {
|
|
3307
|
-
active_version_id: "version-a",
|
|
3308
|
-
storage: storage.clone(),
|
|
3309
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3310
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3311
|
-
schema_definitions: vec![schema_definition],
|
|
3312
|
-
};
|
|
3313
|
-
|
|
3314
|
-
let result = execute_sql(
|
|
3315
|
-
&ctx,
|
|
3316
|
-
"SELECT value, lixcol_entity_id \
|
|
3317
|
-
FROM test_state_schema",
|
|
3318
|
-
&[],
|
|
3319
|
-
)
|
|
3320
|
-
.await
|
|
3321
|
-
.expect("sql2 execute should read entity view");
|
|
3322
|
-
|
|
3323
|
-
assert_eq!(result.columns, vec!["value", "lixcol_entity_id"]);
|
|
3324
|
-
assert_eq!(result.rows.len(), 1);
|
|
3325
|
-
assert_eq!(result.rows[0][0], Value::Text("A".to_string()));
|
|
3326
|
-
assert_eq!(result.rows[0][1], Value::Json(json!(["entity-a"])));
|
|
3327
|
-
})
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
#[test]
|
|
3332
|
-
fn execute_sql_reads_entity_by_version_view() {
|
|
3333
|
-
run_async_test_with_large_stack(|| {
|
|
3334
|
-
Box::pin(async move {
|
|
3335
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3336
|
-
.await
|
|
3337
|
-
.expect("fixture should initialize");
|
|
3338
|
-
let backend = Arc::new(backend);
|
|
3339
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3340
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3341
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3342
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3343
|
-
let ctx = BackendSqlExecutionContext {
|
|
3344
|
-
active_version_id: "version-a",
|
|
3345
|
-
storage: storage.clone(),
|
|
3346
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3347
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3348
|
-
schema_definitions: vec![schema_definition],
|
|
3349
|
-
};
|
|
3350
|
-
|
|
3351
|
-
let result = execute_sql(
|
|
3352
|
-
&ctx,
|
|
3353
|
-
"SELECT value, lixcol_version_id \
|
|
3354
|
-
FROM test_state_schema_by_version \
|
|
3355
|
-
WHERE lixcol_version_id = 'version-b'",
|
|
3356
|
-
&[],
|
|
3357
|
-
)
|
|
3358
|
-
.await
|
|
3359
|
-
.expect("sql2 execute should read entity by-version view");
|
|
3360
|
-
|
|
3361
|
-
assert_eq!(result.columns, vec!["value", "lixcol_version_id"]);
|
|
3362
|
-
assert_eq!(result.rows.len(), 1);
|
|
3363
|
-
assert_eq!(result.rows[0][0], Value::Text("B".to_string()));
|
|
3364
|
-
assert_eq!(result.rows[0][1], Value::Text("version-b".to_string()));
|
|
3365
|
-
})
|
|
3366
|
-
});
|
|
3367
|
-
}
|
|
3368
|
-
|
|
3369
|
-
#[test]
|
|
3370
|
-
fn execute_sql_reads_lix_directory_by_version_view() {
|
|
3371
|
-
run_async_test_with_large_stack(|| {
|
|
3372
|
-
Box::pin(async move {
|
|
3373
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3374
|
-
.await
|
|
3375
|
-
.expect("fixture should initialize");
|
|
3376
|
-
let backend = Arc::new(backend);
|
|
3377
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3378
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3379
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3380
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3381
|
-
let ctx = BackendSqlExecutionContext {
|
|
3382
|
-
active_version_id: "version-a",
|
|
3383
|
-
storage: storage.clone(),
|
|
3384
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3385
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3386
|
-
schema_definitions: vec![schema_definition],
|
|
3387
|
-
};
|
|
3388
|
-
|
|
3389
|
-
let result = execute_sql(
|
|
3390
|
-
&ctx,
|
|
3391
|
-
"SELECT path, name, lixcol_version_id \
|
|
3392
|
-
FROM lix_directory_by_version \
|
|
3393
|
-
WHERE id = 'dir-docs' AND lixcol_version_id = 'version-a'",
|
|
3394
|
-
&[],
|
|
3395
|
-
)
|
|
3396
|
-
.await
|
|
3397
|
-
.expect("sql2 execute should read lix_directory_by_version");
|
|
3398
|
-
|
|
3399
|
-
assert_eq!(result.columns, vec!["path", "name", "lixcol_version_id"]);
|
|
3400
|
-
assert_eq!(result.rows.len(), 1);
|
|
3401
|
-
assert_eq!(result.rows[0][0], Value::Text("/docs/".to_string()));
|
|
3402
|
-
assert_eq!(result.rows[0][1], Value::Text("docs".to_string()));
|
|
3403
|
-
assert_eq!(result.rows[0][2], Value::Text("version-a".to_string()));
|
|
3404
|
-
})
|
|
3405
|
-
});
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
#[test]
|
|
3409
|
-
fn execute_sql_reads_lix_directory_from_active_version() {
|
|
3410
|
-
run_async_test_with_large_stack(|| {
|
|
3411
|
-
Box::pin(async move {
|
|
3412
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3413
|
-
.await
|
|
3414
|
-
.expect("fixture should initialize");
|
|
3415
|
-
let backend = Arc::new(backend);
|
|
3416
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3417
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3418
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3419
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3420
|
-
let ctx = BackendSqlExecutionContext {
|
|
3421
|
-
active_version_id: "version-a",
|
|
3422
|
-
storage: storage.clone(),
|
|
3423
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3424
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3425
|
-
schema_definitions: vec![schema_definition],
|
|
3426
|
-
};
|
|
3427
|
-
|
|
3428
|
-
let result = execute_sql(
|
|
3429
|
-
&ctx,
|
|
3430
|
-
"SELECT path, name \
|
|
3431
|
-
FROM lix_directory \
|
|
3432
|
-
WHERE id = 'dir-docs'",
|
|
3433
|
-
&[],
|
|
3434
|
-
)
|
|
3435
|
-
.await
|
|
3436
|
-
.expect("sql2 execute should read lix_directory");
|
|
3437
|
-
|
|
3438
|
-
assert_eq!(result.columns, vec!["path", "name"]);
|
|
3439
|
-
assert_eq!(result.rows.len(), 1);
|
|
3440
|
-
assert_eq!(result.rows[0][0], Value::Text("/docs/".to_string()));
|
|
3441
|
-
assert_eq!(result.rows[0][1], Value::Text("docs".to_string()));
|
|
3442
|
-
})
|
|
3443
|
-
});
|
|
3444
|
-
}
|
|
3445
|
-
|
|
3446
|
-
#[test]
|
|
3447
|
-
fn execute_sql_reads_lix_file_by_version_view() {
|
|
3448
|
-
run_async_test_with_large_stack(|| {
|
|
3449
|
-
Box::pin(async move {
|
|
3450
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3451
|
-
.await
|
|
3452
|
-
.expect("fixture should initialize");
|
|
3453
|
-
let backend = Arc::new(backend);
|
|
3454
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3455
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3456
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3457
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3458
|
-
let ctx = BackendSqlExecutionContext {
|
|
3459
|
-
active_version_id: "version-a",
|
|
3460
|
-
storage: storage.clone(),
|
|
3461
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3462
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3463
|
-
schema_definitions: vec![schema_definition],
|
|
3464
|
-
};
|
|
3465
|
-
|
|
3466
|
-
let result = execute_sql(
|
|
3467
|
-
&ctx,
|
|
3468
|
-
"SELECT path, name, data, lixcol_version_id \
|
|
3469
|
-
FROM lix_file_by_version \
|
|
3470
|
-
WHERE id = 'file-a' AND lixcol_version_id = 'version-a'",
|
|
3471
|
-
&[],
|
|
3472
|
-
)
|
|
3473
|
-
.await
|
|
3474
|
-
.expect("sql2 execute should read lix_file_by_version");
|
|
3475
|
-
|
|
3476
|
-
assert_eq!(
|
|
3477
|
-
result.columns,
|
|
3478
|
-
vec!["path", "name", "data", "lixcol_version_id"]
|
|
3479
|
-
);
|
|
3480
|
-
assert_eq!(result.rows.len(), 1);
|
|
3481
|
-
assert_eq!(
|
|
3482
|
-
result.rows[0][0],
|
|
3483
|
-
Value::Text("/docs/readme.md".to_string())
|
|
3484
|
-
);
|
|
3485
|
-
assert_eq!(result.rows[0][1], Value::Text("readme.md".to_string()));
|
|
3486
|
-
assert_eq!(result.rows[0][2], Value::Blob(vec![0x41, 0x42]));
|
|
3487
|
-
assert_eq!(result.rows[0][3], Value::Text("version-a".to_string()));
|
|
3488
|
-
})
|
|
3489
|
-
});
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
#[test]
|
|
3493
|
-
fn execute_sql_reads_lix_file_from_active_version() {
|
|
3494
|
-
run_async_test_with_large_stack(|| {
|
|
3495
|
-
Box::pin(async move {
|
|
3496
|
-
let (backend, schema_definition) = setup_sql2_state_fixture()
|
|
3497
|
-
.await
|
|
3498
|
-
.expect("fixture should initialize");
|
|
3499
|
-
let backend = Arc::new(backend);
|
|
3500
|
-
let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
|
|
3501
|
-
let storage = StorageContext::new(Arc::clone(&backend_ref));
|
|
3502
|
-
let blob_reader: Arc<dyn BlobDataReader> =
|
|
3503
|
-
Arc::new(BackendBlobReader(storage.clone()));
|
|
3504
|
-
let ctx = BackendSqlExecutionContext {
|
|
3505
|
-
active_version_id: "version-a",
|
|
3506
|
-
storage: storage.clone(),
|
|
3507
|
-
blob_reader: Arc::clone(&blob_reader),
|
|
3508
|
-
live_state: Arc::new(test_live_state_context().reader(storage.clone())),
|
|
3509
|
-
schema_definitions: vec![schema_definition],
|
|
3510
|
-
};
|
|
3511
|
-
|
|
3512
|
-
let result = execute_sql(
|
|
3513
|
-
&ctx,
|
|
3514
|
-
"SELECT path, name, data \
|
|
3515
|
-
FROM lix_file \
|
|
3516
|
-
WHERE id = 'file-a'",
|
|
3517
|
-
&[],
|
|
3518
|
-
)
|
|
3519
|
-
.await
|
|
3520
|
-
.expect("sql2 execute should read lix_file");
|
|
3521
|
-
|
|
3522
|
-
assert_eq!(result.columns, vec!["path", "name", "data"]);
|
|
3523
|
-
assert_eq!(result.rows.len(), 1);
|
|
3524
|
-
assert_eq!(
|
|
3525
|
-
result.rows[0][0],
|
|
3526
|
-
Value::Text("/docs/readme.md".to_string())
|
|
3527
|
-
);
|
|
3528
|
-
assert_eq!(result.rows[0][1], Value::Text("readme.md".to_string()));
|
|
3529
|
-
assert_eq!(result.rows[0][2], Value::Blob(vec![0x41, 0x42]));
|
|
3530
|
-
})
|
|
3531
|
-
});
|
|
3532
|
-
}
|
|
3533
|
-
}
|