icydb-schema 0.300.0

Bounded public schema proposal contract for IcyDB
Documentation
use super::*;
use crate::{
    MAX_SCHEMA_MIGRATION_PLAN_BYTES, decode_schema_migration_plan, encode_schema_migration_plan,
};

fn entity(value: &str) -> EntitySourceKey {
    EntitySourceKey::try_new(value).expect("entity source should admit")
}

fn field(value: &str) -> FieldSourceKey {
    FieldSourceKey::try_new(value).expect("field source should admit")
}

fn transition(entity_name: &str, from_name: &str) -> EntityMigration {
    EntityMigration::try_new(
        entity(entity_name),
        DeclaredEntityVersion::try_new(1).expect("version should admit"),
        Some(entity(from_name)),
        vec![SchemaMigrationRename::Field {
            from: field("old_value"),
            to: field("value"),
        }],
        Vec::new(),
    )
    .expect("transition should admit")
}

#[test]
fn declared_entity_version_is_strictly_positive() {
    assert_eq!(
        DeclaredEntityVersion::try_new(0),
        Err(SchemaContractError::InvalidEntityVersion),
    );
    assert_eq!(
        DeclaredEntityVersion::try_new(1)
            .expect("version one should admit")
            .get(),
        1,
    );
}

#[test]
fn plan_canonicalization_is_declaration_order_independent() {
    let account = transition("Account", "User");
    let article = transition("Article", "Post");
    let first = SchemaMigrationPlan::try_new(vec![account.clone(), article.clone()])
        .expect("plan should admit");
    let reverse =
        SchemaMigrationPlan::try_new(vec![article, account]).expect("reverse plan should admit");

    assert_eq!(first, reverse);
    assert_eq!(first.digest(), reverse.digest());
    let encoded = encode_schema_migration_plan(&first).expect("plan should encode");
    assert_eq!(
        decode_schema_migration_plan(&encoded).expect("plan should decode"),
        first,
    );
}

#[test]
fn every_current_migration_operation_roundtrips_exactly() {
    let transition = EntityMigration::try_new(
        entity("Account"),
        DeclaredEntityVersion::try_new(3).expect("version should admit"),
        Some(entity("User")),
        vec![
            SchemaMigrationRename::Field {
                from: field("old_field"),
                to: field("new_field"),
            },
            SchemaMigrationRename::NamedType {
                from: TypeSourceKey::try_new("OldType").expect("type should admit"),
                to: TypeSourceKey::try_new("NewType").expect("type should admit"),
            },
            SchemaMigrationRename::EnumVariant {
                named_type: TypeSourceKey::try_new("OldEnum").expect("type should admit"),
                from: TypeSourceKey::try_new("OldVariant").expect("variant should admit"),
                to: TypeSourceKey::try_new("NewVariant").expect("variant should admit"),
            },
            SchemaMigrationRename::RecordField {
                named_type: TypeSourceKey::try_new("OldRecord").expect("type should admit"),
                from: field("old_member"),
                to: field("new_member"),
            },
            SchemaMigrationRename::Relation {
                from: RelationSourceKey::try_new("old_relation").expect("relation should admit"),
                to: RelationSourceKey::try_new("new_relation").expect("relation should admit"),
            },
            SchemaMigrationRename::Constraint {
                from: ConstraintSourceKey::try_new("old_constraint")
                    .expect("constraint should admit"),
                to: ConstraintSourceKey::try_new("new_constraint")
                    .expect("constraint should admit"),
            },
            SchemaMigrationRename::Rule {
                named_type: TypeSourceKey::try_new("OldRuleOwner").expect("type should admit"),
                from: RuleSourceKey::try_new("old_rule").expect("rule should admit"),
                to: RuleSourceKey::try_new("new_rule").expect("rule should admit"),
            },
        ],
        vec![
            SchemaMigrationTransform::Fill {
                to: field("filled"),
                literal: ScalarLiteral::Nat(1),
            },
            SchemaMigrationTransform::Copy {
                from: field("copy_source"),
                to: field("copied"),
            },
            SchemaMigrationTransform::CheckedCast {
                from: field("cast_source"),
                to: field("casted"),
                target: ScalarType::Nat64,
            },
            SchemaMigrationTransform::Coalesce {
                from: field("nullable_source"),
                to: field("coalesced"),
                literal: ScalarLiteral::Nat(0),
            },
        ],
    )
    .expect("transition should admit");
    let plan = SchemaMigrationPlan::try_new(vec![transition]).expect("plan should admit");
    let encoded = encode_schema_migration_plan(&plan).expect("plan should encode");

    assert_eq!(
        decode_schema_migration_plan(&encoded).expect("plan should decode"),
        plan,
    );
}

