Skip to content

Commit 60b5fd2

Browse files
committed
fix(exclude): address upstream review feedback on PR #2307
- Rename `ExclusionConstraint`/`ExclusionElement`/`ExclusionOperator` (and `TableConstraint::Exclusion`) to `ExcludeConstraint`/ `ExcludeConstraintElement`/`ExcludeConstraintOperator`/ `TableConstraint::Exclude` to match the SQL keyword. - Tighten doc comments on the new types: add the canonical `[PostgreSQL](...)` doc link to each and drop redundant grammar sketches where the variant doc already carries it. - Introduce `Dialect::supports_exclude_constraint()` (default `false`, enabled for `PostgreSqlDialect` and `GenericDialect`) and replace the inline `dialect_of!` macro guard with the new method. - Extract the `EXCLUDE` parsing into `parse_exclude_constraint`, `parse_exclude_constraint_element`, and `parse_exclude_constraint_operator` instead of inlining the body of the constraint match arm. - Switch `ExcludeConstraintElement` to embed `IndexColumn` directly, parsed via `parse_create_index_expr`, instead of destructuring an `OrderByExpr`. - Replace the structural-delimiter `match` in `parse_exclude_constraint_operator` with an explicit `if matches!` guard plus a comment explaining the missing-operator case. - Move the `EXCLUDE` constraint tests out of `sqlparser_postgres.rs` into a single `parse_exclude_constraint` test in `sqlparser_common.rs` driven by `all_dialects_where(|d| d.supports_exclude_constraint())`, leaning on `verified_stmt` round-trips for breadth.
1 parent fc8b794 commit 60b5fd2

9 files changed

Lines changed: 206 additions & 445 deletions

File tree

src/ast/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ mod dml;
138138
pub mod helpers;
139139
pub mod table_constraints;
140140
pub use table_constraints::{
141-
CheckConstraint, ConstraintUsingIndex, ExclusionConstraint, ExclusionElement,
142-
ExclusionOperator, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint,
141+
CheckConstraint, ConstraintUsingIndex, ExcludeConstraint, ExcludeConstraintElement,
142+
ExcludeConstraintOperator, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint,
143143
PrimaryKeyConstraint, TableConstraint, UniqueConstraint,
144144
};
145145
mod operator;

src/ast/spans.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ impl Spanned for TableConstraint {
650650
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
651651
TableConstraint::PrimaryKeyUsingIndex(constraint)
652652
| TableConstraint::UniqueUsingIndex(constraint) => constraint.span(),
653-
TableConstraint::Exclusion(constraint) => constraint.span(),
653+
TableConstraint::Exclude(constraint) => constraint.span(),
654654
}
655655
}
656656
}

src/ast/table_constraints.rs

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
use crate::ast::{
2121
display_comma_separated, display_separated, ConstraintCharacteristics,
2222
ConstraintReferenceMatchKind, Expr, Ident, IndexColumn, IndexOption, IndexType,
23-
KeyOrIndexDisplay, NullsDistinctOption, ObjectName, OrderByOptions, ReferentialAction,
23+
KeyOrIndexDisplay, NullsDistinctOption, ObjectName, ReferentialAction,
2424
};
2525
use crate::tokenizer::Span;
2626
use core::fmt;
@@ -117,12 +117,12 @@ pub enum TableConstraint {
117117
///
118118
/// [1]: https://www.postgresql.org/docs/current/sql-altertable.html
119119
UniqueUsingIndex(ConstraintUsingIndex),
120-
/// PostgreSQL `EXCLUDE` constraint.
120+
/// `EXCLUDE` constraint.
121121
///
122122
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
123123
///
124-
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
125-
Exclusion(ExclusionConstraint),
124+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
125+
Exclude(ExcludeConstraint),
126126
}
127127

128128
impl From<UniqueConstraint> for TableConstraint {
@@ -161,9 +161,9 @@ impl From<FullTextOrSpatialConstraint> for TableConstraint {
161161
}
162162
}
163163

