radixdb-catalog 1.1.0

Generational logical catalog owner for RadixDB
Documentation
use crate::{CatalogError, CatalogResult};

pub const BASELINE_CATALOG_MINOR: u16 = 0;
pub const PROCEDURAL_CATALOG_MINOR: u16 = 1;
pub const EXTENSION_CATALOG_MINOR: u16 = 2;
pub const LATEST_CATALOG_MINOR: u16 = EXTENSION_CATALOG_MINOR;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u16)]
pub enum ObjectKind {
    Namespace = 1,
    Table = 2,
    Column = 3,
    Constraint = 4,
    Index = 5,
    View = 6,
    Principal = 32,
    Role = 33,
    AclEntry = 34,
    Function = 36,
    Procedure = 37,
    Trigger = 38,
    Job = 39,
    Extension = 40,
    ExternalType = 41,
    Operator = 42,
    OperatorClass = 43,
    PlannerSupport = 44,
}

impl ObjectKind {
    pub const ALL: [Self; 18] = [
        Self::Namespace,
        Self::Table,
        Self::Column,
        Self::Constraint,
        Self::Index,
        Self::View,
        Self::Principal,
        Self::Role,
        Self::AclEntry,
        Self::Function,
        Self::Procedure,
        Self::Trigger,
        Self::Job,
        Self::Extension,
        Self::ExternalType,
        Self::Operator,
        Self::OperatorClass,
        Self::PlannerSupport,
    ];

    pub const fn tag(self) -> u16 {
        self as u16
    }

    pub const fn name(self) -> &'static str {
        match self {
            Self::Namespace => "Namespace",
            Self::Table => "Table",
            Self::Column => "Column",
            Self::Constraint => "Constraint",
            Self::Index => "Index",
            Self::View => "View",
            Self::Principal => "Principal",
            Self::Role => "Role",
            Self::AclEntry => "AclEntry",
            Self::Function => "Function",
            Self::Procedure => "Procedure",
            Self::Trigger => "Trigger",
            Self::Job => "Job",
            Self::Extension => "Extension",
            Self::ExternalType => "ExternalType",
            Self::Operator => "Operator",
            Self::OperatorClass => "OperatorClass",
            Self::PlannerSupport => "PlannerSupport",
        }
    }

    pub const fn minimum_catalog_minor(self) -> u16 {
        match self {
            Self::Namespace
            | Self::Table
            | Self::Column
            | Self::Constraint
            | Self::Index
            | Self::View => BASELINE_CATALOG_MINOR,
            Self::Principal
            | Self::Role
            | Self::AclEntry
            | Self::Function
            | Self::Procedure
            | Self::Trigger
            | Self::Job => PROCEDURAL_CATALOG_MINOR,
            Self::Extension
            | Self::ExternalType
            | Self::Operator
            | Self::OperatorClass
            | Self::PlannerSupport => EXTENSION_CATALOG_MINOR,
        }
    }

    pub const fn object_class(self) -> ObjectClass {
        match self {
            Self::Namespace => ObjectClass::Namespace,
            Self::Table | Self::View => ObjectClass::Relation,
            Self::Column => ObjectClass::Column,
            Self::Constraint => ObjectClass::Constraint,
            Self::Index => ObjectClass::Index,
            Self::Principal | Self::Role => ObjectClass::SecuritySubject,
            Self::AclEntry => ObjectClass::AclEntry,
            Self::Function => ObjectClass::Function,
            Self::Procedure => ObjectClass::Procedure,
            Self::Trigger => ObjectClass::Trigger,
            Self::Job => ObjectClass::Job,
            Self::Extension => ObjectClass::Extension,
            Self::ExternalType => ObjectClass::Type,
            Self::Operator => ObjectClass::Operator,
            Self::OperatorClass => ObjectClass::OperatorClass,
            Self::PlannerSupport => ObjectClass::PlannerSupport,
        }
    }

    pub const fn requires_containment_parent(self) -> bool {
        !matches!(
            self,
            Self::Principal | Self::Role | Self::AclEntry | Self::Extension
        )
    }

    pub const fn is_executable(self) -> bool {
        matches!(
            self,
            Self::Function | Self::Procedure | Self::Trigger | Self::Job
        )
    }

    pub fn from_tag_for_minor(tag: u16, catalog_minor: u16) -> CatalogResult<Self> {
        if catalog_minor > LATEST_CATALOG_MINOR {
            return Err(CatalogError::UnsupportedCatalogMinor {
                major: 6,
                minor: catalog_minor,
            });
        }
        DecodedObjectKind::decode(tag)?.admit_for_minor(catalog_minor)
    }
}

impl TryFrom<u16> for ObjectKind {
    type Error = CatalogError;

    fn try_from(tag: u16) -> CatalogResult<Self> {
        Self::from_tag_for_minor(tag, LATEST_CATALOG_MINOR)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u16)]
pub enum ReservedObjectKind {
    MaterializedView = 35,
}

