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
+116
View File
@@ -608,6 +608,17 @@ enum Request {
source: Option<String>,
reply: oneshot::Sender<Result<TableDescription, DbError>>,
},
/// `ALTER TABLE … ALTER COLUMN <col> SET DEFAULT <expr>` — set a
/// column's default to raw SQL text (ADR-0035 Amendment 2). Distinct
/// from `AddConstraint(Default(Value))` because the advanced default
/// is raw `sql_expr` text with no typed `Value`.
SetColumnDefault {
table: String,
column: String,
default_sql: String,
source: Option<String>,
reply: oneshot::Sender<Result<TableDescription, DbError>>,
},
/// `ALTER TABLE … ADD [CONSTRAINT <name>] CHECK (<expr>)` — a
/// table-level CHECK, named or unnamed (ADR-0035 §4g).
AlterAddTableCheck {
@@ -1131,6 +1142,27 @@ impl Database {
recv.await.map_err(|_| DbError::WorkerGone)?
}
/// `ALTER TABLE … ALTER COLUMN <col> SET DEFAULT <expr>` — set the
/// column's default to raw SQL text (ADR-0035 Amendment 2).
pub async fn set_column_default(
&self,
table: String,
column: String,
default_sql: String,
source: Option<String>,
) -> Result<TableDescription, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::SetColumnDefault {
table,
column,
default_sql,
source,
reply,
})
.await?;
recv.await.map_err(|_| DbError::WorkerGone)?
}
/// `ALTER TABLE … ADD [CONSTRAINT <name>] CHECK (<expr>)` — a
/// table-level CHECK (ADR-0035 §4g).
pub async fn alter_add_table_check(
@@ -2366,6 +2398,24 @@ fn handle_request(
kind,
));
}
Request::SetColumnDefault {
table,
column,
default_sql,
source,
reply,
} => {
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
do_set_column_default(
conn,
persistence,
source.as_deref(),
&table,
&column,
&default_sql,
)
});
}
Request::AlterAddTableCheck {
table,
name,
@@ -4034,6 +4084,72 @@ fn do_drop_constraint(
do_describe_table(conn, table)
}
/// `ALTER TABLE … ALTER COLUMN <col> SET DEFAULT <expr>` — set the
/// column's default to raw SQL text (ADR-0035 Amendment 2). Mirrors
/// `do_add_constraint`'s `Default` branch (the §6 `serial`/`shortid`
/// refusal; no dry-run — a default never touches existing rows) but
/// takes raw `sql_expr` text instead of a typed `Value` (advanced
/// `SET DEFAULT` has no AST). DEFAULT is recoverable from the engine
/// catalog, so there is no metadata write — the rebuilt table's DDL
/// carries it (ADR-0029 §7).
fn do_set_column_default(
conn: &Connection,
persistence: Option<&Persistence>,
source: Option<&str>,
table: &str,
column: &str,
default_sql: &str,
) -> Result<TableDescription, DbError> {
let canonical_table = require_canonical_table(conn, table)?;
let table = canonical_table.as_str();
let old_schema = read_schema(conn, table)?;
let col_user_type = {
let col = old_schema
.columns
.iter()
.find(|c| c.name == column)
.ok_or_else(|| DbError::Sqlite {
message: format!("no such column: {table}.{column}"),
kind: SqliteErrorKind::NoSuchColumn,
})?;
col.user_type
};
// ADR-0029 §6 — an auto-generated column fills its own values, so a
// `default` would be a second, ambiguous source of "the value when
// none is given" (mirrors do_add_constraint).
if matches!(col_user_type, Some(Type::Serial | Type::ShortId)) {
return Err(DbError::Unsupported(format!(
"`{table}.{column}` is a {ty} column — it auto-fills its own \
values, so it cannot also carry a `default`.",
ty = col_user_type.expect("matched Some above").keyword(),
)));
}
let mut new_schema = old_schema.clone();
{
let target = new_schema
.columns
.iter_mut()
.find(|c| c.name == column)
.expect("column existence checked above");
target.default_sql = Some(default_sql.to_string());
}
let metadata_updates = |tx: &rusqlite::Transaction<'_>| -> Result<(), DbError> {
let changes = Changes {
schema_dirty: true,
rewritten_tables: vec![table.to_string()],
..Changes::default()
};
finalize_persistence(tx, persistence, source, &changes)?;
Ok(())
};
rebuild_table(conn, table, &old_schema, &new_schema, metadata_updates)?;
do_describe_table(conn, table)
}
/// A row's primary-key cell values paired with its value in
/// the column under test — the unit an ADR-0029 §5 dry-run
/// scans.