radixdb-catalog 1.1.0

Generational logical catalog owner for RadixDB
Documentation
use crate::payload::common::{validate_version, CanonicalSql};
use crate::{CatalogDataType, CatalogError, CatalogResult};
use radixdb_core::DataType;

pub const COLUMN_FLAG_AUTO_INCREMENT: u64 = 1 << 0;
const KNOWN_COLUMN_FLAGS: u64 = COLUMN_FLAG_AUTO_INCREMENT;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnPayload {
    ordinal: u32,
    data_type: CatalogDataType,
    nullable: bool,
    auto_increment: bool,
    default_sql: Option<CanonicalSql>,
    generated_sql: Option<CanonicalSql>,
}

impl ColumnPayload {
    pub fn new(
        ordinal: u32,
        data_type: CatalogDataType,
        nullable: bool,
        default_sql: Option<String>,
        generated_sql: Option<String>,
    ) -> CatalogResult<Self> {
        Self::new_with_auto_increment(
            ordinal,
            data_type,
            nullable,
            false,
            default_sql,
            generated_sql,
        )
    }

    pub fn new_with_auto_increment(
        ordinal: u32,
        data_type: CatalogDataType,
        nullable: bool,
        auto_increment: bool,
        default_sql: Option<String>,
        generated_sql: Option<String>,
    ) -> CatalogResult<Self> {
        Self::from_fields(
            super::PAYLOAD_VERSION,
            u64::from(auto_increment) * COLUMN_FLAG_AUTO_INCREMENT,
            ordinal,
            data_type,
            nullable,
            default_sql,
            generated_sql,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub fn from_fields(
        version: u16,
        flags: u64,
        ordinal: u32,
        data_type: CatalogDataType,
        nullable: bool,
        default_sql: Option<String>,
        generated_sql: Option<String>,
    ) -> CatalogResult<Self> {
        validate_version("column", version)?;
        if flags & !KNOWN_COLUMN_FLAGS != 0 {
            return Err(CatalogError::UnknownPayloadFlags {
                kind: "column",
                flags,
            });
        }
        let auto_increment = flags & COLUMN_FLAG_AUTO_INCREMENT != 0;
        let default_sql = default_sql
            .map(|sql| CanonicalSql::new("column.default_sql", sql))
            .transpose()?;
        let generated_sql = generated_sql
            .map(|sql| CanonicalSql::new("column.generated_sql", sql))
            .transpose()?;
        if default_sql.is_some() && generated_sql.is_some() {
            return Err(CatalogError::InvalidColumnPayload {
                detail: "a generated column cannot also have a default",
            });
        }
        if auto_increment && !matches!(data_type.logical_type(), DataType::Integer | DataType::Uuid)
        {
            return Err(CatalogError::InvalidColumnPayload {
                detail: "AUTO_INCREMENT requires INTEGER or UUID",
            });
        }
        if auto_increment && generated_sql.is_some() {
            return Err(CatalogError::InvalidColumnPayload {
                detail: "AUTO_INCREMENT column cannot also be generated",
            });
        }
        Ok(Self {
            ordinal,
            data_type,
            nullable,
            auto_increment,
            default_sql,
            generated_sql,
        })
    }

    pub const fn version(&self) -> u16 {
        super::PAYLOAD_VERSION
    }

    pub const fn flags(&self) -> u64 {
        if self.auto_increment {
            COLUMN_FLAG_AUTO_INCREMENT
        } else {
            0
        }
    }

    pub const fn ordinal(&self) -> u32 {
        self.ordinal
    }

    pub const fn data_type(&self) -> CatalogDataType {
        self.data_type
    }

    pub const fn nullable(&self) -> bool {
        self.nullable
    }

    pub const fn auto_increment(&self) -> bool {
        self.auto_increment
    }

    pub fn default_sql(&self) -> Option<&CanonicalSql> {
        self.default_sql.as_ref()
    }

    pub fn generated_sql(&self) -> Option<&CanonicalSql> {
        self.generated_sql.as_ref()
    }
}

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

    #[test]
    fn auto_increment_is_typed_and_fail_closed() {
        let integer = CatalogDataType::scalar(DataType::Integer).unwrap();
        let auto =
            ColumnPayload::new_with_auto_increment(0, integer, false, true, None, None).unwrap();
        assert!(auto.auto_increment());
        assert_eq!(auto.flags(), COLUMN_FLAG_AUTO_INCREMENT);

        let text = CatalogDataType::scalar(DataType::Text).unwrap();
        assert!(ColumnPayload::new_with_auto_increment(0, text, false, true, None, None).is_err());
        assert!(ColumnPayload::from_fields(
            super::super::PAYLOAD_VERSION,
            COLUMN_FLAG_AUTO_INCREMENT << 1,
            0,
            integer,
            false,
            None,
            None,
        )
        .is_err());
        assert!(ColumnPayload::new_with_auto_increment(
            0,
            integer,
            false,
            true,
            None,
            Some("id + 1".into()),
        )
        .is_err());
    }
}