From c9f9ab639638320561fc1a803141b9c1003ae343 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Sun, 30 Aug 2026 17:34:16 -0700 Subject: [PATCH 1/6] Adds support for clickhouse assume and relaxes parenthesis requirement --- src/ast/mod.rs | 7 +++-- src/ast/spans.rs | 1 + src/ast/table_constraints.rs | 53 +++++++++++++++++++++++++++++++++++ src/dialect/clickhouse.rs | 8 ++++++ src/dialect/mod.rs | 14 +++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 26 +++++++++++++++-- tests/sqlparser_clickhouse.rs | 20 +++++++++++++ 8 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4d86dd6a68..84c5f3c60d 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 a34fe66d98..b2d2473da8 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..03e5399f18 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -78,8 +78,22 @@ pub enum TableConstraint { /// [ON UPDATE ] [ON DELETE ] /// }`). ForeignKey(ForeignKeyConstraint), +<<<<<<< Updated upstream /// `[ CONSTRAINT ] CHECK () [NO INHERIT] [[NOT] ENFORCED]` +======= + /// `[ CONSTRAINT ] CHECK () [[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. +>>>>>>> Stashed changes Check(CheckConstraint), + /// ClickHouse [table constraint][1]: `[ CONSTRAINT ] ASSUME ()`. + /// + /// 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 +163,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 +194,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 +248,38 @@ 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: Option, + /// The boolean expression the ASSUME constraint enforces. + 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(&self.name), + self.expr + )?; + Ok(()) + } +} + +impl crate::ast::Spanned for AssumeConstraint { + fn span(&self) -> Span { + self.expr + .span() + .union_opt(&self.name.as_ref().map(|i| i.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 c81d953d1b..2d2f15f068 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 ff83a4da61..83e5288ac8 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 0c50703c3f..ba36755314 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 6af0fb776b..33225b2136 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,10 +10173,21 @@ 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()?); +<<<<<<< Updated upstream self.expect_token(&Token::RParen)?; let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); +======= + if has_paren { + self.expect_token(&Token::RParen)?; + } +>>>>>>> Stashed changes let enforced = if self.parse_keyword(Keyword::ENFORCED) { Some(true) @@ -10196,6 +10207,17 @@ impl<'a> Parser<'a> { .into(), )) } + Token::Word(w) + if w.keyword == Keyword::ASSUME && self.dialect.supports_assume_constraint() => + { + 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, 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 258f443679..1cd9ed4f90 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -233,6 +233,26 @@ 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_partition_by_after_order_by() { // ClickHouse DDL places PARTITION BY after ORDER BY. From 581800d72200812162c0502371f0438f74df61a5 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Sun, 30 Aug 2026 17:37:17 -0700 Subject: [PATCH 2/6] Fix merge errors --- src/ast/table_constraints.rs | 4 ---- src/parser/mod.rs | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 03e5399f18..9b3c1e5d85 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -78,15 +78,11 @@ pub enum TableConstraint { /// [ON UPDATE ] [ON DELETE ] /// }`). ForeignKey(ForeignKeyConstraint), -<<<<<<< Updated upstream /// `[ CONSTRAINT ] CHECK () [NO INHERIT] [[NOT] ENFORCED]` -======= - /// `[ CONSTRAINT ] CHECK () [[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. ->>>>>>> Stashed changes Check(CheckConstraint), /// ClickHouse [table constraint][1]: `[ CONSTRAINT ] ASSUME ()`. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 33225b2136..f2a7a268f9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10180,14 +10180,10 @@ impl<'a> Parser<'a> { true }; let expr = Box::new(self.parse_expr()?); -<<<<<<< Updated upstream - self.expect_token(&Token::RParen)?; - let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); -======= if has_paren { self.expect_token(&Token::RParen)?; } ->>>>>>> Stashed changes + let no_inherit = self.parse_keywords(&[Keyword::NO, Keyword::INHERIT]); let enforced = if self.parse_keyword(Keyword::ENFORCED) { Some(true) From 97dc95c8ca58836701c6e5365c1c32b3d9e6c885 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Mon, 31 Aug 2026 07:37:25 -0700 Subject: [PATCH 3/6] Add red tests for clickhouse assume required constraint keyword and name Co-authored-by: Luca Cappelletti --- tests/sqlparser_clickhouse.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 1cd9ed4f90..e64c077c70 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -253,6 +253,21 @@ fn parse_table_constraints() { ); } +#[test] +fn parse_create_table_rejects_unnamed_assume_constraint() { + 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. From 713db7a33df0db1f900aac2680031b6c7b5f9772 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Mon, 31 Aug 2026 08:03:48 -0700 Subject: [PATCH 4/6] Makes ASSUME parsing require CONSTRAINT and name, fixes comments --- src/ast/table_constraints.rs | 15 +++++++-------- src/parser/mod.rs | 17 ++++++++++++++++- tests/sqlparser_clickhouse.rs | 10 +++++++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 9b3c1e5d85..2fb267adfa 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -84,7 +84,8 @@ pub enum TableConstraint { /// [`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 ()`. + /// 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. /// @@ -247,11 +248,11 @@ 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 `). +/// An `ASSUME` constraint (`CONSTRAINT ASSUME `). pub struct AssumeConstraint { /// Optional constraint name. - pub name: Option, - /// The boolean expression the ASSUME constraint enforces. + pub name: Ident, + /// The boolean expression the ASSUME constraint claims is true. pub expr: Box, } @@ -261,7 +262,7 @@ impl fmt::Display for AssumeConstraint { write!( f, "{}ASSUME ({})", - display_constraint_name(&self.name), + display_constraint_name(&Some(self.name.clone())), self.expr )?; Ok(()) @@ -270,9 +271,7 @@ impl fmt::Display for AssumeConstraint { impl crate::ast::Spanned for AssumeConstraint { fn span(&self) -> Span { - self.expr - .span() - .union_opt(&self.name.as_ref().map(|i| i.span)) + self.expr.span().union_opt(&Some(self.name.span)) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f2a7a268f9..13e4e4e2d5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10206,13 +10206,28 @@ impl<'a> Parser<'a> { 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, expr }.into())) + Ok(Some( + AssumeConstraint { + name: identifier, + expr, + } + .into(), + )) } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index e64c077c70..09928ee17f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -257,8 +257,16 @@ fn parse_table_constraints() { fn parse_create_table_rejects_unnamed_assume_constraint() { clickhouse() .parse_sql_statements( - r#"CREATE TABLE "x" ("a" "int", ASSUME "a" > 0) ENGINE = MergeTree"#, + 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"); } From 50bdef5d37dbe7bf237119e336946c051a1bddfb Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Wed, 21 Oct 2026 13:03:35 -0700 Subject: [PATCH 5/6] Apply batched suggestions from code review Add suggestions from review Co-authored-by: Luca Cappelletti --- src/ast/table_constraints.rs | 11 ++--------- src/parser/mod.rs | 18 ++++++------------ tests/sqlparser_clickhouse.rs | 12 ++++++++++++ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 2fb267adfa..a02c8b536f 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -250,7 +250,7 @@ impl crate::ast::Spanned for CheckConstraint { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] /// An `ASSUME` constraint (`CONSTRAINT ASSUME `). pub struct AssumeConstraint { - /// Optional constraint name. + /// Constraint name. pub name: Ident, /// The boolean expression the ASSUME constraint claims is true. pub expr: Box, @@ -258,14 +258,7 @@ pub struct AssumeConstraint { 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(()) + write!(f, "CONSTRAINT {} ASSUME ({})", self.name, self.expr) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 821a23a3f3..605fb0c1e2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10214,19 +10214,13 @@ impl<'a> Parser<'a> { 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, - }, - ); + self.prev_token(); + return Ok(None); + }; + let expr = match self.parse_expr()? { + Expr::Nested(expr) => expr, + expr => Box::new(expr), }; - 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 { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 8944364636..dbc11af817 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -251,6 +251,13 @@ fn parse_table_constraints() { 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().one_statement_parses_to( + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME ("a" + 1) > 0) ENGINE = MergeTree"#, + r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" ASSUME (("a" + 1) > 0)) ENGINE = MergeTree"#, + ); } #[test] @@ -270,6 +277,11 @@ fn parse_create_table_rejects_unnamed_assume_constraint() { .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_alter_table_rejects_unnamed_assume_constraint() { clickhouse() From 6277c7344ddf82ca3c12199e7bad04da235229a2 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Wed, 21 Oct 2026 13:49:26 -0700 Subject: [PATCH 6/6] Update constraints to support optional outer parentheses --- src/ast/table_constraints.rs | 27 ++++++++++++++++++--------- src/parser/mod.rs | 31 ++++++++++++++++++------------- tests/sqlparser_clickhouse.rs | 26 +++++++++++++++++++------- tests/sqlparser_common.rs | 1 + tests/sqlparser_postgres.rs | 6 ++++++ 5 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index a02c8b536f..6bc7ed93c8 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -82,12 +82,12 @@ pub enum TableConstraint { /// /// 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. + /// 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 to parse; the constraint always displays with parentheses. + /// The parentheses are optional. /// /// [1]: https://clickhouse.com/docs/reference/statements/create/table#constraints Assume(AssumeConstraint), @@ -210,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, @@ -221,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")?; } @@ -254,11 +256,18 @@ pub struct AssumeConstraint { 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, self.expr) + write!(f, "CONSTRAINT {} ASSUME ", self.name)?; + if self.parenthesized { + write!(f, "({})", self.expr) + } else { + write!(f, "{}", self.expr) + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 605fb0c1e2..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, } @@ -10180,16 +10181,18 @@ impl<'a> Parser<'a> { )) } Token::Word(w) if w.keyword == Keyword::CHECK => { - 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()?); - if has_paren { - 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) { @@ -10204,6 +10207,7 @@ impl<'a> Parser<'a> { CheckConstraint { name, expr, + parenthesized, no_inherit, enforced, } @@ -10217,15 +10221,16 @@ impl<'a> Parser<'a> { self.prev_token(); return Ok(None); }; - let expr = match self.parse_expr()? { - Expr::Nested(expr) => expr, - expr => Box::new(expr), + 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(), )) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index dbc11af817..9dff93335f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -235,18 +235,14 @@ 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( + clickhouse().verified_stmt( 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( + clickhouse().verified_stmt( 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"#, @@ -254,8 +250,10 @@ fn parse_table_constraints() { clickhouse().verified_stmt( r#"CREATE TABLE "x" ("a" "int", CONSTRAINT "y" CHECK (("a" + 1) > 0)) ENGINE = MergeTree"#, ); - clickhouse().one_statement_parses_to( + 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"#, ); } @@ -282,6 +280,20 @@ 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() 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, }