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:
+179
-6
@@ -1948,16 +1948,55 @@ const AT_RENAME_TAIL: Node = Node::Choice(AT_RENAME_TAIL_CHOICES);
|
||||
static AT_RENAME_NODES: &[Node] = &[Node::Word(Word::keyword("rename")), AT_RENAME_TAIL];
|
||||
const AT_RENAME: Node = Node::Seq(AT_RENAME_NODES);
|
||||
|
||||
// `ALTER COLUMN <col> TYPE <type>` (ADR-0035 §4f). The type slot reuses
|
||||
// SQL_TYPE (the same alias map + `double precision` pair the CREATE
|
||||
// TABLE / ADD COLUMN forms use). The builder keys on the `type` keyword
|
||||
// — unique to this action (ADD COLUMN's type is a `col_type` ident).
|
||||
// `ALTER COLUMN <col> <action>` (ADR-0035 §4f + Amendment 2). The action
|
||||
// tail is a Choice on distinct concrete keywords — `type` / `set` /
|
||||
// `drop` — trap-safe. The type slot reuses SQL_TYPE (the same alias map +
|
||||
// `double precision` pair the CREATE TABLE / ADD COLUMN forms use).
|
||||
//
|
||||
// TYPE <ty> — PostgreSQL shorthand (§4f)
|
||||
// SET DATA TYPE <ty> — ISO canonical synonym (Amendment 2)
|
||||
// SET NOT NULL — documented extension (Amendment 2)
|
||||
// SET DEFAULT <expr> — ISO (Amendment 2), raw sql_expr text
|
||||
// DROP NOT NULL — ISO-ish (Amendment 2)
|
||||
// DROP DEFAULT — ISO (Amendment 2)
|
||||
//
|
||||
// `NOT NULL` reused by both SET and DROP tails (distinct sibling leads in
|
||||
// each). `DEFAULT <expr>` reuses the CREATE TABLE `DEFAULT_NODES` so a
|
||||
// default is one syntax, captured as raw text (sql_expr builds no AST).
|
||||
static AT_AC_TYPE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("type")),
|
||||
super::sql_create_table::SQL_TYPE,
|
||||
];
|
||||
const AT_AC_TYPE: Node = Node::Seq(AT_AC_TYPE_NODES);
|
||||
static AT_AC_NOT_NULL_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("not")), Node::Word(Word::keyword("null"))];
|
||||
const AT_AC_NOT_NULL: Node = Node::Seq(AT_AC_NOT_NULL_NODES);
|
||||
static AT_AC_SET_DATA_TYPE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("data")),
|
||||
Node::Word(Word::keyword("type")),
|
||||
super::sql_create_table::SQL_TYPE,
|
||||
];
|
||||
const AT_AC_SET_DATA_TYPE: Node = Node::Seq(AT_AC_SET_DATA_TYPE_NODES);
|
||||
static AT_AC_SET_TAIL_CHOICES: &[Node] = &[
|
||||
AT_AC_SET_DATA_TYPE,
|
||||
AT_AC_NOT_NULL,
|
||||
Node::Seq(super::sql_create_table::DEFAULT_NODES),
|
||||
];
|
||||
const AT_AC_SET_TAIL: Node = Node::Choice(AT_AC_SET_TAIL_CHOICES);
|
||||
static AT_AC_SET_NODES: &[Node] = &[Node::Word(Word::keyword("set")), AT_AC_SET_TAIL];
|
||||
const AT_AC_SET: Node = Node::Seq(AT_AC_SET_NODES);
|
||||
static AT_AC_DROP_TAIL_CHOICES: &[Node] =
|
||||
&[AT_AC_NOT_NULL, Node::Word(Word::keyword("default"))];
|
||||
const AT_AC_DROP_TAIL: Node = Node::Choice(AT_AC_DROP_TAIL_CHOICES);
|
||||
static AT_AC_DROP_NODES: &[Node] = &[Node::Word(Word::keyword("drop")), AT_AC_DROP_TAIL];
|
||||
const AT_AC_DROP: Node = Node::Seq(AT_AC_DROP_NODES);
|
||||
static AT_ALTER_COLUMN_TAIL_CHOICES: &[Node] = &[AT_AC_TYPE, AT_AC_SET, AT_AC_DROP];
|
||||
const AT_ALTER_COLUMN_TAIL: Node = Node::Choice(AT_ALTER_COLUMN_TAIL_CHOICES);
|
||||
static AT_ALTER_COLUMN_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("alter")),
|
||||
Node::Word(Word::keyword("column")),
|
||||
COLUMN_NAME,
|
||||
Node::Word(Word::keyword("type")),
|
||||
super::sql_create_table::SQL_TYPE,
|
||||
AT_ALTER_COLUMN_TAIL,
|
||||
];
|
||||
const AT_ALTER_COLUMN: Node = Node::Seq(AT_ALTER_COLUMN_NODES);
|
||||
|
||||
@@ -2148,6 +2187,42 @@ fn build_alter_column_type(path: &MatchedPath) -> Result<AlterTableAction, Valid
|
||||
Ok(AlterTableAction::AlterColumnType { column, ty })
|
||||
}
|
||||
|
||||
/// Build an `ALTER COLUMN <c> SET/DROP NOT NULL` / `SET/DROP DEFAULT`
|
||||
/// action (ADR-0035 Amendment 2). `SET DATA TYPE` is handled by the
|
||||
/// `type`-keyword branch, so this only sees the four constraint forms.
|
||||
/// The 2×2 is decided by `set` (vs drop) and `default` (vs not null);
|
||||
/// `SET DEFAULT`'s value is captured as raw `sql_expr` text by span,
|
||||
/// reusing `build_alter_add_column_spec`'s mechanism (no AST — 4a.2).
|
||||
fn build_alter_column_attr(
|
||||
path: &MatchedPath,
|
||||
source: &str,
|
||||
) -> Result<AlterTableAction, ValidationError> {
|
||||
let column = require_ident(path, "column_name")?;
|
||||
let is_set = path.contains_word("set");
|
||||
let is_default = path.contains_word("default");
|
||||
Ok(match (is_set, is_default) {
|
||||
(true, true) => {
|
||||
let mut items = path.items.iter().peekable();
|
||||
let mut default_sql: Option<String> = None;
|
||||
while let Some(item) = items.next() {
|
||||
if matches!(&item.kind, MatchedKind::Word("default"))
|
||||
&& let Some((start, end)) = capture_expr_span(&mut items)
|
||||
{
|
||||
default_sql = Some(source[start..end].trim().to_string());
|
||||
}
|
||||
}
|
||||
let default_sql = default_sql.ok_or_else(|| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "set default needs a value".to_string())],
|
||||
})?;
|
||||
AlterTableAction::SetColumnDefault { column, default_sql }
|
||||
}
|
||||
(false, true) => AlterTableAction::DropColumnDefault { column },
|
||||
(true, false) => AlterTableAction::SetColumnNotNull { column },
|
||||
(false, false) => AlterTableAction::DropColumnNotNull { column },
|
||||
})
|
||||
}
|
||||
|
||||
/// Build `Command::SqlAlterTable` (ADR-0035 §4e/§4f/§4g). Exactly one
|
||||
/// action `Choice` branch matched; the builder recovers which from the
|
||||
/// matched words. Discrimination order matters:
|
||||
@@ -2171,7 +2246,19 @@ fn build_alter_column_type(path: &MatchedPath) -> Result<AlterTableAction, Valid
|
||||
fn build_sql_alter_table(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
let table = require_ident(path, "table_name")?;
|
||||
let action = if path.contains_word("type") {
|
||||
// covers `TYPE <ty>` and the ISO synonym `SET DATA TYPE <ty>`
|
||||
build_alter_column_type(path)?
|
||||
} else if path.contains_word("column")
|
||||
&& !path.contains_word("add")
|
||||
&& !path.contains_word("rename")
|
||||
&& (path.contains_word("set")
|
||||
|| path.contains_word("default")
|
||||
|| (path.contains_word("not") && path.contains_word("null")))
|
||||
{
|
||||
// ADR-0035 Amendment 2: `ALTER COLUMN <c> SET/DROP NOT NULL` and
|
||||
// `SET/DROP DEFAULT`. `add column … not null/default` is excluded
|
||||
// by `!add`; plain `drop column` lacks the attribute markers.
|
||||
build_alter_column_attr(path, source)?
|
||||
} else if path.contains_word("column") {
|
||||
if path.contains_word("add") {
|
||||
AlterTableAction::AddColumn(Box::new(build_alter_add_column_spec(path, source)?))
|
||||
@@ -2934,6 +3021,92 @@ mod sql_alter_table_tests {
|
||||
));
|
||||
}
|
||||
|
||||
// --- ADR-0035 Amendment 2: ALTER COLUMN constraint gap-fill ---
|
||||
|
||||
#[test]
|
||||
fn alter_column_set_data_type_is_a_type_synonym() {
|
||||
// ISO `SET DATA TYPE` is the canonical form; it yields the same
|
||||
// AlterColumnType action as the PostgreSQL `TYPE` shorthand.
|
||||
match alter("alter table T alter column qty set data type int").1 {
|
||||
AlterTableAction::AlterColumnType { column, ty } => {
|
||||
assert_eq!(column, "qty");
|
||||
assert_eq!(ty, crate::dsl::types::Type::Int);
|
||||
}
|
||||
other => panic!("expected AlterColumnType, got {other:?}"),
|
||||
}
|
||||
// alias map still applies through the synonym
|
||||
assert!(matches!(
|
||||
alter("alter table T alter column n set data type double precision").1,
|
||||
AlterTableAction::AlterColumnType { ty: crate::dsl::types::Type::Real, .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_set_not_null_parses() {
|
||||
let (table, action) = alter("alter table T alter column email set not null");
|
||||
assert_eq!(table, "T");
|
||||
match action {
|
||||
AlterTableAction::SetColumnNotNull { column } => assert_eq!(column, "email"),
|
||||
other => panic!("expected SetColumnNotNull, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_drop_not_null_parses() {
|
||||
match alter("alter table T alter column email drop not null").1 {
|
||||
AlterTableAction::DropColumnNotNull { column } => assert_eq!(column, "email"),
|
||||
other => panic!("expected DropColumnNotNull, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_set_default_captures_raw_expr() {
|
||||
match alter("alter table T alter column qty set default 0").1 {
|
||||
AlterTableAction::SetColumnDefault { column, default_sql } => {
|
||||
assert_eq!(column, "qty");
|
||||
assert_eq!(default_sql, "0");
|
||||
}
|
||||
other => panic!("expected SetColumnDefault, got {other:?}"),
|
||||
}
|
||||
// a parenthesised expression default round-trips as raw text
|
||||
match alter("alter table T alter column qty set default (1 + 1)").1 {
|
||||
AlterTableAction::SetColumnDefault { default_sql, .. } => {
|
||||
assert_eq!(default_sql, "(1 + 1)");
|
||||
}
|
||||
other => panic!("expected SetColumnDefault, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_drop_default_parses() {
|
||||
match alter("alter table T alter column qty drop default").1 {
|
||||
AlterTableAction::DropColumnDefault { column } => assert_eq!(column, "qty"),
|
||||
other => panic!("expected DropColumnDefault, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_gap_fill_does_not_steal_the_existing_actions() {
|
||||
// The new set/drop column-attribute forms must not misroute the
|
||||
// top-level add/drop/rename-column or the bare `type` form.
|
||||
assert!(matches!(
|
||||
alter("alter table T add column note text not null").1,
|
||||
AlterTableAction::AddColumn(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
alter("alter table T drop column note").1,
|
||||
AlterTableAction::DropColumn { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
alter("alter table T alter column a type text").1,
|
||||
AlterTableAction::AlterColumnType { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
alter("alter table T drop constraint c_chk").1,
|
||||
AlterTableAction::DropConstraint { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_discriminator_probe_column_named_type() {
|
||||
// PROBE (DA): the `type`-keyword discriminator keys on the literal
|
||||
|
||||
Reference in New Issue
Block a user