impl ReservedObjectKind {
    pub const ALL: [Self; 1] = [Self::MaterializedView];
    pub const fn tag(self) -> u16 {
        self as u16
    }
    pub const fn name(self) -> &'static str {
        match self {
            Self::MaterializedView => "MaterializedView",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodedObjectKind {
    Supported(ObjectKind),
    Reserved(ReservedObjectKind),
}

impl DecodedObjectKind {
    pub fn decode(tag: u16) -> CatalogResult<Self> {
        Ok(match tag {
            1 => Self::Supported(ObjectKind::Namespace),
            2 => Self::Supported(ObjectKind::Table),
            3 => Self::Supported(ObjectKind::Column),
            4 => Self::Supported(ObjectKind::Constraint),
            5 => Self::Supported(ObjectKind::Index),
            6 => Self::Supported(ObjectKind::View),
            32 => Self::Supported(ObjectKind::Principal),
            33 => Self::Supported(ObjectKind::Role),
            34 => Self::Supported(ObjectKind::AclEntry),
            35 => Self::Reserved(ReservedObjectKind::MaterializedView),
            36 => Self::Supported(ObjectKind::Function),
            37 => Self::Supported(ObjectKind::Procedure),
            38 => Self::Supported(ObjectKind::Trigger),
            39 => Self::Supported(ObjectKind::Job),
            40 => Self::Supported(ObjectKind::Extension),
            41 => Self::Supported(ObjectKind::ExternalType),
            42 => Self::Supported(ObjectKind::Operator),
            43 => Self::Supported(ObjectKind::OperatorClass),
            44 => Self::Supported(ObjectKind::PlannerSupport),
            _ => return Err(CatalogError::UnknownObjectKind { tag }),
        })
    }

    pub fn admit(self) -> CatalogResult<ObjectKind> {
        self.admit_for_minor(LATEST_CATALOG_MINOR)
    }

    pub fn admit_for_minor(self, catalog_minor: u16) -> CatalogResult<ObjectKind> {
        match self {
            Self::Supported(kind) if kind.minimum_catalog_minor() <= catalog_minor => Ok(kind),
            Self::Supported(kind) => Err(CatalogError::ObjectKindRequiresCatalogMinor {
                tag: kind.tag(),
                name: kind.name(),
                required_minor: kind.minimum_catalog_minor(),
                actual_minor: catalog_minor,
            }),
            Self::Reserved(kind) => Err(CatalogError::ReservedObjectKind {
                tag: kind.tag(),
                name: kind.name(),
            }),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ObjectClass {
    Namespace,
    Relation,
    Column,
    Constraint,
    Index,
    SecuritySubject,
    AclEntry,
    Function,
    Procedure,
    Trigger,
    Job,
    Extension,
    Type,
    Operator,
    OperatorClass,
    PlannerSupport,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn supported_tags_roundtrip_exactly_for_latest_minor() {
        for kind in ObjectKind::ALL {
            assert_eq!(ObjectKind::try_from(kind.tag()).unwrap(), kind);
            assert_eq!(
                DecodedObjectKind::decode(kind.tag()).unwrap(),
                DecodedObjectKind::Supported(kind)
            );
        }
    }

    #[test]
    fn baseline_minor_keeps_new_kinds_closed() {
        for kind in [
            ObjectKind::Principal,
            ObjectKind::Role,
            ObjectKind::AclEntry,
            ObjectKind::Function,
            ObjectKind::Procedure,
            ObjectKind::Trigger,
            ObjectKind::Job,
        ] {
            assert!(
                matches!(ObjectKind::from_tag_for_minor(kind.tag(), BASELINE_CATALOG_MINOR), Err(CatalogError::ObjectKindRequiresCatalogMinor { tag, .. }) if tag == kind.tag())
            );
        }
        assert!(matches!(
            ObjectKind::from_tag_for_minor(35, PROCEDURAL_CATALOG_MINOR),
            Err(CatalogError::ReservedObjectKind { tag: 35, .. })
        ));
    }

    #[test]
    fn unknown_and_future_minor_fail_closed() {
        for tag in [0, 7, 31, 45, u16::MAX] {
            assert_eq!(
                DecodedObjectKind::decode(tag),
                Err(CatalogError::UnknownObjectKind { tag })
            );
        }
        assert!(matches!(
            ObjectKind::from_tag_for_minor(1, LATEST_CATALOG_MINOR + 1),
            Err(CatalogError::UnsupportedCatalogMinor { major: 6, minor: 3 })
        ));
    }

    #[test]
    fn object_classes_match_identity_contract() {
        assert_eq!(ObjectKind::Table.object_class(), ObjectClass::Relation);
        assert_eq!(ObjectKind::View.object_class(), ObjectClass::Relation);
        assert_eq!(
            ObjectKind::Principal.object_class(),
            ObjectClass::SecuritySubject
        );
        assert_eq!(
            ObjectKind::Role.object_class(),
            ObjectClass::SecuritySubject
        );
        assert_ne!(
            ObjectKind::Function.object_class(),
            ObjectKind::Procedure.object_class()
        );
    }
}