Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d266014
feat(ast): add ExclusionConstraint and ExclusionElement types
fmguerreiro Apr 16, 2026
a957bb7
feat(parser): parse EXCLUDE constraints in CREATE TABLE and ALTER TABLE
fmguerreiro Apr 16, 2026
6f47488
test: add EXCLUDE constraint parsing tests
fmguerreiro Apr 16, 2026
17cbba9
refactor: clean up exclusion constraint additions for upstream review
fmguerreiro Apr 16, 2026
a0cacb2
fix: tighten exclusion constraint parsing per upstream review
fmguerreiro Apr 16, 2026
a189d8f
feat(ast): extend ExclusionElement with operator_class and order options
fmguerreiro Apr 16, 2026
dddb2ce
fix(parser): gate EXCLUDE by PG dialect; parse OPERATOR() and element…
fmguerreiro Apr 16, 2026
2d37749
test: realign EXCLUDE tests to current APIs and expand coverage
fmguerreiro Apr 16, 2026
90803e0
refactor: reuse parse_order_by_expr_inner and tighten exclude tests
fmguerreiro Apr 16, 2026
442e196
fix: resolve collapsible_match clippy lints in parser
fmguerreiro Apr 17, 2026
d8e35d9
Merge remote-tracking branch 'origin/feat/exclude-constraint-upstream…
fmguerreiro Apr 17, 2026
e02613e
style: cargo fmt
fmguerreiro Apr 17, 2026
837b5a0
fix: remove accidentally committed worktree dirs
fmguerreiro Apr 17, 2026
228c969
feat(ast): model exclusion operator as an enum
fmguerreiro Apr 17, 2026
abfc1c6
Merge remote-tracking branch 'origin/feat/exclude-constraint-upstream…
fmguerreiro Apr 17, 2026
460c098
fix: address review feedback on exclusion constraint PR
fmguerreiro Apr 17, 2026
f699bea
fix: merge review feedback with upstream EXCLUDE changes
fmguerreiro Apr 17, 2026
fc8b794
Merge remote-tracking branch 'upstream/main' into feat/exclude-constr…
fmguerreiro Apr 24, 2026
60b5fd2
fix(exclude): address upstream review feedback on PR #2307
fmguerreiro Apr 24, 2026
4e5e49d
fix(exclude): address remaining review feedback on PR #2307
fmguerreiro May 12, 2026
c1f95d8
Merge branch 'main' into feat/exclude-constraint-upstream
fmguerreiro May 18, 2026
ca21c1a
fix(exclude): apply PR #2307 review feedback
fmguerreiro May 18, 2026
f03de0a
fix(exclude): apply second-round PR #2307 review feedback
fmguerreiro May 24, 2026
87fada6
fix(exclude): trim parse_exclude_constraint_operator rustdoc to summa…
fmguerreiro May 24, 2026
61fa1b6
revert unrelated .gitignore changes
fmguerreiro May 25, 2026
68702c6
remove newline in .gitignore
fmguerreiro May 25, 2026
46f0f7e
remove trailing newline in .gitignore to match upstream
fmguerreiro May 25, 2026
b04a6f9
fix(exclude): keep EXCLUDE usable as a column name on PG/generic
fmguerreiro May 25, 2026
7e29bd7
test(exclude): cover named EXCLUDE not followed by USING/(
fmguerreiro May 25, 2026
944182c
style(exclude): match project comment conventions
fmguerreiro May 25, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Cargo.lock

*.swp

.DS_store
.DS_store
.worktrees/
5 changes: 3 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ mod dml;
pub mod helpers;
pub mod table_constraints;
pub use table_constraints::{
CheckConstraint, ConstraintUsingIndex, ForeignKeyConstraint, FullTextOrSpatialConstraint,
IndexConstraint, PrimaryKeyConstraint, TableConstraint, UniqueConstraint,
CheckConstraint, ConstraintUsingIndex, ExcludeConstraint, ExcludeConstraintElement,
ExcludeConstraintOperator, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint,
PrimaryKeyConstraint, TableConstraint, UniqueConstraint,
};
mod operator;
mod query;
Expand Down
17 changes: 12 additions & 5 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ use super::{
AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef,
ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements,
ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable,
CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr,
ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr,
FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound,
IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join,
JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause,
CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement,
ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg,
FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr,
HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr,
Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause,
MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr,
MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList,
NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction,
Expand Down Expand Up @@ -654,6 +654,7 @@ impl Spanned for TableConstraint {
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
TableConstraint::PrimaryKeyUsingIndex(constraint)
| TableConstraint::UniqueUsingIndex(constraint) => constraint.span(),
TableConstraint::Exclude(constraint) => constraint.span(),
}
}
}
Expand Down Expand Up @@ -723,6 +724,12 @@ impl Spanned for IndexColumn {
}
}

