lift-migration 0.1.7

Migration runtime and schema builder for lift.
Documentation
use super::blueprint::{ColumnDef, ColumnType, ConstraintDef, ForeignKeyAction, ForeignKeyDef};
use crate::schema::SchemaDialect;

impl SchemaDialect {
    pub(crate) fn quote_ident(self, ident: &str) -> String {
        let quote = match self {
            SchemaDialect::Sqlite | SchemaDialect::Postgres => '"',
            SchemaDialect::MariaDb => '`',
        };
        let escaped = ident.replace(quote, if quote == '"' { "\"\"" } else { "``" });
        format!("{quote}{escaped}{quote}")
    }
}

pub(crate) fn default_index_name(table: &str, column: &str) -> String {
    format!("{table}_{column}_idx")
}

pub(crate) fn infer_referenced_table(column: &str) -> String {
    if let Some(prefix) = column.strip_suffix("_id") {
        if prefix.ends_with('s') {
            prefix.to_owned()
        } else {
            format!("{prefix}s")
        }
    } else if column.ends_with('s') {
        column.to_owned()
    } else {
        format!("{column}s")
    }
}

pub(crate) fn render_column(dialect: SchemaDialect, column: &ColumnDef) -> String {
    let name = dialect.quote_ident(&column.name);

    if column.primary_key && column.auto_increment {
        return match dialect {
            SchemaDialect::Sqlite => format!("{name} integer primary key autoincrement"),
            SchemaDialect::Postgres => match &column.ty {
                ColumnType::Integer => format!("{name} serial primary key"),
                ColumnType::BigInt => format!("{name} bigserial primary key"),
                _ => format!("{name} bigserial primary key"),
            },
            SchemaDialect::MariaDb => match &column.ty {
                ColumnType::Integer => {
                    format!("{name} integer unsigned not null auto_increment primary key")
                }
                ColumnType::BigInt => {
                    format!("{name} bigint unsigned not null auto_increment primary key")
                }
                _ => format!("{name} bigint unsigned not null auto_increment primary key"),
            },
        };
    }

    let ty = match (dialect, &column.ty) {
        (_, ColumnType::Integer) => "integer".to_owned(),
        (_, ColumnType::BigInt) => "bigint".to_owned(),
        (_, ColumnType::Bool) => "boolean".to_owned(),
        (_, ColumnType::Char(len)) => format!("char({len})"),
        (_, ColumnType::Varchar(len)) => format!("varchar({len})"),
        (_, ColumnType::Text) => "text".to_owned(),
        (_, ColumnType::Date) => "date".to_owned(),
        (_, ColumnType::Time) => "time".to_owned(),
        (SchemaDialect::Sqlite, ColumnType::DateTime | ColumnType::Timestamp) => {
            "datetime".to_owned()
        }
        (SchemaDialect::Postgres, ColumnType::DateTime) => "timestamp".to_owned(),
        (SchemaDialect::MariaDb, ColumnType::DateTime) => "datetime".to_owned(),
        (SchemaDialect::Postgres, ColumnType::Timestamp) => "timestamp".to_owned(),
        (SchemaDialect::MariaDb, ColumnType::Timestamp) => "timestamp".to_owned(),
        (SchemaDialect::Sqlite, ColumnType::Decimal(precision, scale)) => {
            format!("numeric({precision}, {scale})")
        }
        (_, ColumnType::Decimal(precision, scale)) => format!("decimal({precision}, {scale})"),
        (SchemaDialect::MariaDb, ColumnType::Float) => "float".to_owned(),
        (_, ColumnType::Float) => "real".to_owned(),
        (SchemaDialect::MariaDb, ColumnType::Double) => "double".to_owned(),
        (_, ColumnType::Double) => "double precision".to_owned(),
        (SchemaDialect::Sqlite, ColumnType::Json) => "text".to_owned(),
        (SchemaDialect::Postgres, ColumnType::Json) => "jsonb".to_owned(),
        (SchemaDialect::MariaDb, ColumnType::Json) => "json".to_owned(),
        (SchemaDialect::Sqlite, ColumnType::Uuid) => "text".to_owned(),
        (SchemaDialect::Postgres, ColumnType::Uuid) => "uuid".to_owned(),
        (SchemaDialect::MariaDb, ColumnType::Uuid) => "char(36)".to_owned(),
        (_, ColumnType::Custom(name)) => name.clone(),
    };

    let mut out = format!("{name} {ty}");
    if !column.nullable {
        out.push_str(" not null");
    }
    if column.unique {
        out.push_str(" unique");
    }
    if let Some(default) = &column.default_raw {
        out.push_str(" default ");
        out.push_str(default);
    }
    out
}

pub(crate) fn render_foreign_key(dialect: SchemaDialect, foreign: &ForeignKeyDef) -> String {
    let mut out = format!(
        "foreign key ({}) references {}({})",
        dialect.quote_ident(&foreign.column),
        dialect.quote_ident(&foreign.references_table),
        dialect.quote_ident(&foreign.references_column),
    );

    if let Some(action) = foreign.on_delete {
        out.push_str(" on delete ");
        out.push_str(match action {
            ForeignKeyAction::Cascade => "cascade",
            ForeignKeyAction::Restrict => "restrict",
            ForeignKeyAction::SetNull => "set null",
            ForeignKeyAction::NoAction => "no action",
        });
    }

    if let Some(action) = foreign.on_update {
        out.push_str(" on update ");
        out.push_str(match action {
            ForeignKeyAction::Cascade => "cascade",
            ForeignKeyAction::Restrict => "restrict",
            ForeignKeyAction::SetNull => "set null",
            ForeignKeyAction::NoAction => "no action",
        });
    }

    out
}

pub(crate) fn render_constraint(dialect: SchemaDialect, constraint: &ConstraintDef) -> String {
    match constraint {
        ConstraintDef::Primary { columns } => {
            let columns = columns
                .iter()
                .map(|column| dialect.quote_ident(column))
                .collect::<Vec<_>>()
                .join(", ");
            format!("primary key ({columns})")
        }
        ConstraintDef::Unique { name, columns } => {
            let columns = columns
                .iter()
                .map(|column| dialect.quote_ident(column))
                .collect::<Vec<_>>()
                .join(", ");
            match name {
                Some(name) => {
                    format!(
                        "constraint {} unique ({columns})",
                        dialect.quote_ident(name)
                    )
                }
                None => format!("unique ({columns})"),
            }
        }
        ConstraintDef::Check { name, expression } => match name {
            Some(name) => format!(
                "constraint {} check ({expression})",
                dialect.quote_ident(name)
            ),
            None => format!("check ({expression})"),
        },
    }
}