From c746bea2ce8094f8fb3757892323c86508130b8f Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 27 Nov 2024 18:20:38 -0800 Subject: [PATCH 1/3] Support parsing optional nulls handling for unique constraint --- src/ast/ddl.rs | 30 +++++++++++++++++++++++++++++- src/ast/mod.rs | 9 +++++---- src/ast/spans.rs | 1 + src/parser/mod.rs | 17 +++++++++++++++++ tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 7 +++++++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 21a716d25..1655d8106 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -667,6 +667,8 @@ pub enum TableConstraint { columns: Vec, index_options: Vec, characteristics: Option, + /// Optional [Postgres] nulls handling: `[ NULLS [ NOT ] DISTINCT ]` + nulls_distinct: NullsDistinctOption, }, /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` @@ -775,10 +777,11 @@ impl fmt::Display for TableConstraint { columns, index_options, characteristics, + nulls_distinct, } => { write!( f, - "{}UNIQUE{index_type_display:>}{}{} ({})", + "{}UNIQUE{nulls_distinct}{index_type_display:>}{}{} ({})", display_constraint_name(name), display_option_spaced(index_name), display_option(" USING ", "", index_type), @@ -986,6 +989,31 @@ impl fmt::Display for IndexOption { } } +/// [Postgres] unique index nulls handling option: `[ NULLS [ NOT ] DISTINCT ]` +/// +/// [Postgres]: https://www.postgresql.org/docs/17/sql-altertable.html +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum NullsDistinctOption { + /// Not specified + None, + /// NULLS DISTINCT + Distinct, + /// NULLS NOT DISTINCT + NotDistinct, +} + +impl fmt::Display for NullsDistinctOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::None => Ok(()), + Self::Distinct => write!(f, " NULLS DISTINCT"), + Self::NotDistinct => write!(f, " NULLS NOT DISTINCT"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 366bf4d25..59d4e137d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -49,9 +49,10 @@ pub use self::ddl::{ ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, Deduplicate, DeferrableInitial, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, Owner, - Partition, ProcedureParam, ReferentialAction, TableConstraint, TagsColumnOption, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + IdentityPropertyKind, IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, TableConstraint, + TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, + ViewColumnDef, }; pub use self::dml::{CreateIndex, CreateTable, Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -885,7 +886,7 @@ pub enum Expr { /// Example: /// /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') /// ``` /// diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8e8c7b14a..c0296834a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -557,6 +557,7 @@ impl Spanned for TableConstraint { columns, index_options: _, characteristics, + nulls_distinct: _, } => union_spans( name.iter() .map(|i| i.span) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b7f5cb866..3c49458f9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6728,6 +6728,8 @@ impl<'a> Parser<'a> { .expected("`index_name` or `(column_name [, ...])`", self.peek_token()); } + let nulls_distinct = self.parse_optional_nulls_distinct()?; + // optional index name let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; @@ -6743,6 +6745,7 @@ impl<'a> Parser<'a> { columns, index_options, characteristics, + nulls_distinct, })) } Token::Word(w) if w.keyword == Keyword::PRIMARY => { @@ -6865,6 +6868,20 @@ impl<'a> Parser<'a> { } } + fn parse_optional_nulls_distinct(&mut self) -> Result { + Ok(if self.parse_keyword(Keyword::NULLS) { + let not = self.parse_keyword(Keyword::NOT); + self.expect_keyword(Keyword::DISTINCT)?; + if not { + NullsDistinctOption::NotDistinct + } else { + NullsDistinctOption::Distinct + } + } else { + NullsDistinctOption::None + }) + } + pub fn maybe_parse_options( &mut self, keyword: Keyword, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 943a61718..3b5bd148c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -669,6 +669,7 @@ fn table_constraint_unique_primary_ctor( columns, index_options, characteristics, + nulls_distinct: NullsDistinctOption::None, }, None => TableConstraint::PrimaryKey { name, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 54f77b7be..039cb2583 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -594,6 +594,13 @@ fn parse_alter_table_constraints_rename() { } } +#[test] +fn parse_alter_table_constraints_unique_nulls_distinct() { + pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)"); + pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS DISTINCT (c)"); + pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE (c)"); +} + #[test] fn parse_alter_table_disable() { pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE ROW LEVEL SECURITY"); From a54331889874ca896100b6666720d6fcb43b6965 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Wed, 27 Nov 2024 18:25:59 -0800 Subject: [PATCH 2/3] Remove absent doc comment link --- src/ast/ddl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1655d8106..3705614fc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -667,7 +667,7 @@ pub enum TableConstraint { columns: Vec, index_options: Vec, characteristics: Option, - /// Optional [Postgres] nulls handling: `[ NULLS [ NOT ] DISTINCT ]` + /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` nulls_distinct: NullsDistinctOption, }, /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ From c8e53efdd3c6c8acb75f229e299cf333004b26de Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Mon, 2 Dec 2024 10:34:28 -0800 Subject: [PATCH 3/3] Add AST inpection test --- tests/sqlparser_postgres.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 039cb2583..ec1717816 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -596,7 +596,19 @@ fn parse_alter_table_constraints_rename() { #[test] fn parse_alter_table_constraints_unique_nulls_distinct() { - pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)"); + match pg_and_generic() + .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") + { + Statement::AlterTable { operations, .. } => match &operations[0] { + AlterTableOperation::AddConstraint(TableConstraint::Unique { + nulls_distinct, .. + }) => { + assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct) + } + _ => unreachable!(), + }, + _ => unreachable!(), + } pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS DISTINCT (c)"); pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE (c)"); }