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:
claude@clouddev1
2026-05-27 21:03:14 +00:00
parent 9f15f386d5
commit 338dc8a4cf
7 changed files with 550 additions and 10 deletions
+185
View File
@@ -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)"
);
}