impl Spanned for ExcludeConstraintElement {
fn span(&self) -> Span {
self.column.span()
}
}

impl Spanned for CaseStatement {
fn span(&self) -> Span {
let CaseStatement {
Expand Down
115 changes: 114 additions & 1 deletion src/ast/table_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::tokenizer::Span;
use core::fmt;

#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, vec::Vec};
use alloc::{boxed::Box, string::String, vec::Vec};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -117,6 +117,12 @@ pub enum TableConstraint {
///
/// [1]: https://www.postgresql.org/docs/current/sql-altertable.html
UniqueUsingIndex(ConstraintUsingIndex),
/// `EXCLUDE` constraint.
///
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
///
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
Exclude(ExcludeConstraint),
}

impl From<UniqueConstraint> for TableConstraint {
Expand Down Expand Up @@ -155,6 +161,12 @@ impl From<FullTextOrSpatialConstraint> for TableConstraint {
}
}

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

impl fmt::Display for TableConstraint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Expand All @@ -166,6 +178,7 @@ impl fmt::Display for TableConstraint {
TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f),
TableConstraint::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"),
TableConstraint::UniqueUsingIndex(c) => c.fmt_with_keyword(f, "UNIQUE"),
TableConstraint::Exclude(constraint) => constraint.fmt(f),
}
}
}
Expand Down Expand Up @@ -603,3 +616,103 @@ impl crate::ast::Spanned for ConstraintUsingIndex {
start.union(&end)
}
}

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

impl fmt::Display for ExcludeConstraintOperator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ExcludeConstraintOperator::Token(token) => f.write_str(token),
ExcludeConstraintOperator::PGCustom(parts) => {
write!(f, "OPERATOR({})", display_separated(parts, "."))
}
}
}
}

/// One element in an `EXCLUDE` constraint's element list.
///
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ExcludeConstraintElement {
/// The index column (`{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]`).
pub column: IndexColumn,
/// The exclusion operator.
pub operator: ExcludeConstraintOperator,
}

impl fmt::Display for ExcludeConstraintElement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} WITH {}", self.column, self.operator)
}
}

/// An `EXCLUDE` constraint.
///
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ExcludeConstraint {
/// Optional constraint name.
pub name: Option<Ident>,
/// Optional index method (e.g. `gist`, `spgist`).
pub index_method: Option<Ident>,
/// The list of index expressions with their exclusion operators.
pub elements: Vec<ExcludeConstraintElement>,
/// Optional list of additional columns to include in the index.
pub include: Vec<Ident>,
/// Optional `WHERE` predicate to restrict the constraint to a subset of rows.
pub where_clause: Option<Box<Expr>>,
/// Optional constraint characteristics like `DEFERRABLE`.
pub characteristics: Option<ConstraintCharacteristics>,
}

