Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ 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, ExclusionConstraint, ExclusionElement,
ExclusionOperator, 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 @@ -643,6 +643,7 @@ impl Spanned for TableConstraint {
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
TableConstraint::PrimaryKeyUsingIndex(constraint)
| TableConstraint::UniqueUsingIndex(constraint) => constraint.span(),
TableConstraint::Exclusion(constraint) => constraint.span(),
}
}
}
Expand Down
137 changes: 135 additions & 2 deletions src/ast/table_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
use crate::ast::{
display_comma_separated, display_separated, ConstraintCharacteristics,
ConstraintReferenceMatchKind, Expr, Ident, IndexColumn, IndexOption, IndexType,
KeyOrIndexDisplay, NullsDistinctOption, ObjectName, ReferentialAction,
KeyOrIndexDisplay, NullsDistinctOption, ObjectName, OrderByOptions, ReferentialAction,
};
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),
/// PostgreSQL `EXCLUDE` constraint.
///
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
///
/// See <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE>
Exclusion(ExclusionConstraint),
}

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

impl From<ExclusionConstraint> for TableConstraint {
fn from(constraint: ExclusionConstraint) -> Self {
TableConstraint::Exclusion(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::Exclusion(constraint) => constraint.fmt(f),
}
}
}
Expand Down Expand Up @@ -603,3 +616,123 @@ impl crate::ast::Spanned for ConstraintUsingIndex {
start.union(&end)
}
}

/// The operator that follows `WITH` in an `EXCLUDE` element.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum ExclusionOperator {
/// A single operator token, e.g. `=`, `&&`, `<->`.
Token(String),
/// Postgres schema-qualified form: `OPERATOR(schema.op)`.
PgCustom(Vec<String>),
}

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

/// One element in an `EXCLUDE` constraint's element list.
///
/// `{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] WITH <operator>`
///
/// See <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 ExclusionElement {
/// The index expression or column name.
pub expr: Expr,
/// Optional operator class (e.g. `gist_geometry_ops_nd`).
pub operator_class: Option<ObjectName>,
/// Ordering options (ASC/DESC, NULLS FIRST/LAST).
pub order: OrderByOptions,
/// The exclusion operator.
pub operator: ExclusionOperator,
}

impl fmt::Display for ExclusionElement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.expr)?;
if let Some(opclass) = &self.operator_class {
write!(f, " {opclass}")?;
}
write!(f, "{} WITH {}", self.order, self.operator)
}
}

impl crate::ast::Spanned for ExclusionElement {
fn span(&self) -> Span {
let mut span = self.expr.span();
if let Some(opclass) = &self.operator_class {
span = span.union(&opclass.span());
}
span
}
}

/// A PostgreSQL `EXCLUDE` constraint.
///
/// `[ CONSTRAINT <name> ] EXCLUDE [ USING <index_method> ] ( <element> WITH <operator> [, ...] ) [ INCLUDE (<cols>) ] [ WHERE (<predicate>) ]`
///
/// See <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 ExclusionConstraint {
/// 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<ExclusionElement>,
/// 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 ExclusionConstraint {
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 ExclusionConstraint {
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())),
)
}
}
Loading
Loading