icydb-core 0.179.5

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use crate::{
    db::schema::{
        FieldId, PersistedFieldKind, PersistedIndexExpressionOp, PersistedIndexKeyItemSnapshot,
        PersistedIndexKeySnapshot, SchemaFieldDefault, SchemaFieldSlot, SchemaVersion,
        compiled_schema_proposal_for_model,
    },
    model::{
        entity::{EntityModel, PrimaryKeyModel, RelationEdgeModel},
        field::{
            FieldDatabaseDefault, FieldKind, FieldModel, FieldStorageDecode, LeafCodec, ScalarCodec,
        },
        index::{IndexExpression, IndexKeyItem, IndexModel},
    },
    testing::entity_model_from_static,
};

static PROFILE_FIELDS: [FieldModel; 2] = [
    FieldModel::generated("nickname", FieldKind::Text { max_len: None }),
    FieldModel::generated("score", FieldKind::Nat64),
];
static FIELDS: [FieldModel; 4] = [
    FieldModel::generated("id", FieldKind::Ulid),
    FieldModel::generated_with_storage_decode_and_nullability(
        "name",
        FieldKind::Text { max_len: None },
        FieldStorageDecode::ByKind,
        true,
    ),
    FieldModel::generated("rank", FieldKind::Nat64),
    FieldModel::generated_with_storage_decode_nullability_write_policies_and_nested_fields(
        "profile",
        FieldKind::Structured { queryable: true },
        FieldStorageDecode::Value,
        false,
        None,
        None,
        &PROFILE_FIELDS,
    ),
];
static NAME_INDEX: IndexModel =
    IndexModel::generated_with_ordinal(1, "idx_entity__name", "entity::name", &["name"], false);
static PROFILE_NICKNAME_INDEX: IndexModel = IndexModel::generated_with_ordinal(
    2,
    "idx_entity__profile_nickname",
    "entity::profile_nickname",
    &["profile.nickname"],
    false,
);
static EXPRESSION_KEY_ITEMS: [IndexKeyItem; 1] =
    [IndexKeyItem::Expression(IndexExpression::Lower("name"))];
static EXPRESSION_INDEX: IndexModel = IndexModel::generated_with_ordinal_and_key_items(
    3,
    "idx_entity__lower_name",
    "entity::lower_name",
    &["name"],
    &EXPRESSION_KEY_ITEMS,
    false,
);
static INDEXES: [&IndexModel; 3] = [&NAME_INDEX, &PROFILE_NICKNAME_INDEX, &EXPRESSION_INDEX];
static RELATION_LOCAL_FIELDS: [&FieldModel; 1] = [&FIELDS[2]];
static RELATIONS: [RelationEdgeModel; 1] = [RelationEdgeModel::generated(
    "score_owner",
    "schema::proposal::tests::ScoreOwner",
    &RELATION_LOCAL_FIELDS,
)];
static MODEL: EntityModel = entity_model_from_static(
    "schema::proposal::tests::Entity",
    "Entity",
    &FIELDS[0],
    0,
    &FIELDS,
    &INDEXES,
);
static RELATION_MODEL: EntityModel = EntityModel::generated_with_primary_key_model_and_relations(
    "schema::proposal::tests::RelationEntity",
    "RelationEntity",
    1,
    PrimaryKeyModel::scalar(&FIELDS[0]),
    0,
    &FIELDS,
    &INDEXES,
    &RELATIONS,
);
static VERSIONED_MODEL: EntityModel = EntityModel::generated(
    "schema::proposal::tests::VersionedEntity",
    "VersionedEntity",
    4,
    &FIELDS[0],
    0,
    &FIELDS,
    &INDEXES,
);
static COMPOSITE_PRIMARY_KEY_FIELDS: [&FieldModel; 2] = [&FIELDS[0], &FIELDS[2]];
static COMPOSITE_MODEL: EntityModel = EntityModel::generated_with_primary_key_model(
    "schema::proposal::tests::CompositeEntity",
    "CompositeEntity",
    1,
    PrimaryKeyModel::ordered(&COMPOSITE_PRIMARY_KEY_FIELDS),
    0,
    &FIELDS,
    &INDEXES,
);

