Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
}
Expand Down
70 changes: 70 additions & 0 deletions src/ast/table_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UniqueConstraint> for TableConstraint {
Expand Down Expand Up @@ -139,6 +147,12 @@ impl From<FullTextOrSpatialConstraint> for TableConstraint {
}
}

impl From<ConstraintUsingIndex> 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 {
Expand All @@ -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),
}
}
}
Expand Down Expand Up @@ -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 <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>,
/// 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<ConstraintCharacteristics>,
}

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)
}
}
32 changes: 32 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
));
}
Comment on lines +9403 to +9407
Copy link
Copy Markdown
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
Copy Markdown
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
40 changes: 40 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading