diff --git a/fuzz/fuzz_seeds/postgres_create_domain_nullability.sql b/fuzz/fuzz_seeds/postgres_create_domain_nullability.sql new file mode 100644 index 0000000000..d4356d41ce --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_create_domain_nullability.sql @@ -0,0 +1,3 @@ +CREATE DOMAIN q AS INT DEFAULT 0 NOT NULL; +CREATE DOMAIN q AS INT CHECK (VALUE > 0) CONSTRAINT n NULL; +CREATE DOMAIN q AS INT CONSTRAINT nn NOT NULL CHECK (VALUE > 0) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f003dc96e9..56a383f3f4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3834,7 +3834,7 @@ pub struct CreateDomain { /// The default value of the domain. pub default: Option, /// The constraints of the domain. - pub constraints: Vec, + pub constraints: Vec, } impl fmt::Display for CreateDomain { @@ -3858,9 +3858,9 @@ impl fmt::Display for CreateDomain { } } -/// A domain constraint, as added by `ALTER DOMAIN ... ADD`. +/// A domain constraint, as in `CREATE DOMAIN` and `ALTER DOMAIN ... ADD`. /// -/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.html) +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createdomain.html) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3870,6 +3870,11 @@ pub enum DomainConstraint { /// Optional constraint name. name: Option, }, + /// `[ CONSTRAINT ] NULL`, accepted by `CREATE DOMAIN` only. + Null { + /// Optional constraint name. + name: Option, + }, /// `[ CONSTRAINT ] CHECK ()` Check(CheckConstraint), } @@ -3880,11 +3885,18 @@ impl fmt::Display for DomainConstraint { DomainConstraint::NotNull { name } => { write!(f, "{}NOT NULL", display_constraint_name(name)) } + DomainConstraint::Null { name } => write!(f, "{}NULL", display_constraint_name(name)), DomainConstraint::Check(check) => write!(f, "{check}"), } } } +impl From for DomainConstraint { + fn from(constraint: CheckConstraint) -> Self { + DomainConstraint::Check(constraint) + } +} + /// The return type of a `CREATE FUNCTION` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d9ba7097bd..9c706e8a80 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7415,13 +7415,21 @@ impl<'a> Parser<'a> { None }; let default = if self.parse_keyword(Keyword::DEFAULT) { - Some(self.parse_expr()?) + Some(self.with_state(ColumnDefinition, |parser| parser.parse_expr())?) } else { None }; let mut constraints = Vec::new(); - while let Some(constraint) = self.parse_optional_table_constraint()? { - constraints.push(constraint); + while self + .peek_one_of_keywords(&[ + Keyword::CONSTRAINT, + Keyword::NOT, + Keyword::NULL, + Keyword::CHECK, + ]) + .is_some() + { + constraints.push(self.parse_domain_constraint(true)?); } Ok(CreateDomain { @@ -10941,8 +10949,12 @@ impl<'a> Parser<'a> { }) } - /// Parses `[ CONSTRAINT ] { NOT NULL | CHECK () }`. - fn parse_domain_constraint(&mut self) -> Result { + /// Parses `[ CONSTRAINT ] { NOT NULL | NULL | CHECK () }`, + /// where `NULL` is accepted only if `allow_null` is set. + fn parse_domain_constraint( + &mut self, + allow_null: bool, + ) -> Result { let name = if self.parse_keyword(Keyword::CONSTRAINT) { Some(self.parse_identifier()?) } else { @@ -10950,10 +10962,14 @@ impl<'a> Parser<'a> { }; if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(DomainConstraint::NotNull { name }) + } else if allow_null && self.parse_keyword(Keyword::NULL) { + Ok(DomainConstraint::Null { name }) } else if self.parse_keyword(Keyword::CHECK) { Ok(DomainConstraint::Check( self.parse_check_constraint_body(name)?, )) + } else if allow_null { + self.expected_ref("NOT NULL, NULL, or CHECK", self.peek_token_ref()) } else { self.expected_ref("NOT NULL or CHECK", self.peek_token_ref()) } @@ -12446,7 +12462,7 @@ impl<'a> Parser<'a> { AlterDomainOperation::DropNotNull } else if self.parse_keyword(Keyword::ADD) { AlterDomainOperation::AddConstraint { - constraint: self.parse_domain_constraint()?, + constraint: self.parse_domain_constraint(false)?, not_valid: self.parse_keywords(&[Keyword::NOT, Keyword::VALID]), } } else if self.parse_keywords(&[Keyword::DROP, Keyword::CONSTRAINT]) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 203dbbde21..d67edd298c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13394,3 +13394,87 @@ fn display_alter_domain_propagates_write_errors() { assert_eq!(write!(writer, "{statement}"), Err(fmt::Error), "{rejected}"); } } + +#[test] +fn parse_create_domain_nullability() { + let positive = || { + DomainConstraint::Check(CheckConstraint { + name: None, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("VALUE"))), + op: BinaryOperator::Gt, + right: Box::new(Expr::value(number("0"))), + }), + no_inherit: false, + enforced: None, + }) + }; + + for (sql, constraints) in [ + ( + "CREATE DOMAIN q2 AS INT NOT NULL", + vec![DomainConstraint::NotNull { name: None }], + ), + ( + "CREATE DOMAIN q2 AS INT NULL", + vec![DomainConstraint::Null { name: None }], + ), + ( + "CREATE DOMAIN q2 AS INT CONSTRAINT nn NOT NULL CHECK (VALUE > 0)", + vec![ + DomainConstraint::NotNull { + name: Some(Ident::new("nn")), + }, + positive(), + ], + ), + ( + "CREATE DOMAIN q2 AS INT CHECK (VALUE > 0) CONSTRAINT n NULL", + vec![ + positive(), + DomainConstraint::Null { + name: Some(Ident::new("n")), + }, + ], + ), + ] { + assert_eq!( + pg().verified_stmt(sql), + Statement::CreateDomain(CreateDomain { + name: ObjectName::from(vec![Ident::new("q2")]), + data_type: DataType::Int(None), + collation: None, + default: None, + constraints, + }), + "{sql}" + ); + } + + pg().verified_stmt("CREATE DOMAIN q2 AS INT DEFAULT 0 NOT NULL"); + + for (sql, expected) in [ + ( + "CREATE DOMAIN q2 AS INT CONSTRAINT nn", + "Expected: NOT NULL, NULL, or CHECK, found: EOF", + ), + ( + "CREATE DOMAIN q2 AS INT DEFAULT", + "Expected: an expression, found: EOF", + ), + ( + "CREATE DOMAIN q2 AS INT PRIMARY KEY", + "Expected: end of statement, found: PRIMARY", + ), + ( + "ALTER DOMAIN q2 ADD NULL", + "Expected: NOT NULL or CHECK, found: NULL", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err().to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +}