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
7 changes: 4 additions & 3 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 48 additions & 0 deletions src/ast/table_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,18 @@ pub enum TableConstraint {
/// }`).
ForeignKey(ForeignKeyConstraint),
/// `[ CONSTRAINT <name> ] CHECK (<expr>) [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 <name> ASSUME (<expr>)`.
/// 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.
///
Expand Down Expand Up @@ -149,6 +160,12 @@ impl From<CheckConstraint> for TableConstraint {
}
}

impl From<AssumeConstraint> for TableConstraint {
fn from(constraint: AssumeConstraint) -> Self {
TableConstraint::Assume(constraint)
}
}

impl From<IndexConstraint> for TableConstraint {
fn from(constraint: IndexConstraint) -> Self {
TableConstraint::Index(constraint)
Expand All @@ -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"),
Expand Down Expand Up @@ -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 <name> ASSUME <expr>`).
pub struct AssumeConstraint {
/// Optional constraint name.
pub name: Ident,
/// The boolean expression the ASSUME constraint claims is true.
pub expr: Box<Expr>,
}

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 <name> ] FOREIGN KEY (<columns>)
/// REFERENCES <foreign_table> (<referred_columns>) [ MATCH { FULL | PARTIAL | SIMPLE } ]
/// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
14 changes: 14 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://clickhouse.com/docs/reference/statements/create/table#constraints>.
fn supports_assume_constraint(&self) -> bool {
false
}

/// Returns true if the dialect supports the `LOAD DATA` statement
fn supports_load_data(&self) -> bool {
false
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ define_keywords!(
ASENSITIVE,
ASOF,
ASSERT,
ASSUME,
ASYMMETRIC,
ASYNC,
AT,
Expand Down
39 changes: 36 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<TableConstraint>, ParserError> {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 <name> 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)
Expand Down
43 changes: 43 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"#,
);
}

Comment thread
pelovett marked this conversation as resolved.
#[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.
Expand Down