diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a59519695..8a0fbca25 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -134,8 +134,8 @@ mod dml; pub mod helpers; pub mod table_constraints; pub use table_constraints::{ - CheckConstraint, ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint, - PrimaryKeyConstraint, TableConstraint, UniqueConstraint, + CheckConstraint, ConstraintUsingIndex, ForeignKeyConstraint, FullTextOrSpatialConstraint, + IndexConstraint, PrimaryKeyConstraint, TableConstraint, UniqueConstraint, }; mod operator; mod query; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 16a9a926f..a72ec85f3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -624,6 +624,7 @@ impl Spanned for TableConstraint { TableConstraint::Check(constraint) => constraint.span(), TableConstraint::Index(constraint) => constraint.span(), TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), + TableConstraint::ConstraintUsingIndex(constraint) => constraint.span(), } } } diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index cb3c2376d..0814fb04b 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -101,6 +101,14 @@ pub enum TableConstraint { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html FulltextOrSpatial(FullTextOrSpatialConstraint), + /// PostgreSQL [definition][1] for promoting an existing unique index to a + /// `PRIMARY KEY` or `UNIQUE` constraint: + /// + /// `[ CONSTRAINT constraint_name ] { UNIQUE | PRIMARY KEY } USING INDEX index_name + /// [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]` + /// + /// [1]: https://www.postgresql.org/docs/current/sql-altertable.html + ConstraintUsingIndex(ConstraintUsingIndex), } impl From for TableConstraint { @@ -139,6 +147,12 @@ impl From for TableConstraint { } } +impl From for TableConstraint { + fn from(constraint: ConstraintUsingIndex) -> Self { + TableConstraint::ConstraintUsingIndex(constraint) + } +} + impl fmt::Display for TableConstraint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -148,6 +162,7 @@ impl fmt::Display for TableConstraint { TableConstraint::Check(constraint) => constraint.fmt(f), TableConstraint::Index(constraint) => constraint.fmt(f), TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f), + TableConstraint::ConstraintUsingIndex(constraint) => constraint.fmt(f), } } } @@ -535,3 +550,58 @@ impl crate::ast::Spanned for UniqueConstraint { ) } } + +/// PostgreSQL constraint that promotes an existing unique index to a table constraint. +/// +/// `[ CONSTRAINT constraint_name ] { UNIQUE | PRIMARY KEY } USING INDEX index_name +/// [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]` +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ConstraintUsingIndex { + /// Optional constraint name. + pub name: Option, + /// Whether this is a `PRIMARY KEY` (true) or `UNIQUE` (false) constraint. + pub is_primary_key: bool, + /// The name of the existing unique index to promote. + pub index_name: Ident, + /// Optional characteristics like `DEFERRABLE`. + pub characteristics: Option, +} + +impl fmt::Display for ConstraintUsingIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::{display_constraint_name, display_option_spaced}; + write!( + f, + "{}{} USING INDEX {}", + display_constraint_name(&self.name), + if self.is_primary_key { + "PRIMARY KEY" + } else { + "UNIQUE" + }, + self.index_name, + )?; + write!(f, "{}", display_option_spaced(&self.characteristics))?; + Ok(()) + } +} + +impl crate::ast::Spanned for ConstraintUsingIndex { + fn span(&self) -> Span { + let start = self + .name + .as_ref() + .map(|i| i.span) + .unwrap_or(self.index_name.span); + let end = self + .characteristics + .as_ref() + .map(|c| c.span()) + .unwrap_or(self.index_name.span); + start.union(&end) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 14ddd2b50..9b229249c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9314,6 +9314,22 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::Word(w) if w.keyword == Keyword::UNIQUE => { + // PostgreSQL: UNIQUE USING INDEX index_name + // https://www.postgresql.org/docs/current/sql-altertable.html + if self.parse_keywords(&[Keyword::USING, Keyword::INDEX]) { + let index_name = self.parse_identifier()?; + let characteristics = self.parse_constraint_characteristics()?; + return Ok(Some( + ConstraintUsingIndex { + name, + is_primary_key: false, + index_name, + characteristics, + } + .into(), + )); + } + let index_type_display = self.parse_index_type_display(); if !dialect_of!(self is GenericDialect | MySqlDialect) && !index_type_display.is_none() @@ -9349,6 +9365,22 @@ impl<'a> Parser<'a> { // after `PRIMARY` always stay `KEY` self.expect_keyword_is(Keyword::KEY)?; + // PostgreSQL: PRIMARY KEY USING INDEX index_name + // https://www.postgresql.org/docs/current/sql-altertable.html + if self.parse_keywords(&[Keyword::USING, Keyword::INDEX]) { + let index_name = self.parse_identifier()?; + let characteristics = self.parse_constraint_characteristics()?; + return Ok(Some( + ConstraintUsingIndex { + name, + is_primary_key: true, + index_name, + characteristics, + } + .into(), + )); + } + // optional index name let index_name = self.parse_optional_ident()?; let index_type = self.parse_optional_using_then_index_type()?; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5853be7eb..419adadd4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -627,6 +627,46 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { pg_and_generic().verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE (c)"); } +#[test] +fn parse_alter_table_constraint_using_index() { + // PRIMARY KEY USING INDEX + // https://www.postgresql.org/docs/current/sql-altertable.html + let sql = "ALTER TABLE tab ADD CONSTRAINT c PRIMARY KEY USING INDEX my_index"; + match pg_and_generic().verified_stmt(sql) { + Statement::AlterTable(alter_table) => match &alter_table.operations[0] { + AlterTableOperation::AddConstraint { + constraint: TableConstraint::ConstraintUsingIndex(c), + .. + } => { + assert_eq!(c.name.as_ref().unwrap().to_string(), "c"); + assert!(c.is_primary_key); + assert_eq!(c.index_name.to_string(), "my_index"); + assert!(c.characteristics.is_none()); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + // UNIQUE USING INDEX + pg_and_generic().verified_stmt("ALTER TABLE tab ADD CONSTRAINT c UNIQUE USING INDEX my_index"); + + // Without constraint name + pg_and_generic().verified_stmt("ALTER TABLE tab ADD PRIMARY KEY USING INDEX my_index"); + pg_and_generic().verified_stmt("ALTER TABLE tab ADD UNIQUE USING INDEX my_index"); + + // With DEFERRABLE + pg_and_generic().verified_stmt( + "ALTER TABLE tab ADD CONSTRAINT c PRIMARY KEY USING INDEX my_index DEFERRABLE", + ); + pg_and_generic().verified_stmt( + "ALTER TABLE tab ADD CONSTRAINT c UNIQUE USING INDEX my_index NOT DEFERRABLE INITIALLY IMMEDIATE", + ); + pg_and_generic().verified_stmt( + "ALTER TABLE tab ADD CONSTRAINT c PRIMARY KEY USING INDEX my_index DEFERRABLE INITIALLY DEFERRED", + ); +} + #[test] fn parse_alter_table_disable() { pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE ROW LEVEL SECURITY");