Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ impl Spanned for TableConstraint {
TableConstraint::Check(constraint) => constraint.span(),
TableConstraint::Index(constraint) => constraint.span(),
TableConstraint::FulltextOrSpatial(constraint) => constraint.span(),
TableConstraint::PrimaryKeyUsingIndex(constraint)
| TableConstraint::UniqueUsingIndex(constraint) => constraint.span(),
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions src/ast/table_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ 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` constraint:
///
/// `[ 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
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<UniqueConstraint> for TableConstraint {
Expand Down Expand Up @@ -148,6 +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::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"),
TableConstraint::UniqueUsingIndex(c) => c.fmt_with_keyword(f, "UNIQUE"),
}
}
}
Expand Down Expand Up @@ -535,3 +553,53 @@ 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 <https://www.postgresql.org/docs/current/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 struct ConstraintUsingIndex {
/// Optional constraint name.
pub name: Option<Ident>,
/// The name of the existing unique index to promote.
pub index_name: Ident,
/// Optional characteristics like `DEFERRABLE`.
pub characteristics: Option<ConstraintCharacteristics>,
}

impl ConstraintUsingIndex {
/// Format as `[CONSTRAINT name] <keyword> 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),
keyword,
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)
}
}
31 changes: 31 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ident>,
) -> Result<ConstraintUsingIndex, ParserError> {
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,
Expand Down Expand Up @@ -9340,6 +9355,14 @@ 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]) {
return Ok(Some(TableConstraint::UniqueUsingIndex(
self.parse_constraint_using_index(name)?,
)));
}

let index_type_display = self.parse_index_type_display();
if !dialect_of!(self is GenericDialect | MySqlDialect)
&& !index_type_display.is_none()
Expand Down Expand Up @@ -9375,6 +9398,14 @@ 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]) {
return Ok(Some(TableConstraint::PrimaryKeyUsingIndex(
self.parse_constraint_using_index(name)?,
)));
}
Comment on lines 9403 to 9407
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pull this out into a e.g. parse_constraint_using_index() function so that it can be reused across both cases?

Also repr wise, I think it would be clearer to have both cases as unique variants on the TableConstraint enum vs using the is_primary_key bool to differentiate. i.e.

TableConstraint::UniqueUsingIndex(ConstraintUsingIndex)
TableConstraint::PrimaryKeyUsingIndex(ConstraintUsingIndex)

Copy link
Member Author

@guan404ming guan404ming Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for the nice suggestions. I just updated with

  1. Split enum variant: TableConstraint::ConstraintUsingIndex into PrimaryKeyUsingIndex and UniqueUsingIndex, removing the is_primary_key bool from the struct.
  2. Extracted helper: Created parse_constraint_using_index() to deduplicate the parsing logic used by both UNIQUE USING INDEX and PRIMARY KEY USING INDEX.


// optional index name
let index_name = self.parse_optional_ident()?;
let index_type = self.parse_optional_using_then_index_type()?;
Expand Down
39 changes: 39 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,45 @@ 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::PrimaryKeyUsingIndex(c),
..
} => {
assert_eq!(c.name.as_ref().unwrap().to_string(), "c");
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");
Expand Down