icydb-core 0.204.0

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use super::*;

#[test]
fn metadata_mutation_plan_is_immediately_publishable() {
    let field = nullable_text_field("nickname", 3, 2);
    let fields = [field];
    let plan: MutationPlan = SchemaMutationRequest::AppendOnlyFields(&fields).into();

    assert_eq!(
        plan.publication_preflight(),
        super::MutationPublicationPreflight::PublishableNow,
    );
}

#[test]
fn index_mutation_plans_preserve_the_current_physical_target() {
    let field_path: MutationPlan =
        SchemaMutationRequest::from_accepted_field_path_index(&non_unique_name_index())
            .expect("field-path index should lower")
            .into();
    let expression: MutationPlan =
        SchemaMutationRequest::from_accepted_expression_index(&expression_name_index())
            .expect("expression index should lower")
            .into();

    assert_eq!(
        field_path.publication_preflight(),
        super::MutationPublicationPreflight::RequiresPhysicalWork,
    );
    let field_path_target = field_path
        .field_path_index_target()
        .expect("field-path plan should expose its runner target");
    assert_eq!(field_path_target.name(), "by_name");
    assert_eq!(field_path_target.store(), "test::mutation::by_name");
    assert_eq!(field_path_target.predicate_sql(), Some("name IS NOT NULL"));
    let [field_path_key] = field_path_target.key_paths() else {
        panic!("test field-path index should retain one accepted key");
    };
    assert_eq!(field_path_key.field_id(), FieldId::new(2));
    assert_eq!(
        field_path_key.kind(),
        &AcceptedFieldKind::Text { max_len: None },
    );
    assert!(!field_path_key.nullable());
    assert!(field_path.expression_index_target().is_none());

    assert_eq!(
        expression.publication_preflight(),
        super::MutationPublicationPreflight::RequiresPhysicalWork,
    );
    let expression_target = expression
        .expression_index_target()
        .expect("expression plan should expose its runner target");
    assert_eq!(expression_target.name(), "by_lower_name");
    assert_eq!(expression_target.store(), "test::mutation::by_lower_name");
    assert_eq!(
        expression_target.predicate_sql(),
        Some("LOWER(name) IS NOT NULL"),
    );
    let [super::SchemaExpressionIndexRebuildKey::Expression(expression_key)] =
        expression_target.key_items()
    else {
        panic!("test expression index should retain one expression key");
    };
    assert_eq!(
        expression_key.input_kind(),
        &AcceptedFieldKind::Text { max_len: None },
    );
    assert_eq!(
        expression_key.output_kind(),
        &AcceptedFieldKind::Text { max_len: None },
    );
    assert_eq!(expression_key.canonical_text(), "expr:v1:LOWER(name)");
    assert!(expression.field_path_index_target().is_none());
}

#[test]
fn runner_input_binds_accepted_snapshots_to_the_same_mutation_plan() {
    let before = base_snapshot();
    let added = nullable_text_field("nickname", 3, 2);
    let after = append_fields_snapshot(&before, std::slice::from_ref(&added));
    let plan: MutationPlan =
        SchemaMutationRequest::AppendOnlyFields(std::slice::from_ref(&added)).into();
    let input = super::SchemaMutationRunnerInput::new(&before, &after, plan.clone())
        .expect("same-entity accepted snapshots should build runner input");

    assert_eq!(
        input.accepted_after().fields().len(),
        before.fields().len() + 1,
    );
    assert_eq!(input.mutation_plan(), &plan);
}

#[test]
fn runner_input_rejects_cross_entity_snapshot_pairs() {
    let before = base_snapshot();
    let wrong_entity = PersistedSchemaSnapshot::new(
        before.version(),
        "test::OtherEntity".to_string(),
        before.entity_name().to_string(),
        before.first_primary_key_field_id(),
        before.row_layout().clone(),
        before.fields().to_vec(),
    );
    let wrong_name = PersistedSchemaSnapshot::new(
        before.version(),
        before.entity_path().to_string(),
        "OtherEntity".to_string(),
        before.first_primary_key_field_id(),
        before.row_layout().clone(),
        before.fields().to_vec(),
    );
    let wrong_pk = PersistedSchemaSnapshot::new(
        before.version(),
        before.entity_path().to_string(),
        before.entity_name().to_string(),
        FieldId::new(99),
        before.row_layout().clone(),
        before.fields().to_vec(),
    );

    assert_eq!(
        super::SchemaMutationRunnerInput::new(&before, &wrong_entity, MutationPlan::exact_match(),),
        Err(super::SchemaMutationRunnerInputError::EntityPath),
    );
    assert_eq!(
        super::SchemaMutationRunnerInput::new(&before, &wrong_name, MutationPlan::exact_match(),),
        Err(super::SchemaMutationRunnerInputError::EntityName),
    );
    assert_eq!(
        super::SchemaMutationRunnerInput::new(&before, &wrong_pk, MutationPlan::exact_match()),
        Err(super::SchemaMutationRunnerInputError::PrimaryKeyField),
    );
}

