From 91a677f5667516accf7737c550c8051aea4f4eff Mon Sep 17 00:00:00 2001 From: Guan-Ming Chiu Date: Mon, 9 Feb 2026 12:52:01 +0800 Subject: [PATCH 1/2] PostgreSQL: Support PRIMARY KEY/UNIQUE USING INDEX Signed-off-by: Guan-Ming Chiu --- src/ast/mod.rs | 4 +-- src/ast/spans.rs | 1 + src/ast/table_constraints.rs | 70 ++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 32 +++++++++++++++++ tests/sqlparser_postgres.rs | 40 +++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 601af1bd51..4481492bad 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -136,8 +136,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 f4bdf85a39..d32e8498f8 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -625,6 +625,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 cb3c2376d6..0814fb04bf 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 7a2bda8aca..eff4799a32 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9340,6 +9340,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() @@ -9375,6 +9391,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 f8c7381366..1310ee76ec 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"); From 483ddeb9775214ae64e815bdd38ea2201c901972 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:57:33 +0800 Subject: [PATCH 2/2] Split ConstraintUsingIndex into PrimaryKeyUsingIndex and UniqueUsingIndex variants --- src/ast/spans.rs | 3 ++- src/ast/table_constraints.rs | 36 ++++++++++++++---------------- src/parser/mod.rs | 43 ++++++++++++++++++------------------ tests/sqlparser_postgres.rs | 3 +-- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d32e8498f8..384cb0d013 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -625,7 +625,8 @@ impl Spanned for TableConstraint { TableConstraint::Check(constraint) => constraint.span(), TableConstraint::Index(constraint) => constraint.span(), TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), - TableConstraint::ConstraintUsingIndex(constraint) => constraint.span(), + TableConstraint::PrimaryKeyUsingIndex(constraint) + | TableConstraint::UniqueUsingIndex(constraint) => constraint.span(), } } } diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 0814fb04bf..9ba196a81e 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -102,13 +102,21 @@ pub enum TableConstraint { /// [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: + /// `PRIMARY KEY` constraint: /// - /// `[ CONSTRAINT constraint_name ] { UNIQUE | PRIMARY KEY } USING INDEX index_name + /// `[ CONSTRAINT constraint_name ] 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), + PrimaryKeyUsingIndex(ConstraintUsingIndex), + /// PostgreSQL [definition][1] for promoting an existing unique index to a + /// `UNIQUE` constraint: + /// + /// `[ CONSTRAINT constraint_name ] UNIQUE USING INDEX index_name + /// [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]` + /// + /// [1]: https://www.postgresql.org/docs/current/sql-altertable.html + UniqueUsingIndex(ConstraintUsingIndex), } impl From for TableConstraint { @@ -147,12 +155,6 @@ 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 { @@ -162,7 +164,8 @@ 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), + TableConstraint::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"), + TableConstraint::UniqueUsingIndex(c) => c.fmt_with_keyword(f, "UNIQUE"), } } } @@ -563,26 +566,21 @@ impl crate::ast::Spanned for UniqueConstraint { 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 { +impl ConstraintUsingIndex { + /// Format as `[CONSTRAINT name] USING INDEX index_name [characteristics]`. + pub fn fmt_with_keyword(&self, f: &mut fmt::Formatter, keyword: &str) -> 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" - }, + keyword, self.index_name, )?; write!(f, "{}", display_option_spaced(&self.characteristics))?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eff4799a32..a6c22b5750 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9276,6 +9276,21 @@ impl<'a> Parser<'a> { } } + /// Parse `index_name [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]` + /// after `{ PRIMARY KEY | UNIQUE } USING INDEX`. + fn parse_constraint_using_index( + &mut self, + name: Option, + ) -> Result { + let index_name = self.parse_identifier()?; + let characteristics = self.parse_constraint_characteristics()?; + Ok(ConstraintUsingIndex { + name, + index_name, + characteristics, + }) + } + /// Parse optional constraint characteristics such as `DEFERRABLE`, `INITIALLY` and `ENFORCED`. pub fn parse_constraint_characteristics( &mut self, @@ -9343,17 +9358,9 @@ impl<'a> Parser<'a> { // 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(), - )); + return Ok(Some(TableConstraint::UniqueUsingIndex( + self.parse_constraint_using_index(name)?, + ))); } let index_type_display = self.parse_index_type_display(); @@ -9394,17 +9401,9 @@ impl<'a> Parser<'a> { // 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(), - )); + return Ok(Some(TableConstraint::PrimaryKeyUsingIndex( + self.parse_constraint_using_index(name)?, + ))); } // optional index name diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1310ee76ec..d79e2b833e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -635,11 +635,10 @@ fn parse_alter_table_constraint_using_index() { match pg_and_generic().verified_stmt(sql) { Statement::AlterTable(alter_table) => match &alter_table.operations[0] { AlterTableOperation::AddConstraint { - constraint: TableConstraint::ConstraintUsingIndex(c), + constraint: TableConstraint::PrimaryKeyUsingIndex(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()); }