toasty-sql 0.2.0

SQL serialization layer for Toasty database drivers
Documentation
use toasty_core::{
    driver::Capability,
    schema::db::{
        Column, ColumnId, IndexId, PrimaryKey, RenameHints, Schema, SchemaDiff, Table, TableId,
        Type,
    },
    stmt as core_stmt,
};
use toasty_sql::{
    Serializer,
    migration::MigrationStatement,
    serializer::{Params, Placeholder},
};

struct NoParams;

impl Params for NoParams {
    fn push(&mut self, _: &core_stmt::Value, _: Option<&core_stmt::Type>) -> Placeholder {
        Placeholder(0)
    }
}

fn make_column(table_id: usize, index: usize, name: &str, storage_ty: Type) -> Column {
    Column {
        id: ColumnId {
            table: TableId(table_id),
            index,
        },
        name: name.to_string(),
        ty: core_stmt::Type::String,
        storage_ty,
        nullable: false,
        primary_key: index == 0,
        auto_increment: false,
    }
}

fn make_table(id: usize, name: &str, columns: Vec<Column>) -> Table {
    let pk_columns: Vec<ColumnId> = columns
        .iter()
        .filter(|c| c.primary_key)
        .map(|c| c.id)
        .collect();

    Table {
        id: TableId(id),
        name: name.to_string(),
        columns,
        primary_key: PrimaryKey {
            columns: pk_columns,
            index: IndexId {
                table: TableId(id),
                index: 0,
            },
        },
        indices: vec![],
    }
}

fn serialize_migration(stmts: &[MigrationStatement<'_>], flavor: &str) -> Vec<String> {
    stmts
        .iter()
        .map(|ms| {
            let serializer = match flavor {
                "sqlite" => Serializer::sqlite(ms.schema()),
                "postgresql" => Serializer::postgresql(ms.schema()),
                "mysql" => Serializer::mysql(ms.schema()),
                _ => panic!("unknown flavor: {flavor}"),
            };
            serializer.serialize(ms.statement(), &mut NoParams)
        })
        .collect()
}

fn add_column_sql(new_col: Column, capability: &Capability, flavor: &str) -> String {
    let from = Schema {
        tables: vec![make_table(
            0,
            "users",
            vec![make_column(0, 0, "id", Type::Integer(8))],
        )],
    };
    let to = Schema {
        tables: vec![make_table(
            0,
            "users",
            vec![make_column(0, 0, "id", Type::Integer(8)), new_col],
        )],
    };

    let hints = RenameHints::new();
    let diff = SchemaDiff::from(&from, &to, &hints);
    let stmts = MigrationStatement::from_diff(&diff, capability);
    let sql = serialize_migration(&stmts, flavor);
    assert_eq!(sql.len(), 1);
    sql.into_iter().next().unwrap()
}

#[test]
fn add_column_not_null_sqlite() {
    let col = make_column(0, 1, "name", Type::Text);
    let sql = add_column_sql(col, &Capability::SQLITE, "sqlite");
    assert_eq!(
        sql,
        "ALTER TABLE \"users\" ADD COLUMN \"name\" TEXT NOT NULL;"
    );
}

#[test]
fn add_column_not_null_postgresql() {
    let col = make_column(0, 1, "name", Type::Text);
    let sql = add_column_sql(col, &Capability::POSTGRESQL, "postgresql");
    assert_eq!(
        sql,
        "ALTER TABLE \"users\" ADD COLUMN \"name\" TEXT NOT NULL;"
    );
}

#[test]
fn add_column_nullable_sqlite() {
    let mut col = make_column(0, 1, "email", Type::Text);
    col.nullable = true;
    let sql = add_column_sql(col, &Capability::SQLITE, "sqlite");
    assert!(sql.contains("\"email\" TEXT"), "got: {sql}");
    assert!(!sql.contains("NOT NULL"), "got: {sql}");
}

#[test]
fn add_column_nullable_postgresql() {
    let mut col = make_column(0, 1, "email", Type::Text);
    col.nullable = true;
    let sql = add_column_sql(col, &Capability::POSTGRESQL, "postgresql");
    assert!(sql.contains("\"email\" TEXT"), "got: {sql}");
    assert!(!sql.contains("NOT NULL"), "got: {sql}");
}

#[test]
fn add_column_auto_increment_sqlite() {
    let mut col = make_column(0, 1, "seq", Type::Integer(8));
    col.auto_increment = true;
    let sql = add_column_sql(col, &Capability::SQLITE, "sqlite");
    assert!(sql.contains("AUTOINCREMENT"), "got: {sql}");
}

#[test]
fn add_column_auto_increment_postgresql() {
    let mut col = make_column(0, 1, "seq", Type::Integer(8));
    col.auto_increment = true;
    let sql = add_column_sql(col, &Capability::POSTGRESQL, "postgresql");
    assert!(
        sql.contains("GENERATED BY DEFAULT AS IDENTITY"),
        "got: {sql}"
    );
}

#[test]
fn add_column_auto_increment_mysql() {
    let mut col = make_column(0, 1, "seq", Type::Integer(8));
    col.auto_increment = true;
    let sql = add_column_sql(col, &Capability::MYSQL, "mysql");
    assert!(sql.contains("AUTO_INCREMENT"), "got: {sql}");
}

#[test]
fn add_column_nullable_auto_increment_sqlite() {
    let mut col = make_column(0, 1, "seq", Type::Integer(8));
    col.nullable = true;
    col.auto_increment = true;
    let sql = add_column_sql(col, &Capability::SQLITE, "sqlite");
    assert!(!sql.contains("NOT NULL"), "got: {sql}");
    assert!(sql.contains("AUTOINCREMENT"), "got: {sql}");
}

#[test]
fn add_multiple_columns() {
    let from = Schema {
        tables: vec![make_table(
            0,
            "users",
            vec![make_column(0, 0, "id", Type::Integer(8))],
        )],
    };
    let to = Schema {
        tables: vec![make_table(
            0,
            "users",
            vec![
                make_column(0, 0, "id", Type::Integer(8)),
                make_column(0, 1, "name", Type::Text),
                make_column(0, 2, "email", Type::Text),
            ],
        )],
    };

    let hints = RenameHints::new();
    let diff = SchemaDiff::from(&from, &to, &hints);
    let stmts = MigrationStatement::from_diff(&diff, &Capability::SQLITE);
    let sql = serialize_migration(&stmts, "sqlite");

    assert_eq!(sql.len(), 2);
    assert!(sql.iter().any(|s| s.contains("\"name\"")));
    assert!(sql.iter().any(|s| s.contains("\"email\"")));
    assert!(sql.iter().all(|s| s.contains("ADD COLUMN")));
}