diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a025be783f..d30045273f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -142,9 +142,10 @@ mod dml; pub mod helpers; pub mod table_constraints; pub use table_constraints::{ - CheckConstraint, ConstraintUsingIndex, ExcludeConstraint, ExcludeConstraintElement, - ExcludeConstraintOperator, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint, - PrimaryKeyConstraint, TableConstraint, UniqueConstraint, + AssumeConstraint, CheckConstraint, ConstraintUsingIndex, ExcludeConstraint, + ExcludeConstraintElement, ExcludeConstraintOperator, ForeignKeyConstraint, + FullTextOrSpatialConstraint, IndexConstraint, PrimaryKeyConstraint, TableConstraint, + UniqueConstraint, }; mod operator; mod query; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7acbd7d0b4..9fe1f5ccfe 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -656,6 +656,7 @@ impl Spanned for TableConstraint { TableConstraint::PrimaryKey(constraint) => constraint.span(), TableConstraint::ForeignKey(constraint) => constraint.span(), TableConstraint::Check(constraint) => constraint.span(), + TableConstraint::Assume(constraint) => constraint.span(), TableConstraint::Index(constraint) => constraint.span(), TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), TableConstraint::PrimaryKeyUsingIndex(constraint) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 799843f3ab..6bc7ed93c8 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -79,7 +79,18 @@ pub enum TableConstraint { /// }`). ForeignKey(ForeignKeyConstraint), /// `[ CONSTRAINT ] CHECK () [NO INHERIT] [[NOT] ENFORCED]` + /// + /// The parentheses are only optional to parse when + /// [`supports_unparenthesized_check_constraint`](crate::dialect::Dialect::supports_unparenthesized_check_constraint) + /// is true for the dialect (e.g. ClickHouse). Check(CheckConstraint), + /// ClickHouse [table constraint][1]: `CONSTRAINT ASSUME ()`. + /// Unlike the other constraints here, the name is mandatory. + /// + /// The parentheses are optional. + /// + /// [1]: https://clickhouse.com/docs/reference/statements/create/table#constraints + Assume(AssumeConstraint), /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage /// is restricted to MySQL, as no other dialects that support this syntax were found. /// @@ -149,6 +160,12 @@ impl From for TableConstraint { } } +impl From for TableConstraint { + fn from(constraint: AssumeConstraint) -> Self { + TableConstraint::Assume(constraint) + } +} + impl From for TableConstraint { fn from(constraint: IndexConstraint) -> Self { TableConstraint::Index(constraint) @@ -174,6 +191,7 @@ impl fmt::Display for TableConstraint { TableConstraint::PrimaryKey(constraint) => constraint.fmt(f), TableConstraint::ForeignKey(constraint) => constraint.fmt(f), TableConstraint::Check(constraint) => constraint.fmt(f), + TableConstraint::Assume(constraint) => constraint.fmt(f), TableConstraint::Index(constraint) => constraint.fmt(f), TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f), TableConstraint::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"), @@ -192,6 +210,8 @@ pub struct CheckConstraint { pub name: Option, /// The boolean expression the CHECK constraint enforces. pub expr: Box, + /// Whether `expr` is wrapped in parentheses. + pub parenthesized: bool, /// PostgreSQL-specific `NO INHERIT` flag: child tables do not inherit the constraint. /// pub no_inherit: bool, @@ -203,12 +223,12 @@ pub struct CheckConstraint { impl fmt::Display for CheckConstraint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use crate::ast::ddl::display_constraint_name; - write!( - f, - "{}CHECK ({})", - display_constraint_name(&self.name), - self.expr - )?; + write!(f, "{}CHECK ", display_constraint_name(&self.name))?; + if self.parenthesized { + write!(f, "({})", self.expr)?; + } else { + write!(f, "{}", self.expr)?; + } if self.no_inherit { write!(f, " NO INHERIT")?; } @@ -227,6 +247,36 @@ impl crate::ast::Spanned for CheckConstraint { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// An `ASSUME` constraint (`CONSTRAINT ASSUME `). +pub struct AssumeConstraint { + /// Constraint name. + pub name: Ident, + /// The boolean expression the ASSUME constraint claims is true. + pub expr: Box, + /// Whether `expr` is wrapped in parentheses. + pub parenthesized: bool, +} + +impl fmt::Display for AssumeConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CONSTRAINT {} ASSUME ", self.name)?; + if self.parenthesized { + write!(f, "({})", self.expr) + } else { + write!(f, "{}", self.expr) + } + } +} + +impl crate::ast::Spanned for AssumeConstraint { + fn span(&self) -> Span { + self.expr.span().union_opt(&Some(self.name.span)) + } +} + /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () /// REFERENCES () [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 880df16e31..07019da26d 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -59,6 +59,14 @@ impl Dialect for ClickHouseDialect { true } + fn supports_unparenthesized_check_constraint(&self) -> bool { + true + } + + fn supports_assume_constraint(&self) -> bool { + true + } + fn supports_insert_table_function(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index eb5ec1dfa7..8e364891af 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1248,6 +1248,20 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect allows the parentheses around the expression + /// in a table-level `CHECK` constraint to be omitted, e.g. `CONSTRAINT y CHECK a > 0` + /// in addition to `CONSTRAINT y CHECK (a > 0)`. + fn supports_unparenthesized_check_constraint(&self) -> bool { + false + } + + /// Returns true if the dialect supports ClickHouse's `ASSUME` table constraint, + /// e.g. `CONSTRAINT y ASSUME a > 0`. + /// See . + fn supports_assume_constraint(&self) -> bool { + false + } + /// Returns true if the dialect supports the `LOAD DATA` statement fn supports_load_data(&self) -> bool { false diff --git a/src/keywords.rs b/src/keywords.rs index 7b91eecba0..38a9bbf1a9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -131,6 +131,7 @@ define_keywords!( ASENSITIVE, ASOF, ASSERT, + ASSUME, ASYMMETRIC, ASYNC, AT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cc8f71b172..d0d09094b0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9719,6 +9719,7 @@ impl<'a> Parser<'a> { CheckConstraint { name: None, // Column-level check constraints don't have names expr: Box::new(expr), + parenthesized: true, no_inherit, enforced, } @@ -10034,7 +10035,7 @@ impl<'a> Parser<'a> { } } - /// Parse an optional table constraint (e.g. `PRIMARY KEY`, `UNIQUE`, `FOREIGN KEY`, `CHECK`). + /// Parse an optional table constraint (e.g. `PRIMARY KEY`, `UNIQUE`, `FOREIGN KEY`, `CHECK`, `ASSUME`). pub fn parse_optional_table_constraint( &mut self, ) -> Result, ParserError> { @@ -10180,9 +10181,18 @@ 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 (expr, parenthesized) = + if self.dialect.supports_unparenthesized_check_constraint() { + match self.parse_expr()? { + Expr::Nested(expr) => (expr, true), + expr => (Box::new(expr), false), + } + } else { + self.expect_token(&Token::LParen)?; + let expr = Box::new(self.parse_expr()?); + self.expect_token(&Token::RParen)?; + (expr, true) + }; let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); let enforced = if self.parse_keyword(Keyword::ENFORCED) { @@ -10197,12 +10207,34 @@ impl<'a> Parser<'a> { CheckConstraint { name, expr, + parenthesized, no_inherit, enforced, } .into(), )) } + Token::Word(w) + if w.keyword == Keyword::ASSUME && self.dialect.supports_assume_constraint() => + { + let Some(identifier) = name else { + self.prev_token(); + return Ok(None); + }; + let (expr, parenthesized) = match self.parse_expr()? { + Expr::Nested(expr) => (expr, true), + expr => (Box::new(expr), false), + }; + + Ok(Some( + AssumeConstraint { + name: identifier, + expr, + parenthesized, + } + .into(), + )) + } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) && dialect_of!(self is GenericDialect | MySqlDialect) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index e680aa0a0e..9dff93335f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -233,6 +233,73 @@ fn parse_create_table() { ); } +#[test] +fn parse_table_constraints() { + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" CHECK "a" > 0) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" CHECK ("a" > 0)) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME "a" > 0) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME ("a" > 0)) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" CHECK (("a" + 1) > 0)) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME ("a" + 1) > 0) ENGINE = MergeTree"#, + ); + clickhouse().verified_stmt( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME (("a" + 1) > 0)) ENGINE = MergeTree"#, + ); +} + +#[test] +fn parse_create_table_rejects_unnamed_assume_constraint() { + clickhouse() + .parse_sql_statements( + r#"CREATE TABLE "x" ("a" "int", "y" ASSUME "a" > 0) ENGINE = MergeTree"#, + ) + .expect_err("ASSUME constraints require CONSTRAINT"); + clickhouse() + .parse_sql_statements( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT ASSUME "a" > 0) ENGINE = MergeTree"#, + ) + .expect_err("ASSUME constraints require name"); + clickhouse() + .parse_sql_statements(r#"CREATE TABLE "x" ("a" "int", ASSUME "a" > 0) ENGINE = MergeTree"#) + .expect_err("ASSUME constraints require CONSTRAINT and a name"); +} + +#[test] +fn parse_create_table_column_named_assume() { + clickhouse().verified_stmt("CREATE TABLE x (assume Int32) ENGINE = MergeTree"); +} + +#[test] +fn parse_check_constraint_starting_with_paren() { + clickhouse().verified_stmt( + r#"CREATE TABLE x ("a" "int", CONSTRAINT c CHECK (a + 1) > 0) ENGINE = MergeTree"#, + ); +} + +#[test] +fn parse_check_constraint_not_starting_with_paren() { + clickhouse().verified_stmt( + r#"CREATE TABLE x ("a" "int", CONSTRAINT c CHECK ((a + 1) > 0)) ENGINE = MergeTree"#, + ); +} + +#[test] +fn parse_alter_table_rejects_unnamed_assume_constraint() { + clickhouse() + .parse_sql_statements(r#"ALTER TABLE "x" ADD ASSUME "a" > 0"#) + .expect_err("ASSUME constraints require CONSTRAINT and a name"); +} #[test] fn parse_create_table_partition_by_after_order_by() { // ClickHouse DDL places PARTITION BY after ORDER BY. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7dbcc91e5c..7a04b2f842 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4202,6 +4202,7 @@ fn parse_create_table() { option: ColumnOption::Check(CheckConstraint { name: None, expr: Box::new(verified_expr("constrained > 0")), + parenthesized: true, no_inherit: false, enforced: None, }), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1f4377219d..d12138db01 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6575,6 +6575,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: false, enforced: None, } @@ -6596,6 +6597,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: false, enforced: None, } @@ -6617,6 +6619,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: false, enforced: None, } @@ -6638,6 +6641,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: false, enforced: None, } @@ -6659,6 +6663,7 @@ fn parse_create_domain() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: false, enforced: None, } @@ -9869,6 +9874,7 @@ fn parse_alter_table_constraint_check_no_inherit() { op: BinaryOperator::Gt, right: Box::new(Expr::Value(test_utils::number("0").into())), }), + parenthesized: true, no_inherit: true, enforced: None, }