164-
impl From<ExclusionConstraint> for TableConstraint {
165-
fn from(constraint: ExclusionConstraint) -> Self {
166-
TableConstraint::Exclusion(constraint)
164+
impl From<ExcludeConstraint> for TableConstraint {
165+
fn from(constraint: ExcludeConstraint) -> Self {
166+
TableConstraint::Exclude(constraint)
167167
}
168168
}
169169

@@ -178,7 +178,7 @@ impl fmt::Display for TableConstraint {
178178
TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f),
179179
TableConstraint::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"),
180180
TableConstraint::UniqueUsingIndex(c) => c.fmt_with_keyword(f, "UNIQUE"),
181-
TableConstraint::Exclusion(constraint) => constraint.fmt(f),
181+
TableConstraint::Exclude(constraint) => constraint.fmt(f),
182182
}
183183
}
184184
}
@@ -617,22 +617,24 @@ impl crate::ast::Spanned for ConstraintUsingIndex {
617617
}
618618
}
619619

620-
/// The operator that follows `WITH` in an `EXCLUDE` element.
620+
/// The operator that follows `WITH` in an `EXCLUDE` constraint element.
621+
///
622+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
621623
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
622624
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
623625
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
624-
pub enum ExclusionOperator {
626+
pub enum ExcludeConstraintOperator {
625627
/// A single operator token, e.g. `=`, `&&`, `<->`.
626628
Token(String),
627629
/// Postgres schema-qualified form: `OPERATOR(schema.op)`.
628630
PgCustom(Vec<String>),
629631
}
630632

631-
impl fmt::Display for ExclusionOperator {
633+
impl fmt::Display for ExcludeConstraintOperator {
632634
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
633635
match self {
634-
ExclusionOperator::Token(token) => f.write_str(token),
635-
ExclusionOperator::PgCustom(parts) => {
636+
ExcludeConstraintOperator::Token(token) => f.write_str(token),
637+
ExcludeConstraintOperator::PgCustom(parts) => {
636638
write!(f, "OPERATOR({})", display_separated(parts, "."))
637639
}
638640
}
@@ -641,58 +643,46 @@ impl fmt::Display for ExclusionOperator {
641643

642644
/// One element in an `EXCLUDE` constraint's element list.
643645
///
644-
/// `{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] WITH <operator>`
645-
///
646-
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
646+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
647647
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
648648
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
649649
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
650-
pub struct ExclusionElement {
651-
/// The index expression or column name.
652-
pub expr: Expr,
653-
/// Optional operator class (e.g. `gist_geometry_ops_nd`).
654-
pub operator_class: Option<ObjectName>,
655-
/// Ordering options (ASC/DESC, NULLS FIRST/LAST).
656-
pub order: OrderByOptions,
650+
pub struct ExcludeConstraintElement {
651+
/// The index column (`{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]`).
652+
pub column: IndexColumn,
657653
/// The exclusion operator.
658-
pub operator: ExclusionOperator,
654+
pub operator: ExcludeConstraintOperator,
659655
}
660656

661-
impl fmt::Display for ExclusionElement {
657+
impl fmt::Display for ExcludeConstraintElement {
662658
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
663-
write!(f, "{}", self.expr)?;
664-
if let Some(opclass) = &self.operator_class {
665-
write!(f, " {opclass}")?;
666-
}
667-
write!(f, "{} WITH {}", self.order, self.operator)
659+
write!(f, "{} WITH {}", self.column, self.operator)
668660
}
669661
}
670662

671-
impl crate::ast::Spanned for ExclusionElement {
663+
impl crate::ast::Spanned for ExcludeConstraintElement {
672664
fn span(&self) -> Span {
673-
let mut span = self.expr.span();
674-
if let Some(opclass) = &self.operator_class {
665+
let mut span = self.column.column.expr.span();
666+
if let Some(opclass) = &self.column.operator_class {
675667
span = span.union(&opclass.span());
676668
}
677669
span
678670
}
679671
}
680672

681-
/// A PostgreSQL `EXCLUDE` constraint.
682-
///
683-
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
673+
/// An `EXCLUDE` constraint.
684674
///
685-
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
675+
/// [PostgreSql](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
686676
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
687677
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
688678
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
689-
pub struct ExclusionConstraint {
679+
pub struct ExcludeConstraint {
690680
/// Optional constraint name.
691681
pub name: Option<Ident>,
692682
/// Optional index method (e.g. `gist`, `spgist`).
693683
pub index_method: Option<Ident>,
694684
/// The list of index expressions with their exclusion operators.
695-
pub elements: Vec<ExclusionElement>,
685+
pub elements: Vec<ExcludeConstraintElement>,
696686
/// Optional list of additional columns to include in the index.
697687
pub include: Vec<Ident>,
698688
/// Optional `WHERE` predicate to restrict the constraint to a subset of rows.
@@ -701,7 +691,7 @@ pub struct ExclusionConstraint {
701691
pub characteristics: Option<ConstraintCharacteristics>,
702692
}
703693

704-
impl fmt::Display for ExclusionConstraint {
694+
impl fmt::Display for ExcludeConstraint {
705695
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
706696
use crate::ast::ddl::display_constraint_name;
707697
write!(f, "{}EXCLUDE", display_constraint_name(&self.name))?;
@@ -722,7 +712,7 @@ impl fmt::Display for ExclusionConstraint {
722712
}
723713
}
724714

725-
impl crate::ast::Spanned for ExclusionConstraint {
715+
impl crate::ast::Spanned for ExcludeConstraint {
726716
fn span(&self) -> Span {
727717
Span::union_iter(
728718
self.name

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ impl Dialect for GenericDialect {
129129
true
130130
}
131131

132+
fn supports_exclude_constraint(&self) -> bool {
133+
true
134+
}
135+
132136
fn supports_limit_comma(&self) -> bool {
133137
true
134138
}

src/dialect/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,13 @@ pub trait Dialect: Debug + Any {
11671167
false
11681168
}
11691169

1170+
/// Returns true if the dialect supports `EXCLUDE` table constraints, e.g.
1171+
/// `EXCLUDE USING gist (c WITH &&)` in `CREATE TABLE`/`ALTER TABLE`.
1172+
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>.
1173+
fn supports_exclude_constraint(&self) -> bool {
1174+
false
1175+
}
1176+
11701177
/// Returns true if the dialect supports the `LOAD DATA` statement
11711178
fn supports_load_data(&self) -> bool {
11721179
false

src/dialect/postgresql.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ impl Dialect for PostgreSqlDialect {
208208
true
209209
}
210210

211+
/// see <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
212+
fn supports_exclude_constraint(&self) -> bool {
213+
true
214+
}
215+
211216
/// see <https://www.postgresql.org/docs/13/functions-math.html>
212217
fn supports_factorial_operator(&self) -> bool {
213218
true

src/parser/mod.rs

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9907,50 +9907,9 @@ impl<'a> Parser<'a> {
99079907
))
99089908
}
99099909
Token::Word(w)
9910-
if w.keyword == Keyword::EXCLUDE
9911-
&& dialect_of!(self is PostgreSqlDialect | GenericDialect) =>
9910+
if w.keyword == Keyword::EXCLUDE && self.dialect.supports_exclude_constraint() =>
99129911
{
9913-
let index_method = if self.parse_keyword(Keyword::USING) {
9914-
Some(self.parse_identifier()?)
9915-
} else {
9916-
None
9917-
};
9918-
9919-
self.expect_token(&Token::LParen)?;
9920-
let elements = self.parse_comma_separated(|p| p.parse_exclusion_element())?;
9921-
self.expect_token(&Token::RParen)?;
9922-
9923-
let include = if self.parse_keyword(Keyword::INCLUDE) {
9924-
self.expect_token(&Token::LParen)?;
9925-
let cols = self.parse_comma_separated(|p| p.parse_identifier())?;
9926-
self.expect_token(&Token::RParen)?;
9927-
cols
9928-
} else {
9929-
vec![]
9930-
};
9931-
9932-
let where_clause = if self.parse_keyword(Keyword::WHERE) {
9933-
self.expect_token(&Token::LParen)?;
9934-
let predicate = self.parse_expr()?;
9935-
self.expect_token(&Token::RParen)?;
9936-
Some(Box::new(predicate))
9937-
} else {
9938-
None
9939-
};
9940-
9941-
let characteristics = self.parse_constraint_characteristics()?;
9942-
9943-
Ok(Some(
9944-
ExclusionConstraint {
9945-
name,
9946-
index_method,
9947-
elements,
9948-
include,
9949-
where_clause,
9950-
characteristics,
9951-
}
9952-
.into(),
9953-
))
9912+
Ok(Some(self.parse_exclude_constraint(name)?.into()))
99549913
}
99559914
_ => {
99569915
if name.is_some() {
@@ -9963,47 +9922,88 @@ impl<'a> Parser<'a> {
99639922
}
99649923
}
99659924

9966-
fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, ParserError> {
9967-
// `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ NULLS FIRST | LAST ].
9968-
// Shared with `CREATE INDEX` columns.
9969-
let (
9970-
OrderByExpr {
9971-
expr,
9972-
options: order,
9973-
..
9974-
},
9975-
operator_class,
9976-
) = self.parse_order_by_expr_inner(true)?;
9925+
/// Parse an `EXCLUDE` table constraint, with the leading `EXCLUDE` keyword
9926+
/// already consumed.
9927+
fn parse_exclude_constraint(
9928+
&mut self,
9929+
name: Option<Ident>,
9930+
) -> Result<ExcludeConstraint, ParserError> {
9931+
let index_method = if self.parse_keyword(Keyword::USING) {
9932+
Some(self.parse_identifier()?)
9933+
} else {
9934+
None
9935+
};
99779936

9978-
self.expect_keyword_is(Keyword::WITH)?;
9979-
let operator = self.parse_exclusion_operator()?;
9937+
self.expect_token(&Token::LParen)?;
9938+
let elements = self.parse_comma_separated(|p| p.parse_exclude_constraint_element())?;
9939+
self.expect_token(&Token::RParen)?;
99809940

9981-
Ok(ExclusionElement {
9982-
expr,
9983-
operator_class,
9984-
order,
9985-
operator,
9941+
let include = if self.parse_keyword(Keyword::INCLUDE) {
9942+
self.expect_token(&Token::LParen)?;
9943+
let cols = self.parse_comma_separated(|p| p.parse_identifier())?;
9944+
self.expect_token(&Token::RParen)?;
9945+
cols
9946+
} else {
9947+
vec![]
9948+
};
9949+
9950+
let where_clause = if self.parse_keyword(Keyword::WHERE) {
9951+
self.expect_token(&Token::LParen)?;
9952+
let predicate = self.parse_expr()?;
9953+
self.expect_token(&Token::RParen)?;
9954+
Some(Box::new(predicate))
9955+
} else {
9956+
None
9957+
};
9958+
9959+
let characteristics = self.parse_constraint_characteristics()?;
9960+
9961+
Ok(ExcludeConstraint {
9962+
name,
9963+
index_method,
9964+
elements,
9965+
include,
9966+
where_clause,
9967+
characteristics,
99869968
})
99879969
}
99889970

9971+
fn parse_exclude_constraint_element(
9972+
&mut self,
9973+
) -> Result<ExcludeConstraintElement, ParserError> {
9974+
// `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ NULLS FIRST | LAST ].
9975+
// Shared with `CREATE INDEX` columns.
9976+
let column = self.parse_create_index_expr()?;
9977+
self.expect_keyword_is(Keyword::WITH)?;
9978+
let operator = self.parse_exclude_constraint_operator()?;
9979+
Ok(ExcludeConstraintElement { column, operator })
9980+
}
9981+
99899982
/// Parse the operator that follows `WITH` in an `EXCLUDE` element.
99909983
///
99919984
/// Accepts either a single operator token (e.g. `=`, `&&`, `<->`) or the
99929985
/// Postgres `OPERATOR(schema.op)` form for schema-qualified operators.
9993-
fn parse_exclusion_operator(&mut self) -> Result<ExclusionOperator, ParserError> {
9986+
fn parse_exclude_constraint_operator(
9987+
&mut self,
9988+
) -> Result<ExcludeConstraintOperator, ParserError> {
99949989
if self.parse_keyword(Keyword::OPERATOR) {
9995-
return Ok(ExclusionOperator::PgCustom(
9990+
return Ok(ExcludeConstraintOperator::PgCustom(
99969991
self.parse_pg_operator_ident_parts()?,
99979992
));
99989993
}
99999994

9995+
// Reject structural delimiters (`,`, `)`, `;`, EOF) since they signal a
9996+
// missing operator between `WITH` and the next element / end of list.
100009997
let operator_token = self.next_token();
10001-
match &operator_token.token {
10002-
Token::EOF | Token::RParen | Token::Comma | Token::SemiColon => {
10003-
self.expected("exclusion operator", operator_token)
10004-
}
10005-
_ => Ok(ExclusionOperator::Token(operator_token.token.to_string())),
9998+
if matches!(
9999+
operator_token.token,
10000+
Token::EOF | Token::RParen | Token::Comma | Token::SemiColon
10001+
) {
10002+
return self.expected("exclusion operator", operator_token);
1000610003
}
10004+
Ok(ExcludeConstraintOperator::Token(
10005+
operator_token.token.to_string(),
10006+
))
1000710007
}
1000810008

1000910009
/// Parse the body of a Postgres `OPERATOR(schema.op)` form — i.e. the

0 commit comments

Comments
 (0)