From 1c129464a7326fa944e8b3e62b9fc4bb6cfdd9ff Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 2 Nov 2026 00:13:46 +0100 Subject: [PATCH 1/2] PostgreSQL: Support ALTER DOMAIN --- fuzz/fuzz_seeds/postgres_alter_domain.sql | 7 + src/ast/ddl.rs | 151 +++++++++++++++++++++ src/ast/mod.rs | 68 ++++++---- src/ast/spans.rs | 2 + src/parser/mod.rs | 127 +++++++++++++---- tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 157 ++++++++++++++++++++++ 7 files changed, 462 insertions(+), 52 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_alter_domain.sql diff --git a/fuzz/fuzz_seeds/postgres_alter_domain.sql b/fuzz/fuzz_seeds/postgres_alter_domain.sql new file mode 100644 index 0000000000..d9d0288845 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_alter_domain.sql @@ -0,0 +1,7 @@ +ALTER DOMAIN s.d SET DEFAULT INTERVAL '1 day'; +ALTER DOMAIN d DROP NOT NULL; +ALTER DOMAIN d ADD CONSTRAINT c CHECK (VALUE > 0) NOT VALID; +ALTER DOMAIN d ADD CONSTRAINT n NOT NULL; +ALTER DOMAIN d DROP CONSTRAINT IF EXISTS c CASCADE; +ALTER DOMAIN d RENAME CONSTRAINT c TO e; +ALTER DOMAIN d OWNER TO CURRENT_USER diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b4e943363b..dfb31cf54f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3745,6 +3745,33 @@ impl fmt::Display for CreateDomain { } } +/// A domain constraint, as added by `ALTER DOMAIN ... ADD`. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DomainConstraint { + /// `[ CONSTRAINT ] NOT NULL` + NotNull { + /// Optional constraint name. + name: Option, + }, + /// `[ CONSTRAINT ] CHECK ()` + Check(CheckConstraint), +} + +impl fmt::Display for DomainConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DomainConstraint::NotNull { name } => { + write!(f, "{}NOT NULL", display_constraint_name(name)) + } + DomainConstraint::Check(check) => write!(f, "{check}"), + } + } +} + /// The return type of a `CREATE FUNCTION` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -4852,6 +4879,130 @@ impl Spanned for AlterCollation { } } +/// `ALTER DOMAIN` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterDomain { + /// Name of the domain being altered. + pub name: ObjectName, + /// The operation to perform on the domain. + pub operation: AlterDomainOperation, +} + +/// Operations supported by `ALTER DOMAIN`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterDomainOperation { + /// `SET DEFAULT ` + SetDefault { + /// New default value. + default: Expr, + }, + /// `DROP DEFAULT` + DropDefault, + /// `SET NOT NULL` + SetNotNull, + /// `DROP NOT NULL` + DropNotNull, + /// `ADD [ NOT VALID ]` + AddConstraint { + /// The constraint to add. + constraint: DomainConstraint, + /// Whether `NOT VALID` was specified. + not_valid: bool, + }, + /// `DROP CONSTRAINT [ IF EXISTS ] [ RESTRICT | CASCADE ]` + DropConstraint { + /// Whether `IF EXISTS` was specified. + if_exists: bool, + /// Name of the constraint to drop. + name: Ident, + /// `RESTRICT` or `CASCADE` behavior. + drop_behavior: Option, + }, + /// `RENAME CONSTRAINT TO ` + RenameConstraint { + /// Existing constraint name. + old_name: Ident, + /// New constraint name. + new_name: Ident, + }, + /// `VALIDATE CONSTRAINT ` + ValidateConstraint { + /// Name of the constraint to validate. + name: Ident, + }, + /// `OWNER TO ` + OwnerTo(Owner), + /// `RENAME TO ` + RenameTo { + /// New domain name. + new_name: Ident, + }, + /// `SET SCHEMA ` + SetSchema { + /// Target schema name. + schema_name: ObjectName, + }, +} + +impl fmt::Display for AlterDomainOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterDomainOperation::SetDefault { default } => write!(f, "SET DEFAULT {default}"), + AlterDomainOperation::DropDefault => write!(f, "DROP DEFAULT"), + AlterDomainOperation::SetNotNull => write!(f, "SET NOT NULL"), + AlterDomainOperation::DropNotNull => write!(f, "DROP NOT NULL"), + AlterDomainOperation::AddConstraint { + constraint, + not_valid, + } => { + write!(f, "ADD {constraint}")?; + if *not_valid { + write!(f, " NOT VALID")?; + } + Ok(()) + } + AlterDomainOperation::DropConstraint { + if_exists, + name, + drop_behavior, + } => { + write!(f, "DROP CONSTRAINT ")?; + if *if_exists { + write!(f, "IF EXISTS ")?; + } + write!(f, "{name}")?; + if let Some(drop_behavior) = drop_behavior { + write!(f, " {drop_behavior}")?; + } + Ok(()) + } + AlterDomainOperation::RenameConstraint { old_name, new_name } => { + write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") + } + AlterDomainOperation::ValidateConstraint { name } => { + write!(f, "VALIDATE CONSTRAINT {name}") + } + AlterDomainOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterDomainOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterDomainOperation::SetSchema { schema_name } => { + write!(f, "SET SCHEMA {schema_name}") + } + } + } +} + +impl fmt::Display for AlterDomain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER DOMAIN {} {}", self.name, self.operation) + } +} + /// `ALTER VIEW` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7dfd13d6b1..454394e32f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,34 +65,34 @@ pub use self::dcl::{ }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, - AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, - AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, - AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, - AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, - AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, - AlterTextSearch, AlterTextSearchOperation, AlterTextSearchOption, AlterTrigger, - AlterTriggerOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, AlterView, AlterViewOperation, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, - ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, - CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, - CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, CreateFunction, - CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, - CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, CreateRuleDoKind, - CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, CreateUserMapping, CreateView, - Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, - DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, - DropRule, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, - GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, - IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, - OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, - Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, - ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, - TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, UserMappingUser, ViewColumnDef, - WithData, + AlterConnectorOwner, AlterDomain, AlterDomainOperation, AlterFunction, AlterFunctionAction, + AlterFunctionKind, AlterFunctionOperation, AlterIndexOperation, AlterOperator, + AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, + AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, + AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, + AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, + AlterTextSearchOption, AlterTrigger, AlterTriggerOperation, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + AlterView, AlterViewOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, + ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, + CreateAggregateArgs, CreateAggregateOption, CreateCollation, CreateCollationDefinition, + CreateConnector, CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, + CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, + CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, + CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, + CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, DomainConstraint, + DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, + DropOperatorSignature, DropPolicy, DropRule, DropTrigger, ForValues, + ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, + OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, SqliteTableOption, + SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, + UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, + UserDefinedTypeStorage, UserMappingUser, ViewColumnDef, WithData, }; pub use self::dml::{ Delete, Insert, InsertOverriding, Merge, MergeAction, MergeClause, MergeClauseKind, @@ -3943,6 +3943,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altercollation.html) AlterCollation(AlterCollation), /// ```sql + /// ALTER DOMAIN + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.html) + AlterDomain(AlterDomain), + /// ```sql /// ALTER OPERATOR /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteroperator.html) @@ -5779,6 +5784,7 @@ impl fmt::Display for Statement { write!(f, "ALTER TYPE {name} {operation}") } Statement::AlterCollation(alter_collation) => write!(f, "{alter_collation}"), + Statement::AlterDomain(alter_domain) => write!(f, "{alter_domain}"), Statement::AlterOperator(alter_operator) => write!(f, "{alter_operator}"), Statement::AlterOperatorFamily(alter_operator_family) => { write!(f, "{alter_operator_family}") @@ -12707,6 +12713,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterDomain) -> Self { + Self::AlterDomain(a) + } +} + impl From for Statement { fn from(a: AlterView) -> Self { Self::AlterView(a) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2b2e78f4ee..10ad33eb87 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -276,6 +276,7 @@ impl Spanned for Values { /// - [Statement::CreateExtension] /// - [Statement::CreateCollation] /// - [Statement::AlterCollation] +/// - [Statement::AlterDomain] /// - [Statement::Fetch] /// - [Statement::Flush] /// - [Statement::Discard] @@ -416,6 +417,7 @@ impl Spanned for Statement { Statement::AlterFunction { .. } => Span::empty(), Statement::AlterType { .. } => Span::empty(), Statement::AlterCollation { .. } => Span::empty(), + Statement::AlterDomain { .. } => Span::empty(), Statement::AlterOperator { .. } => Span::empty(), Statement::AlterOperatorFamily { .. } => Span::empty(), Statement::AlterOperatorClass { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2c16b3655c..f2e2b5301a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10829,28 +10829,7 @@ impl<'a> Parser<'a> { )) } Token::Word(w) if w.keyword == Keyword::CHECK => { - self.expect_token(&Token::LParen)?; - let expr = Box::new(self.parse_expr()?); - self.expect_token(&Token::RParen)?; - let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); - - let enforced = if self.parse_keyword(Keyword::ENFORCED) { - Some(true) - } else if self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { - Some(false) - } else { - None - }; - - Ok(Some( - CheckConstraint { - name, - expr, - no_inherit, - enforced, - } - .into(), - )) + Ok(Some(self.parse_check_constraint_body(name)?.into())) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) @@ -10936,6 +10915,50 @@ impl<'a> Parser<'a> { } } + /// Parses the `() [NO INHERIT] [[NOT] ENFORCED]` that follows `CHECK`. + fn parse_check_constraint_body( + &mut self, + name: Option, + ) -> Result { + self.expect_token(&Token::LParen)?; + let expr = Box::new(self.parse_expr()?); + self.expect_token(&Token::RParen)?; + let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); + + let enforced = if self.parse_keyword(Keyword::ENFORCED) { + Some(true) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { + Some(false) + } else { + None + }; + + Ok(CheckConstraint { + name, + expr, + no_inherit, + enforced, + }) + } + + /// Parses `[ CONSTRAINT ] { NOT NULL | CHECK () }`. + fn parse_domain_constraint(&mut self) -> Result { + let name = if self.parse_keyword(Keyword::CONSTRAINT) { + Some(self.parse_identifier()?) + } else { + None + }; + if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { + Ok(DomainConstraint::NotNull { name }) + } else if self.parse_keyword(Keyword::CHECK) { + Ok(DomainConstraint::Check( + self.parse_check_constraint_body(name)?, + )) + } else { + self.expected_ref("NOT NULL or CHECK", self.peek_token_ref()) + } + } + // The leading `EXCLUDE` keyword is already consumed by the caller. fn parse_exclude_constraint( &mut self, @@ -11891,6 +11914,7 @@ impl<'a> Parser<'a> { Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), Keyword::COLLATION => self.parse_alter_collation().map(Into::into), + Keyword::DOMAIN => self.parse_alter_domain().map(Into::into), Keyword::TABLE => self.parse_alter_table(false), Keyword::ICEBERG => { self.expect_keyword(Keyword::TABLE)?; @@ -11936,7 +11960,7 @@ impl<'a> Parser<'a> { _ => { self.index = index; self.expected_ref( - "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR", + "one of VIEW or TYPE or COLLATION or DOMAIN or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR", self.peek_token_ref(), ) } @@ -12354,6 +12378,63 @@ impl<'a> Parser<'a> { Ok(AlterCollation { name, operation }) } + /// Parse a [Statement::AlterDomain]. + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-alterdomain.html) + pub fn parse_alter_domain(&mut self) -> Result { + let name = self.parse_object_name(false)?; + let operation = if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterDomainOperation::SetDefault { + default: self.parse_expr()?, + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { + AlterDomainOperation::DropDefault + } else if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { + AlterDomainOperation::SetNotNull + } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { + AlterDomainOperation::DropNotNull + } else if self.parse_keyword(Keyword::ADD) { + AlterDomainOperation::AddConstraint { + constraint: self.parse_domain_constraint()?, + not_valid: self.parse_keywords(&[Keyword::NOT, Keyword::VALID]), + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::CONSTRAINT]) { + AlterDomainOperation::DropConstraint { + if_exists: self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]), + name: self.parse_identifier()?, + drop_behavior: self.parse_optional_drop_behavior(), + } + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::CONSTRAINT]) { + let old_name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::TO)?; + AlterDomainOperation::RenameConstraint { + old_name, + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { + AlterDomainOperation::ValidateConstraint { + name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterDomainOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterDomainOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterDomainOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else { + return self.expected_ref( + "SET, DROP, ADD, RENAME, VALIDATE, or OWNER TO after ALTER DOMAIN", + self.peek_token_ref(), + ); + }; + + Ok(AlterDomain { name, operation }) + } + /// Parse a [Statement::AlterOperator] /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-alteroperator.html) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 74693a468b..fee59ec65a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16193,7 +16193,7 @@ fn parse_alter_trigger_spans() { #[test] fn parse_alter_rejects_missing_or_unknown_object_type() { - let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; + let objects = "one of VIEW or TYPE or COLLATION or DOMAIN or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; for (sql, found) in [ ("ALTER", "EOF"), ("ALTER 1", "1"), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 322ce2235b..1be7ef03fe 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12522,3 +12522,160 @@ fn parse_column_generic_options() { ); } } + +#[test] +fn parse_alter_domain() { + let ident = |name: &str| Ident::new(name); + let positive = |name: Option| { + DomainConstraint::Check(CheckConstraint { + name, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(ident("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::value(number("0"))), + }), + no_inherit: false, + enforced: None, + }) + }; + + for (sql, operation) in [ + ( + "ALTER DOMAIN d1 SET DEFAULT 0", + AlterDomainOperation::SetDefault { + default: Expr::value(number("0")), + }, + ), + ( + "ALTER DOMAIN d1 DROP DEFAULT", + AlterDomainOperation::DropDefault, + ), + ( + "ALTER DOMAIN d1 SET NOT NULL", + AlterDomainOperation::SetNotNull, + ), + ( + "ALTER DOMAIN d1 DROP NOT NULL", + AlterDomainOperation::DropNotNull, + ), + ( + "ALTER DOMAIN d1 ADD CONSTRAINT positive CHECK (VALUE > 0) NOT VALID", + AlterDomainOperation::AddConstraint { + constraint: positive(Some(ident("positive"))), + not_valid: true, + }, + ), + ( + "ALTER DOMAIN d1 ADD CONSTRAINT nn NOT NULL", + AlterDomainOperation::AddConstraint { + constraint: DomainConstraint::NotNull { + name: Some(ident("nn")), + }, + not_valid: false, + }, + ), + ( + "ALTER DOMAIN d1 ADD NOT NULL", + AlterDomainOperation::AddConstraint { + constraint: DomainConstraint::NotNull { name: None }, + not_valid: false, + }, + ), + ( + "ALTER DOMAIN d1 ADD CHECK (VALUE > 0)", + AlterDomainOperation::AddConstraint { + constraint: positive(None), + not_valid: false, + }, + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT IF EXISTS pos CASCADE", + AlterDomainOperation::DropConstraint { + if_exists: true, + name: ident("pos"), + drop_behavior: Some(DropBehavior::Cascade), + }, + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT pos", + AlterDomainOperation::DropConstraint { + if_exists: false, + name: ident("pos"), + drop_behavior: None, + }, + ), + ( + "ALTER DOMAIN d1 RENAME CONSTRAINT positive TO pos", + AlterDomainOperation::RenameConstraint { + old_name: ident("positive"), + new_name: ident("pos"), + }, + ), + ( + "ALTER DOMAIN d1 VALIDATE CONSTRAINT positive", + AlterDomainOperation::ValidateConstraint { + name: ident("positive"), + }, + ), + ( + "ALTER DOMAIN d1 OWNER TO dom_owner", + AlterDomainOperation::OwnerTo(Owner::Ident(ident("dom_owner"))), + ), + ( + "ALTER DOMAIN d1 OWNER TO CURRENT_USER", + AlterDomainOperation::OwnerTo(Owner::CurrentUser), + ), + ( + "ALTER DOMAIN d1 RENAME TO span", + AlterDomainOperation::RenameTo { + new_name: ident("span"), + }, + ), + ( + "ALTER DOMAIN d1 SET SCHEMA s2", + AlterDomainOperation::SetSchema { + schema_name: ObjectName::from(vec![ident("s2")]), + }, + ), + ] { + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::AlterDomain(AlterDomain { + name: ObjectName::from(vec![ident("d1")]), + operation, + }), + "{sql}" + ); + } + + pg_and_generic().verified_stmt("ALTER DOMAIN s2.span SET DEFAULT INTERVAL '1 day'"); + + for (sql, expected) in [ + ( + "ALTER DOMAIN d1", + "Expected: SET, DROP, ADD, RENAME, VALIDATE, or OWNER TO after ALTER DOMAIN, found: EOF", + ), + ( + "ALTER DOMAIN d1 SET NULL", + "Expected: SET, DROP, ADD, RENAME, VALIDATE, or OWNER TO after ALTER DOMAIN, found: SET", + ), + ( + "ALTER DOMAIN d1 ADD NOT VALID", + "Expected: NOT NULL or CHECK, found: NOT", + ), + ( + "ALTER DOMAIN d1 ADD PRIMARY KEY (VALUE)", + "Expected: NOT NULL or CHECK, found: PRIMARY", + ), + ( + "ALTER DOMAIN d1 RENAME CONSTRAINT a b", + "Expected: TO, found: b", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err().to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} From ebec3bc942ed56922dba763b4a1a1835f25cbdfe Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 2 Nov 2026 13:11:45 +0100 Subject: [PATCH 2/2] Cover ALTER DOMAIN error and display paths --- tests/sqlparser_postgres.rs | 95 ++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1be7ef03fe..42811eda0c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12648,7 +12648,8 @@ fn parse_alter_domain() { ); } - pg_and_generic().verified_stmt("ALTER DOMAIN s2.span SET DEFAULT INTERVAL '1 day'"); + let stmt = pg_and_generic().verified_stmt("ALTER DOMAIN s2.span SET DEFAULT INTERVAL '1 day'"); + assert_eq!(Span::empty(), stmt.span()); for (sql, expected) in [ ( @@ -12671,6 +12672,53 @@ fn parse_alter_domain() { "ALTER DOMAIN d1 RENAME CONSTRAINT a b", "Expected: TO, found: b", ), + ("ALTER DOMAIN", "Expected: identifier, found: EOF"), + ( + "ALTER DOMAIN d1 SET DEFAULT", + "Expected: an expression, found: EOF", + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DOMAIN d1 RENAME CONSTRAINT", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DOMAIN d1 RENAME CONSTRAINT a TO", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DOMAIN d1 VALIDATE CONSTRAINT", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DOMAIN d1 OWNER TO", + "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF", + ), + ("ALTER DOMAIN d1 RENAME TO", "Expected: identifier, found: EOF"), + ("ALTER DOMAIN d1 SET SCHEMA", "Expected: identifier, found: EOF"), + ( + "ALTER DOMAIN d1 ADD CONSTRAINT", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DOMAIN d1 ADD CHECK VALUE > 0", + "Expected: (, found: VALUE", + ), + ( + "ALTER DOMAIN d1 ADD CHECK ()", + "Expected: an expression, found: )", + ), + ( + "ALTER DOMAIN d1 ADD CHECK (VALUE > 0", + "Expected: ), found: EOF", + ), + ( + "CREATE DOMAIN d1 AS INT CHECK VALUE > 0", + "Expected: (, found: VALUE", + ), ] { assert_eq!( pg().parse_sql_statements(sql).unwrap_err().to_string(), @@ -12679,3 +12727,48 @@ fn parse_alter_domain() { ); } } + +#[test] +fn display_alter_domain_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + for (sql, rejected) in [ + ("ALTER DOMAIN d1 ADD CHECK (VALUE > 0) NOT VALID", "ADD"), + ( + "ALTER DOMAIN d1 ADD CHECK (VALUE > 0) NOT VALID", + "NOT VALID", + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT IF EXISTS c_name CASCADE", + "DROP CONSTRAINT", + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT IF EXISTS c_name CASCADE", + "IF EXISTS", + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT IF EXISTS c_name CASCADE", + "c_name", + ), + ( + "ALTER DOMAIN d1 DROP CONSTRAINT IF EXISTS c_name CASCADE", + "CASCADE", + ), + ] { + let statement = pg().verified_stmt(sql); + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error), "{rejected}"); + } +}