iffyio commented on code in PR #2307:
URL: 
https://github.com/apache/datafusion-sqlparser-rs/pull/2307#discussion_r3137622921


##########
src/ast/table_constraints.rs:
##########
@@ -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),

Review Comment:
   ```suggestion
       Exclude(ExcludeConstraint),
   ```
   does this make more sense if the constraint is called 'EXCLUDE' rather than 
'EXCLUSION'?



##########
src/ast/table_constraints.rs:
##########
@@ -603,3 +616,123 @@ impl crate::ast::Spanned for ConstraintUsingIndex {
         start.union(&end)
     }
 }
+
+/// The operator that follows `WITH` in an `EXCLUDE` element.

Review Comment:
   Can we add a link to the docs? Otherwise I think this struct wouldn't be 
very descriptive on its own



##########
src/ast/table_constraints.rs:
##########
@@ -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>

Review Comment:
   ```suggestion
       /// `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)
   ```



##########
src/ast/table_constraints.rs:
##########
@@ -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>

Review Comment:
   ```suggestion
   /// One element in an `EXCLUDE` constraint's element list.
   ///
   /// 
[PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
   ```



##########
src/ast/table_constraints.rs:
##########
@@ -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 {

Review Comment:
   ```suggestion
   pub struct ExcludeConstraintElement {
   ```



##########
src/parser/mod.rs:
##########
@@ -9915,9 +9895,55 @@ impl<'a> Parser<'a> {
                     .into(),
                 ))
             }
+            Token::Word(w)
+                if w.keyword == Keyword::EXCLUDE
+                    && dialect_of!(self is PostgreSqlDialect | GenericDialect) 
=>

Review Comment:
   can we introduce a dialect method for this instead of the macro



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].

Review Comment:
   ```suggestion
   ```



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].
+        // Shared with `CREATE INDEX` columns.
+        let (
+            OrderByExpr {
+                expr,
+                options: order,
+                ..
+            },
+            operator_class,

Review Comment:
   this pattern looks a bit unusual, does it make more sense to instead call 
parse_order_by_expr_inner directly and store the IndexColumn instead of 
destructuring it in the ExclusionElement struct?



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].
+        // Shared with `CREATE INDEX` columns.
+        let (
+            OrderByExpr {
+                expr,
+                options: order,
+                ..
+            },
+            operator_class,
+        ) = self.parse_order_by_expr_inner(true)?;
+
+        self.expect_keyword_is(Keyword::WITH)?;
+        let operator = self.parse_exclusion_operator()?;
+
+        Ok(ExclusionElement {
+            expr,
+            operator_class,
+            order,
+            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.
+    fn parse_exclusion_operator(&mut self) -> Result<ExclusionOperator, 
ParserError> {
+        if self.parse_keyword(Keyword::OPERATOR) {
+            return Ok(ExclusionOperator::PgCustom(
+                self.parse_pg_operator_ident_parts()?,
+            ));
+        }
+
+        let operator_token = self.next_token();
+        match &operator_token.token {
+            Token::EOF | Token::RParen | Token::Comma | Token::SemiColon => {

Review Comment:
   hmm could you explain this condition e.g. how does an optional RParen work?



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].
+        // Shared with `CREATE INDEX` columns.
+        let (
+            OrderByExpr {
+                expr,
+                options: order,
+                ..
+            },
+            operator_class,
+        ) = self.parse_order_by_expr_inner(true)?;
+
+        self.expect_keyword_is(Keyword::WITH)?;
+        let operator = self.parse_exclusion_operator()?;
+
+        Ok(ExclusionElement {
+            expr,
+            operator_class,
+            order,
+            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.
+    fn parse_exclusion_operator(&mut self) -> Result<ExclusionOperator, 
ParserError> {

Review Comment:
   ```suggestion
       fn parse_exclude_constraint_operator(&mut self) -> 
Result<ExclusionOperator, ParserError> {
   ```



##########
tests/sqlparser_postgres.rs:
##########
@@ -9193,3 +9243,310 @@ fn parse_lock_table() {
         }
     }
 }
+
+#[test]
+fn parse_exclude_constraint_with_where() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) WHERE 
(col > 0))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert!(c.where_clause.is_some());
+                    match c.where_clause.as_ref().unwrap().as_ref() {
+                        Expr::BinaryOp { left, op, right } => {
+                            assert_eq!(**left, 
Expr::Identifier(Ident::new("col")));
+                            assert_eq!(*op, BinaryOperator::Gt);
+                            assert_eq!(**right, 
Expr::Value(number("0").with_empty_span()));
+                        }
+                        other => panic!("Expected BinaryOp, got {other:?}"),
+                    }
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_with_include() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) 
INCLUDE (col))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert_eq!(c.elements.len(), 1);
+                    assert_eq!(c.include, vec![Ident::new("col")]);
+                    assert!(c.where_clause.is_none());
+                    assert!(c.characteristics.is_none());
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_no_using() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE (col WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert!(c.index_method.is_none());
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_deferrable() {
+    let sql =
+        "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) DEFERRABLE 
INITIALLY DEFERRED)";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    let characteristics = c.characteristics.as_ref().unwrap();
+                    assert_eq!(characteristics.deferrable, Some(true));
+                    assert_eq!(characteristics.initially, 
Some(DeferrableInitial::Deferred));
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_in_alter_table() {
+    let sql = "ALTER TABLE t ADD CONSTRAINT no_overlap EXCLUDE USING gist 
(room WITH =)";
+    match pg().verified_stmt(sql) {
+        Statement::AlterTable(alter_table) => match &alter_table.operations[0] 
{
+            AlterTableOperation::AddConstraint {
+                constraint: TableConstraint::Exclusion(c),
+                ..
+            } => {
+                assert_eq!(c.name, Some(Ident::new("no_overlap")));
+                assert_eq!(c.elements[0].operator.to_string(), "=");
+            }
+            other => panic!("Expected AddConstraint(Exclusion), got 
{other:?}"),
+        },
+        _ => panic!("Expected AlterTable"),
+    }
+}
+
+#[test]
+fn roundtrip_exclude_constraint() {
+    let sql = "CREATE TABLE t (CONSTRAINT no_overlap EXCLUDE USING gist (room 
WITH =, during WITH &&) INCLUDE (id) WHERE (active = true))";

Review Comment:
   a couple comments re the tests, for this scenario we already use 
verified_stmt in the other tests so that unless this is testing some specific 
syntax we can remove it (or rename it accordingly)?
   Then can we merge the tests into the same function and just rely on verified 
statement for them (except maybe one simple scenario if we want to verify the 
ast)? that would keep the tests simpler



##########
tests/sqlparser_postgres.rs:
##########
@@ -9193,3 +9243,310 @@ fn parse_lock_table() {
         }
     }
 }
+
+#[test]
+fn parse_exclude_constraint_with_where() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) WHERE 
(col > 0))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert!(c.where_clause.is_some());
+                    match c.where_clause.as_ref().unwrap().as_ref() {
+                        Expr::BinaryOp { left, op, right } => {
+                            assert_eq!(**left, 
Expr::Identifier(Ident::new("col")));
+                            assert_eq!(*op, BinaryOperator::Gt);
+                            assert_eq!(**right, 
Expr::Value(number("0").with_empty_span()));
+                        }
+                        other => panic!("Expected BinaryOp, got {other:?}"),
+                    }
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_with_include() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) 
INCLUDE (col))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert_eq!(c.elements.len(), 1);
+                    assert_eq!(c.include, vec![Ident::new("col")]);
+                    assert!(c.where_clause.is_none());
+                    assert!(c.characteristics.is_none());
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_no_using() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE (col WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    assert!(c.index_method.is_none());
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_deferrable() {
+    let sql =
+        "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) DEFERRABLE 
INITIALLY DEFERRED)";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => {
+            assert_eq!(1, create_table.constraints.len());
+            match &create_table.constraints[0] {
+                TableConstraint::Exclusion(c) => {
+                    let characteristics = c.characteristics.as_ref().unwrap();
+                    assert_eq!(characteristics.deferrable, Some(true));
+                    assert_eq!(characteristics.initially, 
Some(DeferrableInitial::Deferred));
+                }
+                other => panic!("Expected Exclusion, got {other:?}"),
+            }
+        }
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_in_alter_table() {
+    let sql = "ALTER TABLE t ADD CONSTRAINT no_overlap EXCLUDE USING gist 
(room WITH =)";
+    match pg().verified_stmt(sql) {
+        Statement::AlterTable(alter_table) => match &alter_table.operations[0] 
{
+            AlterTableOperation::AddConstraint {
+                constraint: TableConstraint::Exclusion(c),
+                ..
+            } => {
+                assert_eq!(c.name, Some(Ident::new("no_overlap")));
+                assert_eq!(c.elements[0].operator.to_string(), "=");
+            }
+            other => panic!("Expected AddConstraint(Exclusion), got 
{other:?}"),
+        },
+        _ => panic!("Expected AlterTable"),
+    }
+}
+
+#[test]
+fn roundtrip_exclude_constraint() {
+    let sql = "CREATE TABLE t (CONSTRAINT no_overlap EXCLUDE USING gist (room 
WITH =, during WITH &&) INCLUDE (id) WHERE (active = true))";
+    pg().verified_stmt(sql);
+}
+
+#[test]
+fn parse_exclude_constraint_not_deferrable_initially_immediate() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) NOT 
DEFERRABLE INITIALLY IMMEDIATE)";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => {
+                let characteristics = c.characteristics.as_ref().unwrap();
+                assert_eq!(characteristics.deferrable, Some(false));
+                assert_eq!(
+                    characteristics.initially,
+                    Some(DeferrableInitial::Immediate)
+                );
+            }
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_collate() {
+    // `COLLATE` is consumed by the element expression parser; verify that
+    // a collated column round-trips inside an EXCLUDE element.
+    pg().verified_stmt(
+        "CREATE TABLE t (name TEXT, EXCLUDE USING btree (name COLLATE \"C\" 
WITH =))",
+    );
+}
+
+#[test]
+fn parse_exclude_constraint_operator_class() {
+    let sql = "CREATE TABLE t (col TEXT, EXCLUDE USING gist (col 
text_pattern_ops WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => {
+                assert_eq!(c.elements.len(), 1);
+                assert_eq!(c.elements[0].expr, 
Expr::Identifier(Ident::new("col")));
+                assert_eq!(
+                    c.elements[0].operator_class,
+                    
Some(ObjectName::from(vec![Ident::new("text_pattern_ops")]))
+                );
+                assert_eq!(c.elements[0].operator.to_string(), "=");
+            }
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_asc_nulls_last() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING btree (col ASC NULLS 
LAST WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => {
+                assert_eq!(c.elements[0].order.asc, Some(true));
+                assert_eq!(c.elements[0].order.nulls_first, Some(false));
+            }
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_desc_nulls_first() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING btree (col DESC NULLS 
FIRST WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => {
+                assert_eq!(c.elements[0].order.asc, Some(false));
+                assert_eq!(c.elements[0].order.nulls_first, Some(true));
+            }
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_function_expression() {
+    let sql =
+        "CREATE TABLE t (name TEXT, EXCLUDE USING gist ((lower(name)) 
text_pattern_ops WITH =))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => {
+                assert_eq!(c.elements.len(), 1);
+                match &c.elements[0].expr {
+                    Expr::Nested(inner) => match inner.as_ref() {
+                        Expr::Function(func) => {
+                            assert_eq!(func.name.to_string(), "lower");
+                        }
+                        other => panic!("Expected Function inside Nested, got 
{other:?}"),
+                    },
+                    other => panic!("Expected Nested expr, got {other:?}"),
+                }
+                assert_eq!(
+                    c.elements[0].operator_class,
+                    
Some(ObjectName::from(vec![Ident::new("text_pattern_ops")]))
+                );
+                assert_eq!(c.elements[0].operator.to_string(), "=");
+            }
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn parse_exclude_constraint_pg_custom_operator() {
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH 
OPERATOR(pg_catalog.=)))";
+    match pg().verified_stmt(sql) {
+        Statement::CreateTable(create_table) => match 
&create_table.constraints[0] {
+            TableConstraint::Exclusion(c) => match &c.elements[0].operator {
+                ExclusionOperator::PgCustom(parts) => {
+                    assert_eq!(parts, &vec!["pg_catalog".to_string(), 
"=".to_string()]);
+                }
+                other => panic!("Expected PgCustom operator, got {other:?}"),
+            },
+            other => panic!("Expected Exclusion, got {other:?}"),
+        },
+        _ => panic!("Expected CreateTable"),
+    }
+}
+
+#[test]
+fn exclude_missing_with_keyword_errors() {
+    let sql = "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist (col))";
+    let err = pg().parse_sql_statements(sql).unwrap_err();
+    assert!(
+        err.to_string().contains("Expected: WITH"),
+        "unexpected error: {err}"
+    );
+}
+
+#[test]
+fn exclude_empty_element_list_errors() {
+    let sql = "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist ())";
+    let err = pg().parse_sql_statements(sql).unwrap_err();
+    assert!(
+        err.to_string().contains("Expected: an expression"),
+        "unexpected error: {err}"
+    );
+}
+
+#[test]
+fn exclude_missing_operator_errors() {
+    let sql = "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist (col WITH))";
+    let err = pg().parse_sql_statements(sql).unwrap_err();
+    assert!(
+        err.to_string().contains("exclusion operator"),
+        "unexpected error: {err}"
+    );
+}
+
+#[test]
+fn parse_exclude_constraint_operator_with_ordering() {
+    pg().verified_stmt(
+        "CREATE TABLE t (col INT, CONSTRAINT c EXCLUDE USING gist (col ASC 
WITH OPERATOR(pg_catalog.=)))",
+    );
+}
+
+#[test]
+fn exclude_rejected_in_non_postgres_dialects() {
+    // `GenericDialect` is intentionally excluded — it opts in to the
+    // Postgres EXCLUDE syntax alongside `PostgreSqlDialect`.
+    let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =))";
+    for dialect in
+        all_dialects_except(|d| d.is::<PostgreSqlDialect>() || 
d.is::<GenericDialect>()).dialects
+    {
+        let parser = TestedDialects::new(vec![dialect]);
+        assert!(
+            parser.parse_sql_statements(sql).is_err(),
+            "dialect unexpectedly accepted EXCLUDE: {sql}"
+        );
+    }
+}
+
+#[test]
+fn exclude_as_column_name_parses_in_mysql_and_sqlite() {
+    // `exclude` must remain usable as an identifier where it is not a
+    // reserved keyword; PG reserves it as a constraint keyword.

Review Comment:
   for both tests, after introducing the dialect method I think we can move 
them to common.rs



##########
src/ast/table_constraints.rs:
##########
@@ -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>

Review Comment:
   ```suggestion
   /// An `EXCLUDE` constraint.
   ///
   /// 
[PostgreSql](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-EXCLUDE)
   ```



##########
src/parser/mod.rs:
##########
@@ -508,10 +508,10 @@ impl<'a> Parser<'a> {
                 Token::EOF => break,
 
                 // end of statement
-                Token::Word(word) => {
-                    if expecting_statement_delimiter && word.keyword == 
Keyword::END {
-                        break;
-                    }
+                Token::Word(word)
+                    if expecting_statement_delimiter && word.keyword == 
Keyword::END =>

Review Comment:
   to get rid of these diffs, I think we might need to fetch the latest main 
(if they're from clippy)



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {

Review Comment:
   ```suggestion
       fn parse_exclude_constraint_element(&mut self) -> 
Result<ExclusionElement, ParserError> {
   ```



##########
src/ast/table_constraints.rs:
##########
@@ -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 {

Review Comment:
   ```suggestion
   pub enum ExcludeConstraintOperator {
   ```



##########
src/parser/mod.rs:
##########
@@ -9915,9 +9895,55 @@ impl<'a> Parser<'a> {
                     .into(),
                 ))
             }
+            Token::Word(w)
+                if w.keyword == Keyword::EXCLUDE
+                    && dialect_of!(self is PostgreSqlDialect | GenericDialect) 
=>
+            {
+                let index_method = if self.parse_keyword(Keyword::USING) {

Review Comment:
   can we move the impl to a `parse_exclude_constraint()` function or similar?



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].
+        // Shared with `CREATE INDEX` columns.

Review Comment:
   ```suggestion
   ```



##########
src/parser/mod.rs:
##########
@@ -9926,6 +9952,71 @@ impl<'a> Parser<'a> {
         }
     }
 
+    fn parse_exclusion_element(&mut self) -> Result<ExclusionElement, 
ParserError> {
+        // `index_elem` grammar: { col | (expr) } [ opclass ] [ ASC | DESC ] [ 
NULLS FIRST | LAST ].
+        // Shared with `CREATE INDEX` columns.
+        let (
+            OrderByExpr {
+                expr,
+                options: order,
+                ..
+            },
+            operator_class,
+        ) = self.parse_order_by_expr_inner(true)?;
+
+        self.expect_keyword_is(Keyword::WITH)?;
+        let operator = self.parse_exclusion_operator()?;
+
+        Ok(ExclusionElement {
+            expr,
+            operator_class,
+            order,
+            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.

Review Comment:
   ```suggestion
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to