fluidattacks-database-core-domain 0.1.0

Database core functional core: database-agnostic table identifiers and types (no I/O)
Documentation
use alloc::format;
use alloc::string::String;
use core::fmt;

/// A SQL identifier, stored as its raw (unescaped) name. `to_str` returns the
/// raw value; `quoted`/`Display` escape embedded double quotes and delimit it.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Identifier {
    inner: String,
}

impl Identifier {
    #[must_use]
    pub fn new(raw: &str) -> Self {
        Self {
            inner: String::from(raw),
        }
    }

    #[must_use]
    pub fn to_str(&self) -> &str {
        &self.inner
    }

    #[must_use]
    pub fn quoted(&self) -> String {
        format!("\"{}\"", self.inner.replace('"', "\"\""))
    }
}

impl fmt::Display for Identifier {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.quoted())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SchemaId {
    name: Identifier,
}

impl SchemaId {
    #[must_use]
    pub const fn new(name: Identifier) -> Self {
        Self { name }
    }

    #[must_use]
    pub const fn name(&self) -> &Identifier {
        &self.name
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableId {
    name: Identifier,
}

impl TableId {
    #[must_use]
    pub const fn new(name: Identifier) -> Self {
        Self { name }
    }

    #[must_use]
    pub const fn name(&self) -> &Identifier {
        &self.name
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ColumnId {
    name: Identifier,
}

impl ColumnId {
    #[must_use]
    pub const fn new(name: Identifier) -> Self {
        Self { name }
    }

    #[must_use]
    pub const fn name(&self) -> &Identifier {
        &self.name
    }
}

/// Fully qualified table name: `schema.table`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DbTableId {
    schema: SchemaId,
    table: TableId,
}

impl DbTableId {
    #[must_use]
    pub const fn new(schema: SchemaId, table: TableId) -> Self {
        Self { schema, table }
    }

    #[must_use]
    pub const fn schema(&self) -> &SchemaId {
        &self.schema
    }

    #[must_use]
    pub const fn table(&self) -> &TableId {
        &self.table
    }

    #[must_use]
    pub fn qualified(&self) -> String {
        format!(
            "{}.{}",
            self.schema.name().quoted(),
            self.table.name().quoted()
        )
    }
}

#[cfg(test)]
mod tests {
    use alloc::format;

    use super::{ColumnId, DbTableId, Identifier, SchemaId, TableId};

    #[test]
    fn display_renders_the_quoted_name() {
        let id = Identifier::new("we\"ird");
        assert_eq!(format!("{id}"), id.quoted());
    }

    #[test]
    fn column_names_are_case_sensitive() {
        let upper = ColumnId::new(Identifier::new("SUBJECT"));
        let lower = ColumnId::new(Identifier::new("subject"));
        assert_ne!(upper, lower);
        assert_eq!(upper.name().quoted(), "\"SUBJECT\"");
    }

    #[test]
    fn a_loading_mirror_keeps_the_schema_and_suffixes_the_table() {
        let target = DbTableId::new(
            SchemaId::new(Identifier::new("ZOHO_DESK")),
            TableId::new(Identifier::new("TICKETS")),
        );
        let loading = DbTableId::new(
            target.schema().clone(),
            TableId::new(Identifier::new(&format!(
                "{}_LOADING",
                target.table().name().to_str()
            ))),
        );
        assert_eq!(loading.qualified(), "\"ZOHO_DESK\".\"TICKETS_LOADING\"");
    }

    #[test]
    fn identifier_escapes_double_quotes() {
        let id = Identifier::new("we\"ird");
        assert_eq!(id.to_str(), "we\"ird");
        assert_eq!(id.quoted(), "\"we\"\"ird\"");
    }

    #[test]
    fn identifier_keeps_percent_literal() {
        let id = Identifier::new("a%b");
        assert_eq!(id.quoted(), "\"a%b\"");
    }

    #[test]
    fn db_table_id_renders_qualified_name() {
        let table = DbTableId::new(
            SchemaId::new(Identifier::new("observes")),
            TableId::new(Identifier::new("sca_advisories")),
        );
        assert_eq!(table.qualified(), "\"observes\".\"sca_advisories\"");
    }

    #[test]
    fn db_table_id_qualified_escapes_each_part() {
        let table = DbTableId::new(
            SchemaId::new(Identifier::new("we\"ird")),
            TableId::new(Identifier::new("t")),
        );
        assert_eq!(table.qualified(), "\"we\"\"ird\".\"t\"");
    }
}