impl fmt::Display for ExcludeConstraint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use crate::ast::ddl::display_constraint_name;
write!(f, "{}EXCLUDE", display_constraint_name(&self.name))?;
if let Some(method) = &self.index_method {
write!(f, " USING {method}")?;
}
write!(f, " ({})", display_comma_separated(&self.elements))?;
if !self.include.is_empty() {
write!(f, " INCLUDE ({})", display_comma_separated(&self.include))?;
}
if let Some(predicate) = &self.where_clause {
write!(f, " WHERE ({predicate})")?;
}
if let Some(characteristics) = &self.characteristics {
write!(f, " {characteristics}")?;
}
Ok(())
}
}

impl crate::ast::Spanned for ExcludeConstraint {
fn span(&self) -> Span {
Span::union_iter(
self.name
.iter()
.map(|i| i.span)
.chain(self.index_method.iter().map(|i| i.span))
.chain(self.elements.iter().map(|e| e.span()))
.chain(self.include.iter().map(|i| i.span))
.chain(self.where_clause.iter().map(|e| e.span()))
.chain(self.characteristics.iter().map(|c| c.span())),
)
}
}
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_exclude_constraint(&self) -> bool {
true
}

fn supports_limit_comma(&self) -> bool {
true
}
Expand Down
7 changes: 7 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,13 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports `EXCLUDE` table constraints, e.g.
/// `EXCLUDE USING gist (c WITH &&)` in `CREATE TABLE`/`ALTER TABLE`.
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>.
fn supports_exclude_constraint(&self) -> bool {
false
}

/// Returns true if the dialect supports the `LOAD DATA` statement
fn supports_load_data(&self) -> bool {
false
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ impl Dialect for PostgreSqlDialect {
true
}

/// see <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
fn supports_exclude_constraint(&self) -> bool {
true
}

/// see <https://www.postgresql.org/docs/13/functions-math.html>
fn supports_factorial_operator(&self) -> bool {
true
Expand Down
131 changes: 116 additions & 15 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3873,21 +3873,12 @@ impl<'a> Parser<'a> {
Keyword::XOR => Some(BinaryOperator::Xor),
Keyword::OVERLAPS => Some(BinaryOperator::Overlaps),
Keyword::OPERATOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => {
self.expect_token(&Token::LParen)?;
// there are special rules for operator names in
// postgres so we can not use 'parse_object'
// or similar.
// Postgres has special rules for operator names so we can
// not use `parse_object` or similar.
// See https://www.postgresql.org/docs/current/sql-createoperator.html
let mut idents = vec![];
loop {
self.advance_token();
idents.push(self.get_current_token().to_string());
if !self.consume_token(&Token::Period) {
break;
}
}
self.expect_token(&Token::RParen)?;
Some(BinaryOperator::PGCustomBinaryOperator(idents))
Some(BinaryOperator::PGCustomBinaryOperator(
self.parse_pg_operator_ident_parts()?,
))
}
_ => None,
},
Expand Down Expand Up @@ -10009,9 +10000,14 @@ impl<'a> Parser<'a> {
.into(),
))
}
Token::Word(w)
if w.keyword == Keyword::EXCLUDE && self.dialect.supports_exclude_constraint() =>
{
Ok(Some(self.parse_exclude_constraint(name)?.into()))
}
_ => {
if name.is_some() {
self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", next_token)
self.expected("PRIMARY, UNIQUE, FOREIGN, CHECK, or EXCLUDE", next_token)
} else {
self.prev_token();
Ok(None)
Expand All @@ -10020,6 +10016,111 @@ impl<'a> Parser<'a> {
}
}

/// Parse an `EXCLUDE` table constraint, with the leading `EXCLUDE` keyword
/// already consumed.
fn parse_exclude_constraint(
&mut self,
name: Option<Ident>,
) -> Result<ExcludeConstraint, ParserError> {
let index_method = if self.parse_keyword(Keyword::USING) {
Some(self.parse_identifier()?)
} else {
None
};

self.expect_token(&Token::LParen)?;
let elements = self.parse_comma_separated(|p| p.parse_exclude_constraint_element())?;
self.expect_token(&Token::RParen)?;

let include = if self.parse_keyword(Keyword::INCLUDE) {
self.expect_token(&Token::LParen)?;
let cols = self.parse_comma_separated(|p| p.parse_identifier())?;
self.expect_token(&Token::RParen)?;
cols
} else {
vec![]
};

let where_clause = if self.parse_keyword(Keyword::WHERE) {
self.expect_token(&Token::LParen)?;
let predicate = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
Some(Box::new(predicate))
} else {
None
};

let characteristics = self.parse_constraint_characteristics()?;

Ok(ExcludeConstraint {
name,
index_method,
elements,
include,
where_clause,
characteristics,
})
}

fn parse_exclude_constraint_element(
&mut self,
) -> Result<ExcludeConstraintElement, ParserError> {
// `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ NULLS FIRST | LAST ].
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ NULLS FIRST | LAST ].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed in f03de0a.

let column = self.parse_create_index_expr()?;
self.expect_keyword_is(Keyword::WITH)?;
let operator = self.parse_exclude_constraint_operator()?;
Ok(ExcludeConstraintElement { column, operator })
}

