You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/06/18 08:37:44 UTC
(datafusion-sqlparser-rs) 01/01: feat: add PostgreSQL EXCLUDE constraint parsing (#2307)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-2307-8cd2997dd7a85725ddc4025a3872c6ab4c158456
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 021e97df1ca96b49e759db3fad3b5508f08574f4
Author: Filipe Guerreiro <fi...@gmail.com>
AuthorDate: Thu Jun 18 17:37:19 2026 +0900
feat: add PostgreSQL EXCLUDE constraint parsing (#2307)
---
src/ast/mod.rs | 5 +-
src/ast/spans.rs | 17 ++++--
src/ast/table_constraints.rs | 115 ++++++++++++++++++++++++++++++++++++++-
src/dialect/generic.rs | 4 ++
src/dialect/mod.rs | 7 +++
src/dialect/postgresql.rs | 5 ++
src/parser/mod.rs | 125 ++++++++++++++++++++++++++++++++++++++-----
tests/sqlparser_postgres.rs | 124 +++++++++++++++++++++++++++++++++++++++++-
8 files changed, 381 insertions(+), 21 deletions(-)
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 23944c45..9dad8470 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -140,8 +140,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;
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index cd4034c7..7855ff15 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -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,
@@ -658,6 +658,7 @@ impl Spanned for TableConstraint {
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
TableConstraint::PrimaryKeyUsingIndex(constraint)
| TableConstraint::UniqueUsingIndex(constraint) => constraint.span(),
+ TableConstraint::Exclude(constraint) => constraint.span(),
}
}
}
@@ -727,6 +728,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 {
diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs
index 9ba196a8..463bf435 100644
--- a/src/ast/table_constraints.rs
+++ b/src/ast/table_constraints.rs
@@ -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};
@@ -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 {
@@ -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 {
@@ -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),
}
}
}
@@ -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)`.
+ PGOperator(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::PGOperator(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())),
+ )
+ }
+}
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index 7ee826c2..d408cb18 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -137,6 +137,10 @@ impl Dialect for GenericDialect {
true
}
+ fn supports_exclude_constraint(&self) -> bool {
+ true
+ }
+
fn supports_limit_comma(&self) -> bool {
true
}
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 4b791d8e..8a963cd4 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1188,6 +1188,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
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs
index 8b52ef6e..5dc2dbcf 100644
--- a/src/dialect/postgresql.rs
+++ b/src/dialect/postgresql.rs
@@ -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
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index d36e1adc..6540cdc0 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -3973,21 +3973,13 @@ 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.
// 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,
},
@@ -10185,9 +10177,26 @@ impl<'a> Parser<'a> {
.into(),
))
}
+ Token::Word(w)
+ if w.keyword == Keyword::EXCLUDE && self.dialect.supports_exclude_constraint() =>
+ {
+ // `EXCLUDE` is a non-reserved keyword in PostgreSQL, so it is a
+ // valid column name. Only treat it as an exclusion constraint
+ // when it begins one (named, or followed by `USING` / `(`);
+ // otherwise backtrack and let it parse as a column.
+ if name.is_some()
+ || self.peek_keyword(Keyword::USING)
+ || self.peek_token_ref().token == Token::LParen
+ {
+ Ok(Some(self.parse_exclude_constraint(name)?.into()))
+ } else {
+ self.prev_token();
+ Ok(None)
+ }
+ }
_ => {
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)
@@ -10196,6 +10205,98 @@ impl<'a> Parser<'a> {
}
}
+ // The leading `EXCLUDE` keyword is already consumed by the caller.
+ 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> {
+ 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.
+ fn parse_exclude_constraint_operator(
+ &mut self,
+ ) -> Result<ExcludeConstraintOperator, ParserError> {
+ if self.parse_keyword(Keyword::OPERATOR) {
+ return Ok(ExcludeConstraintOperator::PGOperator(
+ self.parse_pg_operator_ident_parts()?,
+ ));
+ }
+
+ let operator_token = self.next_token();
+ Ok(ExcludeConstraintOperator::Token(
+ operator_token.token.to_string(),
+ ))
+ }
+
+ /// Parse the body of a Postgres `OPERATOR(schema.op)` form: the
+ /// parenthesized `.`-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);
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index a61dd9af..aa8e37c0 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -24,7 +24,7 @@ mod test_utils;
use helpers::attached_token::AttachedToken;
use sqlparser::ast::*;
-use sqlparser::dialect::{GenericDialect, PostgreSqlDialect};
+use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
use sqlparser::parser::ParserError;
use sqlparser::tokenizer::Span;
use test_utils::*;
@@ -593,6 +593,92 @@ fn parse_create_table_constraints_only() {
};
}
+#[test]
+fn parse_exclude_constraint() {
+ let dialects = pg_and_generic();
+
+ let sql = "CREATE TABLE t (room INT, CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =))";
+ match dialects.verified_stmt(sql) {
+ Statement::CreateTable(create_table) => match &create_table.constraints[..] {
+ [TableConstraint::Exclude(c)] => {
+ assert_eq!(c.name, Some(Ident::new("no_overlap")));
+ assert_eq!(c.index_method, Some(Ident::new("gist")));
+ assert_eq!(c.elements.len(), 1);
+ assert_eq!(
+ c.elements[0].column.column.expr,
+ Expr::Identifier(Ident::new("room"))
+ );
+ assert_eq!(c.elements[0].operator.to_string(), "=");
+ assert!(c.elements[0].column.operator_class.is_none());
+ assert!(c.include.is_empty());
+ assert!(c.where_clause.is_none());
+ assert!(c.characteristics.is_none());
+ }
+ other => panic!("expected single Exclude constraint, got {other:?}"),
+ },
+ other => panic!("expected CreateTable, got {other:?}"),
+ }
+
+ for sql in [
+ "CREATE TABLE t (col INT, EXCLUDE (col WITH =))",
+ "CREATE TABLE t (room INT, during INT, EXCLUDE USING gist (room WITH =, during WITH &&))",
+ "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) INCLUDE (col))",
+ "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) WHERE (col > 0))",
+ "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) DEFERRABLE INITIALLY DEFERRED)",
+ "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) NOT DEFERRABLE INITIALLY IMMEDIATE)",
+ "CREATE TABLE t (col INT, EXCLUDE USING btree (col ASC NULLS LAST WITH =))",
+ "CREATE TABLE t (col INT, EXCLUDE USING btree (col DESC NULLS FIRST WITH =))",
+ "CREATE TABLE t (col TEXT, EXCLUDE USING gist (col text_pattern_ops WITH =))",
+ "CREATE TABLE t (name TEXT, EXCLUDE USING gist ((lower(name)) text_pattern_ops WITH =))",
+ "CREATE TABLE t (name TEXT, EXCLUDE USING btree (name COLLATE \"C\" WITH =))",
+ "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH OPERATOR(pg_catalog.=)))",
+ "CREATE TABLE t (col INT, CONSTRAINT c EXCLUDE USING gist (col ASC WITH OPERATOR(pg_catalog.=)))",
+ "CREATE TABLE t (CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =, during WITH &&) INCLUDE (id) WHERE (active = true))",
+ "ALTER TABLE t ADD CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =)",
+ ] {
+ dialects.verified_stmt(sql);
+ }
+
+ // Error cases: malformed EXCLUDE syntax must be rejected with a useful
+ // message rather than silently accepted.
+ for (sql, expected_message) in [
+ (
+ "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist (col))",
+ "Expected: WITH, found: )",
+ ),
+ (
+ "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist ())",
+ "Expected: an expression, found: )",
+ ),
+ (
+ "CREATE TABLE t (CONSTRAINT c EXCLUDE USING gist (col WITH))",
+ "Expected: ',' or ')' after column definition, found: EOF",
+ ),
+ (
+ "CREATE TABLE t (CONSTRAINT c EXCLUDE foo)",
+ "Expected: (, found: foo",
+ ),
+ ] {
+ let result = dialects.parse_sql_statements(sql);
+ assert_eq!(
+ ParserError::ParserError(expected_message.to_string()),
+ result.unwrap_err()
+ );
+ }
+
+ // Dialects that do not opt in via `supports_exclude_constraint` must
+ // refuse to parse `EXCLUDE` constraints.
+ let unsupported = all_dialects_where(|d| !d.supports_exclude_constraint());
+ let sql = "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =))";
+ for dialect in unsupported.dialects {
+ let parser = TestedDialects::new(vec![dialect]);
+ assert!(
+ parser.parse_sql_statements(sql).is_err(),
+ "dialect unexpectedly accepted EXCLUDE: {sql}"
+ );
+ }
+}
+
#[test]
fn parse_create_table_like_with_defaults() {
let sql = "CREATE TABLE new (LIKE old INCLUDING DEFAULTS)";
@@ -4234,6 +4320,15 @@ fn parse_custom_operator() {
);
}
+#[test]
+fn parse_operator_empty_parens_rejected() {
+ let result = pg_and_generic().parse_sql_statements("SELECT a OPERATOR() b");
+ assert_eq!(
+ ParserError::ParserError("Expected: operator name, found: )".to_string()),
+ result.unwrap_err()
+ );
+}
+
#[test]
fn parse_create_role() {
let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b";
@@ -9314,3 +9409,30 @@ fn parse_lock_table() {
}
}
}
+
+#[test]
+fn exclude_as_column_name() {
+ // `EXCLUDE` is a non-reserved keyword, so it stays usable as a column name
+ // even on dialects that parse `EXCLUDE` constraints: a bare `exclude` not
+ // followed by `USING` or `(` must not be mistaken for a constraint.
+ let sql = "CREATE TABLE t (exclude INT)";
+ for dialect in [
+ Box::new(MySqlDialect {}) as Box<dyn Dialect>,
+ Box::new(SQLiteDialect {}),
+ Box::new(PostgreSqlDialect {}),
+ Box::new(GenericDialect {}),
+ ] {
+ let type_name = format!("{dialect:?}");
+ let parser = TestedDialects::new(vec![dialect]);
+ let stmts = parser
+ .parse_sql_statements(sql)
+ .unwrap_or_else(|e| panic!("{type_name} failed to parse {sql}: {e}"));
+ match &stmts[0] {
+ Statement::CreateTable(create_table) => {
+ assert_eq!(create_table.columns.len(), 1);
+ assert_eq!(create_table.columns[0].name.value, "exclude");
+ }
+ other => panic!("{type_name}: expected CreateTable, got {other:?}"),
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org