feat: advanced ALTER COLUMN SET/DROP NOT NULL & DEFAULT, SET DATA TYPE (ADR-0035 Am2)
The standard-first ALTER COLUMN constraint gap-fill advanced mode lacked: - ALTER COLUMN <c> SET DATA TYPE <ty> — ISO canonical synonym for the PostgreSQL TYPE shorthand (same AlterColumnType action + executor). - SET NOT NULL / DROP NOT NULL — reuse the ADR-0029 do_add_constraint / do_drop_constraint executors (dry-run + internal-table guards free). - SET DEFAULT <expr> / DROP DEFAULT — SET DEFAULT uses a dedicated raw-SQL executor (do_set_column_default); sql_expr yields no typed Value, so it can't go through do_add_constraint. DROP DEFAULT reuses do_drop_constraint. Grammar: AT_ALTER_COLUMN gains a tail Choice (type / set / drop), reusing SQL_TYPE and the CREATE TABLE DEFAULT_NODES; builder dispatch routes the new column-attribute forms; runtime decomposes to the executors. ADR-0035 Am2 corrected in-place: SET DEFAULT decomposes to do_set_column_default, not do_add_constraint (Value-based) — found during build. Tests (test-first): 6 parse + 7 Tier-3 execution via run_replay. Suite 1962/0/1; clippy clean.
This commit is contained in:
@@ -1279,3 +1279,188 @@ fn e2e_rename_table_refusals() {
|
||||
let tables = table_names(&db, &r);
|
||||
assert!(tables.contains(&"T".to_string()) && tables.contains(&"X".to_string()));
|
||||
}
|
||||
|
||||
// --- ADR-0035 Amendment 2: ALTER COLUMN constraint gap-fill -------------
|
||||
//
|
||||
// Full advanced-mode pipeline (parse → SqlAlterTable → runtime
|
||||
// decomposition → ADR-0029 executors / the raw-default executor →
|
||||
// persist), driven via run_replay. Behaviour-based assertions: the
|
||||
// constraint is exercised by a follow-up insert, the way the 4e/4f tests
|
||||
// exercise CHECK.
|
||||
|
||||
/// `ALTER COLUMN … SET NOT NULL` on a clean column succeeds and is then
|
||||
/// enforced (a NULL insert is refused).
|
||||
#[test]
|
||||
fn e2e_alter_column_set_not_null_enforced() {
|
||||
let (project, db, _d) = open();
|
||||
let r = rt();
|
||||
std::fs::write(
|
||||
project.path().join("a.commands"),
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
insert into T (id, qty) values (1, 5)\n\
|
||||
alter table T alter column qty set not null\n",
|
||||
)
|
||||
.expect("write");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "a.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })),
|
||||
"set not null on a clean column succeeds; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
r.block_on(db.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["id".to_string(), "qty".to_string()]),
|
||||
vec![Value::Number("2".to_string()), Value::Null],
|
||||
Some("insert".to_string()),
|
||||
))
|
||||
.is_err(),
|
||||
"SET NOT NULL is enforced — a NULL is refused"
|
||||
);
|
||||
}
|
||||
|
||||
/// The ADR-0029 §5 dry-run fires through the SQL surface: SET NOT NULL on
|
||||
/// a column that already holds a NULL is refused (the replay aborts).
|
||||
#[test]
|
||||
fn e2e_alter_column_set_not_null_refused_on_existing_null() {
|
||||
assert!(replay_is_refused(
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
insert into T (id) values (1)\n\
|
||||
alter table T alter column qty set not null\n",
|
||||
));
|
||||
}
|
||||
|
||||
/// `DROP NOT NULL` reverses it — a NULL insert is accepted again.
|
||||
#[test]
|
||||
fn e2e_alter_column_drop_not_null_allows_nulls() {
|
||||
let (project, db, _d) = open();
|
||||
let r = rt();
|
||||
std::fs::write(
|
||||
project.path().join("a.commands"),
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
alter table T alter column qty set not null\n\
|
||||
alter table T alter column qty drop not null\n",
|
||||
)
|
||||
.expect("write");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "a.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })),
|
||||
"events: {events:?}"
|
||||
);
|
||||
r.block_on(db.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["id".to_string(), "qty".to_string()]),
|
||||
vec![Value::Number("1".to_string()), Value::Null],
|
||||
Some("insert".to_string()),
|
||||
))
|
||||
.expect("NULL qty accepted after DROP NOT NULL");
|
||||
}
|
||||
|
||||
/// `ALTER COLUMN … SET DEFAULT <expr>` backfills an omitted insert.
|
||||
#[test]
|
||||
fn e2e_alter_column_set_default_applies() {
|
||||
let (project, db, _d) = open();
|
||||
let r = rt();
|
||||
std::fs::write(
|
||||
project.path().join("a.commands"),
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
alter table T alter column qty set default 5\n",
|
||||
)
|
||||
.expect("write");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "a.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 3, .. })),
|
||||
"events: {events:?}"
|
||||
);
|
||||
r.block_on(db.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["id".to_string()]),
|
||||
vec![Value::Number("1".to_string())],
|
||||
Some("insert".to_string()),
|
||||
))
|
||||
.expect("insert omitting qty");
|
||||
let rows = r
|
||||
.block_on(db.query_data("T".to_string(), None, None, None))
|
||||
.expect("query")
|
||||
.rows;
|
||||
assert_eq!(
|
||||
rows[0][1].as_deref(),
|
||||
Some("5"),
|
||||
"SET DEFAULT 5 backfilled the omitted column"
|
||||
);
|
||||
}
|
||||
|
||||
/// `SET DEFAULT` on an auto-generated column is refused (ADR-0029 §6).
|
||||
#[test]
|
||||
fn e2e_alter_column_set_default_refused_on_serial() {
|
||||
// `create table T with pk` → id serial; a default on it is refused.
|
||||
assert!(replay_is_refused(
|
||||
"create table T with pk\n\
|
||||
alter table T alter column id set default 0\n",
|
||||
));
|
||||
}
|
||||
|
||||
/// `DROP DEFAULT` removes it — an omitted insert is then NULL.
|
||||
#[test]
|
||||
fn e2e_alter_column_drop_default_removes_it() {
|
||||
let (project, db, _d) = open();
|
||||
let r = rt();
|
||||
std::fs::write(
|
||||
project.path().join("a.commands"),
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
alter table T alter column qty set default 5\n\
|
||||
alter table T alter column qty drop default\n",
|
||||
)
|
||||
.expect("write");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "a.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })),
|
||||
"events: {events:?}"
|
||||
);
|
||||
r.block_on(db.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["id".to_string()]),
|
||||
vec![Value::Number("1".to_string())],
|
||||
Some("insert".to_string()),
|
||||
))
|
||||
.expect("insert omitting qty");
|
||||
let rows = r
|
||||
.block_on(db.query_data("T".to_string(), None, None, None))
|
||||
.expect("query")
|
||||
.rows;
|
||||
assert_eq!(
|
||||
rows[0][1].as_deref(),
|
||||
None,
|
||||
"DROP DEFAULT — the omitted column is NULL"
|
||||
);
|
||||
}
|
||||
|
||||
/// The ISO `SET DATA TYPE` synonym executes the same conversion as the
|
||||
/// PostgreSQL `TYPE` shorthand (ADR-0035 Amendment 2).
|
||||
#[test]
|
||||
fn e2e_alter_column_set_data_type_converts() {
|
||||
let (project, db, _d) = open();
|
||||
let r = rt();
|
||||
std::fs::write(
|
||||
project.path().join("a.commands"),
|
||||
"create table T with pk id(int)\n\
|
||||
add column T: qty (int)\n\
|
||||
insert into T (id, qty) values (1, 7)\n\
|
||||
alter table T alter column qty set data type text\n",
|
||||
)
|
||||
.expect("write");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "a.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })),
|
||||
"events: {events:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
col_type(&db, &r, "qty"),
|
||||
Some(Type::Text),
|
||||
"SET DATA TYPE converted qty to text (same as the TYPE shorthand)"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user