#[test]
fn plan_digest_has_one_fixed_current_vector() {
    let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
        .expect("plan should admit");
    assert_eq!(
        plan.digest().to_bytes(),
        [
            185, 94, 228, 94, 125, 249, 118, 171, 39, 218, 171, 230, 105, 177, 195, 14, 41, 14, 39,
            78, 248, 107, 177, 13, 16, 115, 37, 15, 145, 40, 134, 207,
        ],
    );
}

#[test]
fn dependency_only_companion_roundtrips_exactly() {
    let companion = EntityMigration::try_new(
        entity("Holder"),
        DeclaredEntityVersion::try_new(1).unwrap(),
        None,
        Vec::new(),
        Vec::new(),
    )
    .unwrap();
    let plan =
        SchemaMigrationPlan::try_new(vec![transition("CatalogItem", "Item"), companion]).unwrap();
    let encoded = crate::encode_schema_migration_plan(&plan).unwrap();
    assert_eq!(crate::decode_schema_migration_plan(&encoded).unwrap(), plan);
}

#[test]
fn duplicate_targets_predecessors_and_transform_targets_reject() {
    let account = transition("Account", "User");
    assert_eq!(
        SchemaMigrationPlan::try_new(vec![account.clone(), account]),
        Err(SchemaContractError::DuplicateMigrationTarget),
    );
    assert_eq!(
        SchemaMigrationPlan::try_new(vec![
            transition("Account", "User"),
            transition("Profile", "User"),
        ]),
        Err(SchemaContractError::DuplicateMigrationSource),
    );
    assert_eq!(
        EntityMigration::try_new(
            entity("Account"),
            DeclaredEntityVersion::try_new(1).expect("version should admit"),
            None,
            Vec::new(),
            vec![
                SchemaMigrationTransform::Fill {
                    to: field("value"),
                    literal: ScalarLiteral::Nat(1),
                },
                SchemaMigrationTransform::Copy {
                    from: field("old_value"),
                    to: field("value"),
                },
            ],
        ),
        Err(SchemaContractError::DuplicateMigrationTarget),
    );
}

#[test]
fn wire_plan_restoration_keeps_nested_constructor_admission() {
    let valid = transition("Account", "User");
    let mut duplicate = valid.clone();
    duplicate.renames.push(duplicate.renames[0].clone());
    let mut same_name = valid.clone();
    same_name.from_name = Some(same_name.entity.clone());
    let mut invalid_cast = valid;
    invalid_cast
        .transforms
        .push(SchemaMigrationTransform::CheckedCast {
            from: field("old_value"),
            to: field("value"),
            target: ScalarType::Float64,
        });
    for transition in [duplicate, same_name, invalid_cast] {
        let bytes = crate::wire::encode_one((*b"ICWM\x01", (1_u16, vec![transition]))).unwrap();
        assert!(decode_schema_migration_plan(&bytes).is_err());
    }
}

#[test]
fn unsupported_program_versions_and_oversized_transport_fail_closed() {
    let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
        .expect("plan should admit");
    let mut bytes = encode_schema_migration_plan(&plan).expect("plan should encode");
    bytes[5] = 2;
    assert_eq!(
        decode_schema_migration_plan(&bytes),
        Err(SchemaContractError::UnsupportedMigrationProgramVersion {
            found: 2,
            supported: 1,
        }),
    );
    assert!(matches!(
        decode_schema_migration_plan(&vec![0; MAX_SCHEMA_MIGRATION_PLAN_BYTES + 1]),
        Err(SchemaContractError::EncodedTooLarge { .. }),
    ));
}