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}`"))
}
#[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()])]);
}
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),
]
);
}
#[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}"
);
}
}