#[test]
fn compiled_schema_proposal_assigns_initial_field_ids_from_slots() {
    let proposal = compiled_schema_proposal_for_model(&MODEL);

    assert_eq!(proposal.entity_path(), "schema::proposal::tests::Entity");
    assert_eq!(proposal.entity_name(), "Entity");
    assert_eq!(proposal.declared_schema_version(), SchemaVersion::initial());
    assert_eq!(proposal.first_primary_key_field_id(), FieldId::new(1));
    assert_eq!(proposal.primary_key_field_ids(), &[FieldId::new(1)]);
    assert_eq!(proposal.fields().len(), 4);
    assert_eq!(
        proposal.indexes().len(),
        3,
        "field-path and expression indexes should both have accepted-index proposals",
    );

    let ids = proposal
        .fields()
        .iter()
        .map(super::CompiledFieldProposal::id)
        .collect::<Vec<_>>();
    assert_eq!(
        ids,
        vec![
            FieldId::new(1),
            FieldId::new(2),
            FieldId::new(3),
            FieldId::new(4),
        ],
    );
}

#[test]
fn compiled_schema_proposal_carries_declared_schema_version() {
    let proposal = compiled_schema_proposal_for_model(&VERSIONED_MODEL);
    let snapshot = proposal.initial_persisted_schema_snapshot();

    assert_eq!(proposal.declared_schema_version(), SchemaVersion::new(4));
    assert_eq!(
        proposal.initial_row_layout().version(),
        SchemaVersion::new(4)
    );
    assert_eq!(snapshot.version(), SchemaVersion::new(4));
    assert_eq!(snapshot.row_layout().version(), SchemaVersion::new(4));
}

#[test]
fn compiled_schema_proposal_preserves_ordered_primary_key_field_ids() {
    let proposal = compiled_schema_proposal_for_model(&COMPOSITE_MODEL);

    assert_eq!(proposal.first_primary_key_field_id(), FieldId::new(1));
    assert_eq!(
        proposal.primary_key_field_ids(),
        &[FieldId::new(1), FieldId::new(3)],
    );
}

#[test]
fn compiled_schema_proposal_preserves_generated_relation_edges() {
    let proposal = compiled_schema_proposal_for_model(&RELATION_MODEL);

    assert_eq!(proposal.relations().len(), 1);
    assert_eq!(proposal.relations()[0].name(), "score_owner");
    assert_eq!(
        proposal.relations()[0].target_path(),
        "schema::proposal::tests::ScoreOwner"
    );
    assert_eq!(
        proposal.relations()[0].local_field_ids(),
        &[FieldId::new(3)]
    );

    let snapshot = proposal.initial_persisted_schema_snapshot();
    assert_eq!(snapshot.relations().len(), 1);
    assert_eq!(snapshot.relations()[0].name(), "score_owner");
    assert_eq!(
        snapshot.relations()[0].target_path(),
        "schema::proposal::tests::ScoreOwner"
    );
    assert_eq!(
        snapshot.relations()[0].local_field_ids(),
        &[FieldId::new(3)]
    );
}

#[test]
fn compiled_schema_proposal_preserves_generated_storage_contracts() {
    let proposal = compiled_schema_proposal_for_model(&MODEL);
    let name = &proposal.fields()[1];

    assert_eq!(name.name(), "name");
    assert_eq!(name.slot(), SchemaFieldSlot::from_generated_index(1));
    std::assert_matches!(name.kind(), FieldKind::Text { max_len: None });
    assert!(name.nullable());
    assert_eq!(name.database_default(), FieldDatabaseDefault::None);
    assert_eq!(name.storage_decode(), FieldStorageDecode::ByKind);
    assert_eq!(name.leaf_codec(), LeafCodec::Scalar(ScalarCodec::Text));
}

#[test]
fn compiled_schema_proposal_builds_initial_row_layout() {
    let proposal = compiled_schema_proposal_for_model(&MODEL);
    let layout = proposal.initial_row_layout();

    assert_eq!(layout.version(), SchemaVersion::initial());
    assert_eq!(
        layout.field_to_slot(),
        &[
            (FieldId::new(1), SchemaFieldSlot::from_generated_index(0)),
            (FieldId::new(2), SchemaFieldSlot::from_generated_index(1)),
            (FieldId::new(3), SchemaFieldSlot::from_generated_index(2)),
            (FieldId::new(4), SchemaFieldSlot::from_generated_index(3)),
        ]
    );
}

