db_dsl 0.3.3

A simple DSL for creating database objects.
Documentation
use crate::{Dialect, SqlDown, SqlUp, TableDefinition, TableName};
use log::{error, warn};

#[derive(Clone, Debug)]
pub struct Table {
    pub name: TableName,
    pub options: Option<TableOptions>,
    pub definition: Vec<TableDefinition>,
}

impl Table {
    pub fn new(name: &str, definition: Vec<TableDefinition>) -> Self {
        Self {
            name: TableName::new(name),
            options: Some(TableOptions::default()),
            definition,
        }
    }

    pub fn no_timestamps(&mut self) -> &mut Self {
        let options = self.options.clone().unwrap_or_default();
        self.options = Some(TableOptions {
            timestamps: None,
            ..options
        });
        self
    }

    pub fn with_options(
        name: &str,
        options: TableOptions,
        definition: Vec<TableDefinition>,
    ) -> Self {
        Self {
            name: TableName::new(name),
            options: Some(options),
            definition,
        }
    }
}

impl SqlUp for Table {
    fn sqlite_up(&self) -> Option<String> {
        let mut sql = "CREATE TABLE".to_string();
        let options = self.options.clone().unwrap_or_default();
        if options.if_not_exists {
            sql.push_str(" IF NOT EXISTS");
        }
        sql.push(' ');
        sql.push_str(self.name.to_string().as_str());
        let options = options.sqlite_up();
        if let Some(options) = options {
            sql.push_str(&options);
        } else {
            warn!("Failed to generate table options");
            return None;
        }
        sql.push_str(", ");
        for (i, def) in self.definition.iter().enumerate() {
            if i > 0 {
                sql.push_str(", ");
            }

            let def = if let TableDefinition::Index(index) = def {
                let mut index = index.clone();
                index.table_name = Some(self.name.clone());
                TableDefinition::Index(index)
            } else {
                def.clone()
            };
            let def = def.sqlite_up();
            if let Some(def) = def {
                sql.push_str(&def);
            } else {
                warn!("Failed to generate table definition");
                return None;
            }
        }
        let options = self.options.clone().unwrap_or_default();
        let timestamps = options.timestamps;
        if timestamps.is_some() {
            let timestamps = timestamps.unwrap();
            match timestamps {
                Timestamps::All => {
                    sql.push_str(", created_at DATETIME DEFAULT CURRENT_TIMESTAMP");
                    sql.push_str(", updated_at DATETIME DEFAULT CURRENT_TIMESTAMP");
                }
                Timestamps::CreatedAt => {
                    sql.push_str(", created_at DATETIME DEFAULT CURRENT_TIMESTAMP");
                }
                Timestamps::UpdatedAt => {
                    sql.push_str(", updated_at DATETIME DEFAULT CURRENT_TIMESTAMP");
                }
            }
        }
        sql.push(')');
        Some(sql)
    }
}

impl SqlDown for Table {
    fn sql_down(&self, _dialect: Dialect) -> Option<String> {
        let sql = format!("DROP TABLE {}", self.name);
        Some(sql)
    }
}

#[derive(Clone, Debug)]
pub struct TableOptions {
    pub if_not_exists: bool,
    pub primary_key: PrimaryKey,
    pub timestamps: Option<Timestamps>,
}

#[derive(Clone, Debug, PartialEq, Default)]
pub enum PrimaryKey {
    #[default]
    BigInt,
    Int,
    UUID,
    Clustered(Vec<String>),
    None,
}

impl Default for TableOptions {
    fn default() -> Self {
        TableOptions {
            if_not_exists: true,
            primary_key: PrimaryKey::BigInt,
            timestamps: Some(Timestamps::default()),
        }
    }
}

#[derive(Clone, Debug, Default)]
pub enum Timestamps {
    #[default]
    All,
    CreatedAt,
    UpdatedAt,
}

impl SqlUp for TableOptions {
    fn sqlite_up(&self) -> Option<String> {
        let mut sql = String::new();
        if self.primary_key == PrimaryKey::None {
            return Some(sql);
        }
        sql.push_str(" (");
        match self.primary_key {
            PrimaryKey::Int => sql.push_str("id INTEGER PRIMARY KEY AUTOINCREMENT"),
            PrimaryKey::BigInt => {
                // TODO it'd be nice to support sequences for this instead
                error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY, not BIGINT, using INTEGER instead.");
                sql.push_str("id INTEGER PRIMARY KEY AUTOINCREMENT")
            },
            PrimaryKey::UUID => sql.push_str("id UUID PRIMARY KEY"),
            PrimaryKey::Clustered(ref columns) => {
                sql.push_str("PRIMARY KEY (");
                for (i, column) in columns.iter().enumerate() {
                    if i > 0 {
                        sql.push_str(", ");
                    }
                    sql.push_str(column);
                }
                sql.push(')');
            }
            PrimaryKey::None => {}
        }
        Some(sql)
    }
}