rok-sqlc 0.6.0

Generate SQLx Rust database modules from SQL schema files
Documentation
use super::*;
use crate::schema::{Column, UniqueKey};

fn column<'a>(table: &'a Table, name: &str) -> &'a Column {
    table
        .columns
        .iter()
        .find(|column| column.sql_name == name)
        .unwrap_or_else(|| panic!("no column `{name}`"))
}

/// `TYPE`, with or without `USING`, gives the column its new type; keys
/// name columns, so they follow the change.
#[test]
fn alter_column_type_changes_the_column_and_its_keys() {
    let parsed = parse_files(&[
            (
                "0001.up.sql",
                "CREATE TABLE readings (id INTEGER PRIMARY KEY, code VARCHAR(8) UNIQUE, amount INTEGER NOT NULL, note TEXT);",
            ),
            (
                "0002.up.sql",
                "ALTER TABLE readings ALTER COLUMN id TYPE BIGINT, ALTER COLUMN code SET DATA TYPE UUID USING code::uuid;
                 ALTER TABLE readings ALTER amount TYPE NUMERIC(12, 2) USING amount::numeric;
                 ALTER TABLE readings ALTER COLUMN note TYPE BYTEA USING convert_to(note, 'UTF8');",
            ),
        ]);
    assert!(parsed.failures.is_empty(), "{:?}", parsed.failures);
    let table = &parsed.schema.tables[0];
    let types: Vec<(&str, &str, bool)> = table
        .columns
        .iter()
        .map(|column| {
            (
                column.rust_type.as_str(),
                column.sqlx_type.as_str(),
                column.nullable,
            )
        })
        .collect();
    assert_eq!(
        types,
        [
            ("i64", "i64", false),
            ("Option<uuid::Uuid>", "uuid::Uuid", true),
            ("rust_decimal::Decimal", "rust_decimal::Decimal", false),
            ("Option<Vec<u8>>", "Vec<u8>", true),
        ]
    );
    assert_eq!(table.primary_key, ["id"]);
    assert!(column(table, "id").is_primary_key);
    assert_eq!(table.unique_keys, [UniqueKey::new(vec!["code".to_owned()])]);
}

/// `(Rust type, nullable, has a default, generated)` of each column.
fn shapes(table: &Table) -> Vec<(&str, &str, bool, bool, bool)> {
    table
        .columns
        .iter()
        .map(|column| {
            (
                column.sql_name.as_str(),
                column.rust_type.as_str(),
                column.nullable,
                column.has_default,
                column.is_generated,
            )
        })
        .collect()
}

#[test]
fn alter_column_sets_and_drops_not_null_and_defaults() {
    let table = one_table(
        "CREATE TABLE notes (
                 id BIGINT PRIMARY KEY,
                 title TEXT,
                 body TEXT NOT NULL DEFAULT '',
                 views INTEGER,
                 seq SERIAL NOT NULL
             );
             ALTER TABLE notes ALTER COLUMN title SET NOT NULL, ALTER COLUMN title TYPE VARCHAR(80);
             ALTER TABLE notes ALTER COLUMN body DROP NOT NULL, ALTER COLUMN body DROP DEFAULT;
             ALTER TABLE notes ALTER COLUMN views SET DEFAULT 0;
             ALTER TABLE notes ALTER COLUMN seq DROP DEFAULT;",
    );
    assert_eq!(
        shapes(&table),
        [
            ("id", "i64", false, false, false),
            ("title", "String", false, false, false),
            ("body", "Option<String>", true, false, false),
            ("views", "Option<i32>", true, true, false),
            ("seq", "i32", false, false, false),
        ]
    );
}

#[test]
fn alter_column_add_identity_makes_the_column_generated() {
    let table = one_table(
            "CREATE TABLE notes (id BIGINT NOT NULL, other BIGINT NOT NULL);
             ALTER TABLE notes ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY;
             ALTER TABLE notes ALTER COLUMN other ADD GENERATED BY DEFAULT AS IDENTITY (START WITH 10);
             ALTER TABLE notes ADD PRIMARY KEY (id);",
        );
    assert_eq!(
        shapes(&table),
        [
            ("id", "i64", false, true, true),
            ("other", "i64", false, true, true),
        ]
    );
}

/// A value list restricts text; a type change away from text drops it.
#[test]
fn alter_column_type_keeps_value_lists_only_on_text() {
    let parsed = parse(
        "CREATE TABLE orders (
                 id BIGSERIAL PRIMARY KEY,
                 state TEXT CHECK (state IN ('new', 'done')),
                 grade TEXT CHECK (grade IN ('1', '2'))
             );
             ALTER TABLE orders ALTER COLUMN state TYPE VARCHAR(10);
             ALTER TABLE orders ALTER COLUMN grade TYPE INTEGER USING grade::integer;",
    );
    assert!(parsed.failures.is_empty(), "{:?}", parsed.failures);
    assert_eq!(
        enum_summary(&parsed.schema),
        [(
            "OrderState".to_owned(),
            Some("orders.state"),
            vec!["new", "done"]
        )]
    );
    let table = &parsed.schema.tables[0];
    assert_eq!(column(table, "grade").rust_type, "Option<i32>");
    assert_eq!(column(table, "grade").value_list, None);
}

#[test]
fn rejects_alter_column_changes_it_cannot_apply_with_their_location() {
    let cases = [
        (
            "ALTER TABLE notes ALTER COLUMN body TYPE POINT;",
            "ALTER COLUMN: column `body`: type `POINT` is not supported yet",
        ),
        (
            "ALTER TABLE notes ALTER COLUMN body TYPE BIGSERIAL;",
            "ALTER COLUMN: column `body`: type `BIGSERIAL` exists only in CREATE TABLE and ADD COLUMN",
        ),
        (
            "ALTER TABLE notes ALTER COLUMN id DROP NOT NULL;",
            "ALTER COLUMN: column `id` is in the primary key, so it cannot drop NOT NULL",
        ),
        (
            "ALTER TABLE notes ALTER COLUMN body ADD GENERATED ALWAYS AS IDENTITY;",
            "ALTER COLUMN: column `body` must be NOT NULL before it can become an identity column",
        ),
        (
            "ALTER TABLE notes ALTER COLUMN title SET NOT NULL;",
            "ALTER COLUMN: column `title` does not exist",
        ),
    ];
    for (alter, reason) in cases {
        let sql = format!("-- change notes\n{alter}");
        let parsed = parse_files(&[
            (
                "0001.up.sql",
                "CREATE TABLE notes (id BIGINT PRIMARY KEY, body TEXT);",
            ),
            ("0002.up.sql", &sql),
        ]);
        assert!(parsed.schema.tables.is_empty(), "{alter}");
        assert_eq!(parsed.failures.len(), 1, "{alter}");
        assert_eq!(parsed.failures[0].reason, reason, "{alter}");
        assert_eq!(parsed.failures[0].line, Some(2), "{alter}");
        assert_eq!(
            parsed.failures[0].path.as_deref(),
            Some(Path::new("0002.up.sql")),
            "{alter}"
        );
    }
}