#[test]
fn field_path_index_request_lowering_fails_closed_for_unsupported_indexes() {
    let unique = PersistedIndexSnapshot::new(
        1,
        "unique_name".to_string(),
        "test::mutation::unique_name".to_string(),
        true,
        PersistedIndexKeySnapshot::FieldPath(vec![name_key_path()]),
        None,
    );
    let explicit_items = PersistedIndexSnapshot::new(
        2,
        "items_name".to_string(),
        "test::mutation::items_name".to_string(),
        false,
        PersistedIndexKeySnapshot::Items(vec![PersistedIndexKeyItemSnapshot::FieldPath(
            name_key_path(),
        )]),
        None,
    );
    let empty = PersistedIndexSnapshot::new(
        3,
        "empty_name".to_string(),
        "test::mutation::empty_name".to_string(),
        false,
        PersistedIndexKeySnapshot::FieldPath(Vec::new()),
        None,
    );

    let SchemaMutationRequest::AddFieldPathIndex { target } =
        SchemaMutationRequest::from_accepted_field_path_index(&unique)
            .expect("unique field-path indexes should lower")
    else {
        panic!("unique field-path index should preserve its target");
    };
    assert!(target.unique());
    assert_eq!(
        SchemaMutationRequest::from_accepted_field_path_index(&explicit_items),
        Err(AcceptedSchemaMutationError::UnsupportedIndexKeyShape),
    );
    assert_eq!(
        SchemaMutationRequest::from_accepted_field_path_index(&empty),
        Err(AcceptedSchemaMutationError::EmptyIndexKey),
    );
}

#[test]
fn expression_index_request_lowering_fails_closed_for_unsupported_indexes() {
    let field_path_only = non_unique_name_index();
    let items_without_expression = PersistedIndexSnapshot::new(
        2,
        "items_name".to_string(),
        "test::mutation::items_name".to_string(),
        false,
        PersistedIndexKeySnapshot::Items(vec![PersistedIndexKeyItemSnapshot::FieldPath(
            name_key_path(),
        )]),
        None,
    );
    let empty = PersistedIndexSnapshot::new(
        3,
        "empty_expression".to_string(),
        "test::mutation::empty_expression".to_string(),
        false,
        PersistedIndexKeySnapshot::Items(Vec::new()),
        None,
    );

    assert_eq!(
        SchemaMutationRequest::from_accepted_expression_index(&field_path_only),
        Err(AcceptedSchemaMutationError::UnsupportedIndexKeyShape),
    );
    assert_eq!(
        SchemaMutationRequest::from_accepted_expression_index(&items_without_expression),
        Err(AcceptedSchemaMutationError::ExpressionIndexRequiresExpressionKey),
    );
    assert_eq!(
        SchemaMutationRequest::from_accepted_expression_index(&empty),
        Err(AcceptedSchemaMutationError::EmptyIndexKey),
    );
}

#[test]
fn snapshot_delta_request_lowers_only_current_plan_shapes() {
    let stored = base_snapshot();
    let added = nullable_text_field("nickname", 3, 2);
    let generated = append_fields_snapshot(&stored, std::slice::from_ref(&added));

    let Some(SchemaMutationRequest::AppendOnlyFields(added_fields)) =
        schema_mutation_request_for_snapshots(&stored, &generated)
    else {
        panic!("append-only delta should lower into a request");
    };
    let plan: MutationPlan = SchemaMutationRequest::AppendOnlyFields(added_fields).into();
    assert_eq!(
        plan.publication_preflight(),
        super::MutationPublicationPreflight::PublishableNow,
    );

    let with_index = snapshot_with_indexes(&stored, vec![non_unique_name_index()]);
    let Some(SchemaMutationRequest::AddFieldPathIndex { target }) =
        schema_mutation_request_for_snapshots(&stored, &with_index)
    else {
        panic!("single field-path index delta should lower into a request");
    };
    assert_eq!(target.name(), "by_name");

    let multiple = snapshot_with_indexes(
        &stored,
        vec![non_unique_name_index(), expression_name_index()],
    );
    assert_eq!(
        classify_schema_mutation_delta(&stored, &multiple),
        SchemaMutationDelta::Incompatible,
    );
    assert_eq!(
        schema_mutation_request_for_snapshots(&stored, &multiple),
        None
    );
}

#[test]
fn snapshot_delta_classifier_rejects_non_prefix_field_changes() {
    let stored = base_snapshot();
    let mut generated_fields = stored.fields().to_vec();
    generated_fields[1] = nullable_text_field("renamed", 2, 1);
    let generated = PersistedSchemaSnapshot::new(
        stored.version(),
        stored.entity_path().to_string(),
        stored.entity_name().to_string(),
        stored.first_primary_key_field_id(),
        stored.row_layout().clone(),
        generated_fields,
    );

    assert_eq!(
        classify_schema_mutation_delta(&stored, &generated),
        SchemaMutationDelta::Incompatible,
    );
    assert_eq!(
        schema_mutation_request_for_snapshots(&stored, &generated),
        None
    );
}