Skip to content

Commit c76e088

Browse files
authored
Merge pull request #4 from fmguerreiro/feat/exclude-constraint
feat: parse EXCLUDE constraints for PostgreSQL
2 parents 723a21c + d819ea0 commit c76e088

6 files changed

Lines changed: 270 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
[package]
1919
name = "pgmold-sqlparser"
2020
description = "Fork of sqlparser with additional PostgreSQL features (PARTITION OF, SECURITY DEFINER/INVOKER, SET params)"
21-
version = "0.60.5"
21+
version = "0.60.6"
2222
authors = ["Filipe Guerreiro <filipe.m.guerreiro@gmail.com>"]
2323
homepage = "https://github.com/fmguerreiro/datafusion-sqlparser-rs"
2424
documentation = "https://docs.rs/pgmold-sqlparser/"

src/ast/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ mod dml;
129129
pub mod helpers;
130130
pub mod table_constraints;
131131
pub use table_constraints::{
132-
CheckConstraint, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint,
133-
PrimaryKeyConstraint, TableConstraint, UniqueConstraint,
132+
CheckConstraint, ExclusionConstraint, ExclusionElement, ForeignKeyConstraint,
133+
FullTextOrSpatialConstraint, IndexConstraint, PrimaryKeyConstraint, TableConstraint,
134+
UniqueConstraint,
134135
};
135136
mod operator;
136137
mod query;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ impl Spanned for TableConstraint {
646646
TableConstraint::Check(constraint) => constraint.span(),
647647
TableConstraint::Index(constraint) => constraint.span(),
648648
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
649+
TableConstraint::Exclusion(constraint) => constraint.span(),
649650
}
650651
}
651652
}

src/ast/table_constraints.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ pub enum TableConstraint {
101101
/// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html
102102
/// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html
103103
FulltextOrSpatial(FullTextOrSpatialConstraint),
104+
/// PostgreSQL `EXCLUDE` constraint:
105+
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
106+
Exclusion(ExclusionConstraint),
104107
}
105108

106109
impl From<UniqueConstraint> for TableConstraint {
@@ -139,6 +142,12 @@ impl From<FullTextOrSpatialConstraint> for TableConstraint {
139142
}
140143
}
141144

145+
impl From<ExclusionConstraint> for TableConstraint {
146+
fn from(constraint: ExclusionConstraint) -> Self {
147+
TableConstraint::Exclusion(constraint)
148+
}
149+
}
150+
142151
impl fmt::Display for TableConstraint {
143152
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144153
match self {
@@ -148,6 +157,7 @@ impl fmt::Display for TableConstraint {
148157
TableConstraint::Check(constraint) => constraint.fmt(f),
149158
TableConstraint::Index(constraint) => constraint.fmt(f),
150159
TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f),
160+
TableConstraint::Exclusion(constraint) => constraint.fmt(f),
151161
}
152162
}
153163
}
@@ -518,3 +528,72 @@ impl crate::ast::Spanned for UniqueConstraint {
518528
)
519529
}
520530
}
531+
532+
/// One element in an `EXCLUDE` constraint's element list:
533+
/// `<expr> WITH <operator>`
534+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
535+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
536+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
537+
pub struct ExclusionElement {
538+
pub expr: Expr,
539+
pub operator: String,
540+
}
541+
542+
impl fmt::Display for ExclusionElement {
543+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
544+
write!(f, "{} WITH {}", self.expr, self.operator)
545+
}
546+
}
547+
548+
/// PostgreSQL `EXCLUDE` constraint:
549+
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
550+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
551+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
552+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
553+
pub struct ExclusionConstraint {
554+
pub name: Option<Ident>,
555+
pub index_method: Option<Ident>,
556+
pub elements: Vec<ExclusionElement>,
557+
pub include: Vec<Ident>,
558+
pub where_clause: Option<Box<Expr>>,
559+
pub characteristics: Option<ConstraintCharacteristics>,
560+
}
561+
562+
impl fmt::Display for ExclusionConstraint {
563+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
564+
use crate::ast::ddl::display_constraint_name;
565+
write!(f, "{}EXCLUDE", display_constraint_name(&self.name))?;
566+
if let Some(method) = &self.index_method {
567+
write!(f, " USING {method}")?;
568+
}
569+
write!(f, " ({})", display_comma_separated(&self.elements))?;
570+
if !self.include.is_empty() {
571+
write!(f, " INCLUDE ({})", display_comma_separated(&self.include))?;
572+
}
573+
if let Some(predicate) = &self.where_clause {
574+
write!(f, " WHERE ({predicate})")?;
575+
}
576+
if let Some(characteristics) = &self.characteristics {
577+
write!(f, " {characteristics}")?;
578+
}
579+
Ok(())
580+
}
581+
}
582+
583+
impl crate::ast::Spanned for ExclusionConstraint {
584+
fn span(&self) -> Span {
585+
fn union_spans<I: Iterator<Item = Span>>(iter: I) -> Span {
586+
Span::union_iter(iter)
587+
}
588+
589+
union_spans(
590+
self.name
591+
.iter()
592+
.map(|i| i.span)
593+
.chain(self.index_method.iter().map(|i| i.span))
594+
.chain(self.include.iter().map(|i| i.span))
595+
.chain(self.where_clause.iter().map(|e| e.span()))
596+
.chain(self.characteristics.iter().map(|c| c.span())),
597+
)
598+
}
599+
}

