diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4d86dd6a6..84c5f3c60 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 a34fe66d9..b2d2473da 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 799843f3a..2fb267adf 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); the constraint always displays with parentheses. Check(CheckConstraint), + /// ClickHouse [table constraint][1]: `CONSTRAINT ASSUME ()`. + /// Unlike the other constraints here, the name is mandatory. + /// + /// The parentheses are optional to parse; the constraint always displays with parentheses. + /// + /// [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"), @@ -227,6 +245,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 { + /// Optional constraint name. + pub name: Ident, + /// The boolean expression the ASSUME constraint claims is true. + pub expr: Box, +} + +impl fmt::Display for AssumeConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::display_constraint_name; + write!( + f, + "{}ASSUME ({})", + display_constraint_name(&Some(self.name.clone())), + self.expr + )?; + Ok(()) + } +} + +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 c81d953d1..2d2f15f06 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 ff83a4da6..83e5288ac 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1218,6 +1218,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 0c50703c3..ba3675531 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -130,6 +130,7 @@ define_keywords!( ASENSITIVE, ASOF, ASSERT, + ASSUME, ASYMMETRIC, ASYNC, AT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6af0fb776..13e4e4e2d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10027,7 +10027,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> { @@ -10173,9 +10173,16 @@ impl<'a> Parser<'a> { )) } Token::Word(w) if w.keyword == Keyword::CHECK => { - self.expect_token(&Token::LParen)?; + let has_paren = if self.dialect.supports_unparenthesized_check_constraint() { + self.consume_token(&Token::LParen) + } else { + self.expect_token(&Token::LParen)?; + true + }; let expr = Box::new(self.parse_expr()?); - self.expect_token(&Token::RParen)?; + if has_paren { + self.expect_token(&Token::RParen)?; + } let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); let enforced = if self.parse_keyword(Keyword::ENFORCED) { @@ -10196,6 +10203,32 @@ impl<'a> Parser<'a> { .into(), )) } + Token::Word(w) + if w.keyword == Keyword::ASSUME && self.dialect.supports_assume_constraint() => + { + let Some(identifier) = name else { + return self.expected( + "CONSTRAINT before ASSUME", + TokenWithSpan { + token: Token::make_keyword("ASSUME"), + span: next_token.span, + }, + ); + }; + let has_paren = self.consume_token(&Token::LParen); + let expr = Box::new(self.parse_expr()?); + if has_paren { + self.expect_token(&Token::RParen)?; + } + + Ok(Some( + AssumeConstraint { + name: identifier, + expr, + } + .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 258f44367..09928ee17 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -233,6 +233,49 @@ fn parse_create_table() { ); } +#[test] +fn parse_table_constraints() { + // The parentheses around the expression are optional to parse, but the + // constraint always displays with parentheses. + clickhouse().one_statement_parses_to( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" CHECK "a" > 0) ENGINE = MergeTree"#, + 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().one_statement_parses_to( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME "a" > 0) ENGINE = MergeTree"#, + 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"#, + ); +} + +#[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_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.