From c9f9ab639638320561fc1a803141b9c1003ae343 Mon Sep 17 00:00:00 2001 From: Peter Lovett Date: Sun, 30 Aug 2026 17:34:16 -0700 Subject: [PATCH 1/4] 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 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..03e5399f1 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 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..33225b213 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 258f44367..1cd9ed4f9 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/4] 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 03e5399f1..9b3c1e5d8 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 33225b213..f2a7a268f 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/4] 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 1cd9ed4f9..e64c077c7 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/4] 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 9b3c1e5d8..2fb267adf 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 f2a7a268f..13e4e4e2d 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 e64c077c7..09928ee17 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"); }