Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions fuzz/fuzz_seeds/postgres_create_domain_nullability.sql
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 15 additions & 3 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3834,7 +3834,7 @@ pub struct CreateDomain {
/// The default value of the domain.
pub default: Option<Expr>,
/// The constraints of the domain.
pub constraints: Vec<TableConstraint>,
pub constraints: Vec<DomainConstraint>,
}

impl fmt::Display for CreateDomain {
Expand All @@ -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))]
Expand All @@ -3870,6 +3870,11 @@ pub enum DomainConstraint {
/// Optional constraint name.
name: Option<Ident>,
},
/// `[ CONSTRAINT <name> ] NULL`, accepted by `CREATE DOMAIN` only.
Null {
/// Optional constraint name.
name: Option<Ident>,
},
/// `[ CONSTRAINT <name> ] CHECK (<expr>)`
Check(CheckConstraint),
}
Expand All @@ -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<CheckConstraint> 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))]
Expand Down
28 changes: 22 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -10941,19 +10949,27 @@ impl<'a> Parser<'a> {
})
}

/// Parses `[ CONSTRAINT <name> ] { NOT NULL | CHECK (<expr>) }`.
fn parse_domain_constraint(&mut self) -> Result<DomainConstraint, ParserError> {
/// Parses `[ CONSTRAINT <name> ] { NOT NULL | NULL | CHECK (<expr>) }`,
/// where `NULL` is accepted only if `allow_null` is set.
fn parse_domain_constraint(
&mut self,
allow_null: bool,
) -> Result<DomainConstraint, ParserError> {
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 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())
}
Expand Down Expand Up @@ -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]) {
Expand Down
84 changes: 84 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}
}
Loading