#[test]
fn compiled_schema_proposal_builds_initial_persisted_snapshot() {
    let proposal = compiled_schema_proposal_for_model(&MODEL);
    let snapshot = proposal.initial_persisted_schema_snapshot();

    assert_eq!(snapshot.version(), SchemaVersion::initial());
    assert_eq!(snapshot.entity_path(), "schema::proposal::tests::Entity");
    assert_eq!(snapshot.entity_name(), "Entity");
    assert_eq!(snapshot.first_primary_key_field_id(), FieldId::new(1));
    assert_eq!(snapshot.fields().len(), 4);
    assert_eq!(snapshot.indexes().len(), 3);

    let name = &snapshot.fields()[1];
    assert_eq!(name.id(), FieldId::new(2));
    assert_eq!(name.name(), "name");
    assert_eq!(name.slot(), SchemaFieldSlot::from_generated_index(1));
    std::assert_matches!(name.kind(), PersistedFieldKind::Text { max_len: None });
    assert!(name.nullable());
    assert_eq!(name.default(), &SchemaFieldDefault::None);
    assert_eq!(name.storage_decode(), FieldStorageDecode::ByKind);
    assert_eq!(name.leaf_codec(), LeafCodec::Scalar(ScalarCodec::Text));

    let profile = &snapshot.fields()[3];
    assert_eq!(profile.name(), "profile");
    assert_eq!(profile.nested_leaves().len(), 2);
    assert_eq!(profile.nested_leaves()[0].path(), &["nickname".to_string()],);
    std::assert_matches!(
        profile.nested_leaves()[0].kind(),
        PersistedFieldKind::Text { max_len: None }
    );
    assert_eq!(profile.nested_leaves()[1].path(), &["score".to_string()]);
    std::assert_matches!(profile.nested_leaves()[1].kind(), PersistedFieldKind::Nat64);

    let name_index = &snapshot.indexes()[0];
    assert_eq!(name_index.ordinal(), 1);
    assert_eq!(name_index.name(), "idx_entity__name");
    assert!(!name_index.unique());
    assert_eq!(name_index.key().field_paths().len(), 1);
    assert_eq!(
        name_index.key().field_paths()[0].field_id(),
        FieldId::new(2)
    );
    assert_eq!(
        name_index.key().field_paths()[0].slot(),
        SchemaFieldSlot::from_generated_index(1)
    );
    assert_eq!(
        name_index.key().field_paths()[0].path(),
        &["name".to_string()]
    );

    let nested_index = &snapshot.indexes()[1];
    assert_eq!(nested_index.name(), "idx_entity__profile_nickname");
    assert_eq!(
        nested_index.key().field_paths()[0].field_id(),
        FieldId::new(4)
    );
    assert_eq!(
        nested_index.key().field_paths()[0].path(),
        &["profile".to_string(), "nickname".to_string()]
    );
    std::assert_matches!(
        nested_index.key().field_paths()[0].kind(),
        PersistedFieldKind::Text { max_len: None }
    );

    let expression_index = &snapshot.indexes()[2];
    assert_eq!(expression_index.ordinal(), 3);
    assert_eq!(expression_index.name(), "idx_entity__lower_name");
    let PersistedIndexKeySnapshot::Items(items) = expression_index.key() else {
        panic!("expression index should preserve explicit key items");
    };
    assert_eq!(items.len(), 1);
    let PersistedIndexKeyItemSnapshot::Expression(expression) = &items[0] else {
        panic!("expression index key should persist an accepted expression item");
    };
    assert_eq!(expression.op(), PersistedIndexExpressionOp::Lower);
    assert_eq!(expression.source().field_id(), FieldId::new(2));
    assert_eq!(
        expression.source().slot(),
        SchemaFieldSlot::from_generated_index(1)
    );
    assert_eq!(expression.source().path(), &["name".to_string()]);
    std::assert_matches!(
        expression.input_kind(),
        PersistedFieldKind::Text { max_len: None }
    );
    std::assert_matches!(
        expression.output_kind(),
        PersistedFieldKind::Text { max_len: None }
    );
    assert_eq!(expression.canonical_text(), "expr:v1:LOWER(name)");
}