/// Parse the operator that follows `WITH` in an `EXCLUDE` element.
///
/// Accepts either a single operator token (e.g. `=`, `&&`, `<->`) or the
/// Postgres `OPERATOR(schema.op)` form for schema-qualified operators.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
///
/// Accepts either a single operator token (e.g. `=`, `&&`, `<->`) or the
/// Postgres `OPERATOR(schema.op)` form for schema-qualified operators.

Copy link
Copy Markdown
Contributor Author

@fmguerreiro fmguerreiro Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 60b5fd2.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't seem that this was addressed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the destructuring was dropped in 60b5fd2: parse_exclude_constraint_element now calls parse_create_index_expr() and stores the IndexColumn directly as column, no OrderByExpr destructuring. the redundant inline grammar comment is removed in f03de0a. the remaining /// on parse_exclude_constraint_operator is rustdoc, consistent with the other private parse_* helpers in this file. let me know if you want that dropped too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trimmed the rustdoc to the summary line in 87fada6.

fn parse_exclude_constraint_operator(
&mut self,
) -> Result<ExcludeConstraintOperator, ParserError> {
if self.parse_keyword(Keyword::OPERATOR) {
return Ok(ExcludeConstraintOperator::PGCustom(
self.parse_pg_operator_ident_parts()?,
));
}

// Reject structural delimiters (`,`, `)`, `;`, EOF) since they signal a
// missing operator between `WITH` and the next element / end of list.
let operator_token = self.next_token();
if matches!(
operator_token.token,
Token::EOF | Token::RParen | Token::Comma | Token::SemiColon
) {
return self.expected("exclusion operator", operator_token);
}
Ok(ExcludeConstraintOperator::Token(
operator_token.token.to_string(),
))
}

/// Parse the body of a Postgres `OPERATOR(schema.op)` form — i.e. the
/// parenthesised `.`-separated path of name parts after the `OPERATOR`
/// keyword. Shared between binary expression parsing and exclusion
/// constraint parsing.
fn parse_pg_operator_ident_parts(&mut self) -> Result<Vec<String>, ParserError> {
self.expect_token(&Token::LParen)?;
if self.peek_token_ref().token == Token::RParen {
let token = self.next_token();
return self.expected("operator name", token);
}
let mut idents = vec![];
loop {
self.advance_token();
idents.push(self.get_current_token().to_string());
if !self.consume_token(&Token::Period) {
break;
}
}
self.expect_token(&Token::RParen)?;
Ok(idents)
}

fn parse_optional_nulls_distinct(&mut self) -> Result<NullsDistinctOption, ParserError> {
Ok(if self.parse_keyword(Keyword::NULLS) {
let not = self.parse_keyword(Keyword::NOT);
Expand Down
Loading
Loading