src/parser/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9171,6 +9171,50 @@ impl<'a> Parser<'a> {
91719171
.into(),
91729172
))
91739173
}
9174+
Token::Word(w) if w.keyword == Keyword::EXCLUDE => {
9175+
let index_method = if self.parse_keyword(Keyword::USING) {
9176+
Some(self.parse_identifier()?)
9177+
} else {
9178+
None
9179+
};
9180+
9181+
self.expect_token(&Token::LParen)?;
9182+
let elements =
9183+
self.parse_comma_separated(|p| p.parse_exclusion_element())?;
9184+
self.expect_token(&Token::RParen)?;
9185+
9186+
let include = if self.parse_keyword(Keyword::INCLUDE) {
9187+
self.expect_token(&Token::LParen)?;
9188+
let cols = self.parse_comma_separated(|p| p.parse_identifier())?;
9189+
self.expect_token(&Token::RParen)?;
9190+
cols
9191+
} else {
9192+
vec![]
9193+
};
9194+
9195+
let where_clause = if self.parse_keyword(Keyword::WHERE) {
9196+
self.expect_token(&Token::LParen)?;
9197+
let predicate = self.parse_expr()?;
9198+
self.expect_token(&Token::RParen)?;
9199+
Some(Box::new(predicate))
9200+
} else {
9201+
None
9202+
};
9203+
9204+
let characteristics = self.parse_constraint_characteristics()?;
9205+
9206+
Ok(Some(
9207+
ExclusionConstraint {
9208+
name,
9209+
index_method,
9210+
elements,
9211+
include,
9212+
where_clause,
9213+
characteristics,
9214+
}
9215+
.into(),
9216+
))
9217+
}
91749218
_ => {
91759219
if name.is_some() {
91769220
self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", next_token)
@@ -9182,6 +9226,14 @@ impl<'a> Parser<'a> {
91829226
}
91839227
}
91849228

9229+
fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, ParserError> {
9230+
let expr = self.parse_expr()?;
9231+
self.expect_keyword_is(Keyword::WITH)?;
9232+
let operator_token = self.next_token();
9233+
let operator = operator_token.token.to_string();
9234+
Ok(ExclusionElement { expr, operator })
9235+
}
9236+
91859237
fn parse_optional_nulls_distinct(&mut self) -> Result<NullsDistinctOption, ParserError> {
91869238
Ok(if self.parse_keyword(Keyword::NULLS) {
91879239
let not = self.parse_keyword(Keyword::NOT);

tests/sqlparser_postgres.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7753,3 +7753,137 @@ CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\
77537753
_ => panic!("Expected CreateTable"),
77547754
}
77557755
}
7756+
7757+
#[test]
7758+
fn parse_exclude_constraint_basic() {
7759+
let sql =
7760+
"CREATE TABLE t (room INT, CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =))";
7761+
match pg().verified_stmt(sql) {
7762+
Statement::CreateTable(create_table) => {
7763+
assert_eq!(1, create_table.constraints.len());
7764+
match &create_table.constraints[0] {
7765+
TableConstraint::Exclusion(c) => {
7766+
assert_eq!(c.name, Some(Ident::new("no_overlap")));
7767+
assert_eq!(c.index_method, Some(Ident::new("gist")));
7768+
assert_eq!(c.elements.len(), 1);
7769+
assert_eq!(c.elements[0].operator, "=");
7770+
assert_eq!(c.include.len(), 0);
7771+
assert!(c.where_clause.is_none());
7772+
}
7773+
other => panic!("Expected Exclusion, got {other:?}"),
7774+
}
7775+
}
7776+
_ => panic!("Expected CreateTable"),
7777+
}
7778+
}
7779+
7780+
#[test]
7781+
fn parse_exclude_constraint_multi_element() {
7782+
let sql =
7783+
"CREATE TABLE t (room INT, during INT, EXCLUDE USING gist (room WITH =, during WITH &&))";
7784+
match pg().verified_stmt(sql) {
7785+
Statement::CreateTable(create_table) => {
7786+
assert_eq!(1, create_table.constraints.len());
7787+
match &create_table.constraints[0] {
7788+
TableConstraint::Exclusion(c) => {
7789+
assert!(c.name.is_none());
7790+
assert_eq!(c.index_method, Some(Ident::new("gist")));
7791+
assert_eq!(c.elements.len(), 2);
7792+
assert_eq!(c.elements[0].operator, "=");
7793+
assert_eq!(c.elements[1].operator, "&&");
7794+
}
7795+
other => panic!("Expected Exclusion, got {other:?}"),
7796+
}
7797+
}
7798+
_ => panic!("Expected CreateTable"),
7799+
}
7800+
}
7801+
7802+
#[test]
7803+
fn parse_exclude_constraint_with_where() {
7804+
let sql =
7805+
"CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) WHERE (col > 0))";
7806+
match pg().verified_stmt(sql) {
7807+
Statement::CreateTable(create_table) => {
7808+
assert_eq!(1, create_table.constraints.len());
7809+
match &create_table.constraints[0] {
7810+
TableConstraint::Exclusion(c) => {
7811+
assert!(c.where_clause.is_some());
7812+
}
7813+
other => panic!("Expected Exclusion, got {other:?}"),
7814+
}
7815+
}
7816+
_ => panic!("Expected CreateTable"),
7817+
}
7818+
}
7819+
7820+
#[test]
7821+
fn parse_exclude_constraint_with_include() {
7822+
let sql =
7823+
"CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) INCLUDE (col))";
7824+
match pg().verified_stmt(sql) {
7825+
Statement::CreateTable(create_table) => {
7826+
assert_eq!(1, create_table.constraints.len());
7827+
match &create_table.constraints[0] {
7828+
TableConstraint::Exclusion(c) => {
7829+
assert_eq!(c.include, vec![Ident::new("col")]);
7830+
}
7831+
other => panic!("Expected Exclusion, got {other:?}"),
7832+
}
7833+
}
7834+
_ => panic!("Expected CreateTable"),
7835+
}
7836+
}
7837+
7838+
#[test]
7839+
fn parse_exclude_constraint_no_using() {
7840+
let sql = "CREATE TABLE t (col INT, EXCLUDE (col WITH =))";
7841+
match pg().verified_stmt(sql) {
7842+
Statement::CreateTable(create_table) => {
7843+
assert_eq!(1, create_table.constraints.len());
7844+
match &create_table.constraints[0] {
7845+
TableConstraint::Exclusion(c) => {
7846+
assert!(c.index_method.is_none());
7847+
}
7848+
other => panic!("Expected Exclusion, got {other:?}"),
7849+
}
7850+
}
7851+
_ => panic!("Expected CreateTable"),
7852+
}
7853+
}
7854+
7855+
#[test]
7856+
fn parse_exclude_constraint_deferrable() {
7857+
let sql =
7858+
"CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) DEFERRABLE INITIALLY DEFERRED)";
7859+
match pg().verified_stmt(sql) {
7860+
Statement::CreateTable(create_table) => {
7861+
assert_eq!(1, create_table.constraints.len());
7862+
match &create_table.constraints[0] {
7863+
TableConstraint::Exclusion(c) => {
7864+
let characteristics = c.characteristics.as_ref().unwrap();
7865+
assert_eq!(characteristics.deferrable, Some(true));
7866+
assert_eq!(
7867+
characteristics.initially,
7868+
Some(DeferrableInitial::Deferred)
7869+
);
7870+
}
7871+
other => panic!("Expected Exclusion, got {other:?}"),
7872+
}
7873+
}
7874+
_ => panic!("Expected CreateTable"),
7875+
}
7876+
}
7877+
7878+
#[test]
7879+
fn parse_exclude_constraint_in_alter_table() {
7880+
let sql =
7881+
"ALTER TABLE t ADD CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =)";
7882+
pg().verified_stmt(sql);
7883+
}
7884+
7885+
#[test]
7886+
fn roundtrip_exclude_constraint() {
7887+
let sql = "CREATE TABLE t (CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =, during WITH &&) INCLUDE (id) WHERE (active = true))";
7888+
pg().verified_stmt(sql);
7889+
}

0 commit comments

Comments
 (0)