From 068cb08bc8ceac5341beba07c23f5ee74c1a4a21 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:37:21 +0300 Subject: [PATCH 01/17] Add `TableConstraint` and `ConstraintCreateStatement` as constraint definition --- src/constraint/common.rs | 133 ++++++++++++++++++++++++++++++++ src/constraint/create.rs | 160 +++++++++++++++++++++++++++++++++++++++ src/constraint/drop.rs | 0 src/constraint/mod.rs | 38 ++++++++++ 4 files changed, 331 insertions(+) create mode 100644 src/constraint/common.rs create mode 100644 src/constraint/create.rs create mode 100644 src/constraint/drop.rs create mode 100644 src/constraint/mod.rs diff --git a/src/constraint/common.rs b/src/constraint/common.rs new file mode 100644 index 000000000..3c11793e3 --- /dev/null +++ b/src/constraint/common.rs @@ -0,0 +1,133 @@ +use crate::{Check, ConditionHolder, IndexType, IntoIndexColumn, TableIndex, types::*}; + +/// Specification of a constraint +#[derive(Default, Debug, Clone)] +pub struct TableConstraint { + pub(crate) name: Option, + pub(crate) index: TableIndex, + pub(crate) constraint_type: Option, + pub(crate) nulls_not_distinct: bool, + pub(crate) index_type: Option, + pub(crate) using_index: Option, + pub(crate) r#where: ConditionHolder, + pub(crate) include_columns: Vec, +} + +impl TableConstraint { + /// Construct a new constraint + pub fn new() -> Self { + Self::default() + } + + /// Set constraint name + pub fn constraint_name(&mut self, name: T) -> &mut Self + where + T: Into, + { + self.name = Some(name.into()); + self + } + + /// Set index name + pub fn index_name(&mut self, name: T) -> &mut Self + where + T: Into, + { + self.index.name(name); + self + } + + /// Add constraint column + pub fn col(&mut self, col: C) -> &mut Self + where + C: IntoIndexColumn, + { + self.index.col(col.into_index_column()); + self + } + + /// Set constraint as primary key + pub fn primary(&mut self) -> &mut Self { + self.constraint_type = Some(ConstraintCreateStatementType::PrimaryKey); + self + } + + /// Set constraint as unique + pub fn unique(&mut self) -> &mut Self { + self.constraint_type = Some(ConstraintCreateStatementType::Unique); + self + } + + /// Set constraint as check + pub fn check(&mut self, check: T) -> &mut Self + where + T: Into, + { + self.constraint_type = Some(ConstraintCreateStatementType::Check(check.into())); + self + } + + /// Set nulls to not be treated as distinct values. Only available on Postgres. + pub fn nulls_not_distinct(&mut self) -> &mut Self { + self.nulls_not_distinct = true; + self + } + + /// Set index as full text. Only available on MySQL. + pub fn full_text(&mut self) -> &mut Self { + self.index_type(IndexType::FullText) + } + + /// Set index type. Only available on MySQL. + pub fn index_type(&mut self, index_type: IndexType) -> &mut Self { + self.index_type = Some(index_type); + self + } + + /// Set index type. Only available on MySQL. + pub fn using_index(&mut self, using_index: T) -> &mut Self + where + T: IntoIden, + { + self.using_index = Some(using_index.into_iden()); + self + } + + /// Add include column. Only available on Postgres. + pub fn include(&mut self, col: C) -> &mut Self + where + C: IntoIden, + { + self.include_columns.push(col.into_iden()); + self + } + + pub fn is_nulls_not_distinct(&self) -> bool { + self.nulls_not_distinct + } + + pub fn get_index_spec(&self) -> &TableIndex { + &self.index + } + + pub fn take(&mut self) -> Self { + Self { + name: self.name.take(), + index: self.index.take(), + constraint_type: self.constraint_type.take(), + nulls_not_distinct: self.nulls_not_distinct, + index_type: self.index_type.take(), + using_index: self.using_index.take(), + r#where: self.r#where.clone(), + include_columns: self.include_columns.clone(), + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub(crate) enum ConstraintCreateStatementType { + Check(Check), + Unique, + PrimaryKey, +} diff --git a/src/constraint/create.rs b/src/constraint/create.rs new file mode 100644 index 000000000..7e612edc2 --- /dev/null +++ b/src/constraint/create.rs @@ -0,0 +1,160 @@ +use inherent::inherent; + +use crate::{ + Check, ConditionalStatement, IndexType, IntoCondition, IntoIndexColumn, TableConstraint, + TableIndex, +}; +use crate::{SchemaStatementBuilder, backend::SchemaBuilder, types::*}; + +#[derive(Default, Debug, Clone)] +pub struct ConstraintCreateStatement { + pub(crate) table: Option, + pub(crate) constraint: TableConstraint, +} + +impl ConstraintCreateStatement { + /// Construct a new [`ConstraintCreateStatement`] + pub fn new() -> Self { + Self::default() + } + + /// Set constraint name + pub fn constraint_name(&mut self, name: T) -> &mut Self + where + T: Into, + { + self.constraint.constraint_name(name); + self + } + + /// Set index name. Only available on MySQL. + pub fn index_name(&mut self, name: T) -> &mut Self + where + T: Into, + { + self.constraint.index_name(name); + self + } + + /// Set target table + pub fn table(&mut self, table: T) -> &mut Self + where + T: IntoTableRef, + { + self.table = Some(table.into_table_ref()); + self + } + + /// Add constraint column + pub fn col(&mut self, col: C) -> &mut Self + where + C: IntoIndexColumn, + { + self.constraint.col(col); + self + } + + /// Set constraint as primary key + pub fn primary(&mut self) -> &mut Self { + self.constraint.primary(); + self + } + + /// Set constraint as unique + pub fn unique(&mut self) -> &mut Self { + self.constraint.unique(); + self + } + + /// Set constraint as check + pub fn check(&mut self, check: T) -> &mut Self + where + T: Into, + { + self.constraint.check(check); + self + } + + /// Set nulls to not be treated as distinct values. Only available on Postgres. + pub fn nulls_not_distinct(&mut self) -> &mut Self { + self.constraint.nulls_not_distinct(); + self + } + + /// Set index as full text. Only available on MySQL. + pub fn full_text(&mut self) -> &mut Self { + self.index_type(IndexType::FullText) + } + + /// Set index type. Only available on MySQL. + pub fn index_type(&mut self, index_type: IndexType) -> &mut Self { + self.constraint.index_type(index_type); + self + } + + /// Set index type. Only available on MySQL. + pub fn using_index(&mut self, using_index: T) -> &mut Self + where + T: IntoIden, + { + self.constraint.using_index(using_index); + self + } + + /// Add include column. Only available on Postgres. + pub fn include(&mut self, col: C) -> &mut Self + where + C: IntoIden, + { + self.constraint.include(col); + self + } + + pub fn is_nulls_not_distinct(&self) -> bool { + self.constraint.is_nulls_not_distinct() + } + + pub fn get_index_spec(&self) -> &TableIndex { + self.constraint.get_index_spec() + } + + pub fn take(&mut self) -> Self { + Self { + table: self.table.take(), + constraint: self.constraint.take(), + } + } +} + +#[inherent] +impl SchemaStatementBuilder for ConstraintCreateStatement { + pub fn build(&self, schema_builder: T) -> String + where + T: SchemaBuilder, + { + let mut sql = String::with_capacity(256); + schema_builder.prepare_constraint_create_statement(self, &mut sql); + sql + } + + pub fn to_string(&self, schema_builder: T) -> String + where + T: SchemaBuilder; +} + +impl ConditionalStatement for ConstraintCreateStatement { + fn and_or_where(&mut self, condition: LogicalChainOper) -> &mut Self { + self.constraint.r#where.add_and_or(condition); + self + } + + fn cond_where(&mut self, condition: C) -> &mut Self + where + C: IntoCondition, + { + self.constraint + .r#where + .add_condition(condition.into_condition()); + self + } +} diff --git a/src/constraint/drop.rs b/src/constraint/drop.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/constraint/mod.rs b/src/constraint/mod.rs new file mode 100644 index 000000000..837382605 --- /dev/null +++ b/src/constraint/mod.rs @@ -0,0 +1,38 @@ +//! Constraint definition +//! +//! # Usage +//! +//! - Table Constraint Create, see [`ConstraintCreateStatement`] +//! - Table Constraint Drop, see [`ConstraintDropStatement`] + +mod common; +mod create; +mod drop; + +pub use common::*; +pub use create::*; +pub use drop::*; + +/// Shorthand for constructing constraint statement +#[derive(Debug, Clone)] +pub struct Constraint; + +/// All available types of index statement +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ConstraintStatement { + Create(ConstraintCreateStatement), + /* Drop(ConstraintDropStatement), */ +} + +impl Constraint { + /// Construct constraint [`ConstraintCreateStatement`] + pub fn create() -> ConstraintCreateStatement { + ConstraintCreateStatement::new() + } + + /* /// Construct constraint [`ConstraintDropStatement`] + pub fn drop() -> ConstraintDropStatement { + ConstraintDropStatement::new() + } */ +} From 096c445acf55288466d1cf75567fcf4f82b4bb10 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:39:22 +0300 Subject: [PATCH 02/17] Add `ConstraintBuilder` and `ConstraintMode` types to build constraint into SQL --- src/backend/constraint_builder.rs | 28 ++++++++++++++++++++++++++++ src/backend/mod.rs | 7 ++++++- src/backend/postgres/index.rs | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/backend/constraint_builder.rs diff --git a/src/backend/constraint_builder.rs b/src/backend/constraint_builder.rs new file mode 100644 index 000000000..973be4848 --- /dev/null +++ b/src/backend/constraint_builder.rs @@ -0,0 +1,28 @@ +use crate::*; + +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConstraintMode { + Alter, + TableAlter, +} + +pub trait ConstraintBuilder: IndexBuilder { + /// Translate [`ConstraintCreateStatement`] into SQL statement. + fn prepare_constraint_create_statement( + &self, + create: &ConstraintCreateStatement, + sql: &mut impl SqlWriter, + ) { + self.prepare_constraint_create_statement_internal(create, sql, ConstraintMode::Alter) + } + + #[doc(hidden)] + /// Internal function to factor constraint with alter and without + fn prepare_constraint_create_statement_internal( + &self, + create: &ConstraintCreateStatement, + sql: &mut impl SqlWriter, + mode: ConstraintMode, + ); +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 21d91030b..1c816d7f6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -20,12 +20,14 @@ pub use postgres::*; #[cfg(feature = "backend-sqlite")] pub use sqlite::*; +mod constraint_builder; mod foreign_key_builder; mod index_builder; mod query_builder; mod table_builder; mod table_ref_builder; +pub use self::constraint_builder::*; pub use self::foreign_key_builder::*; pub use self::index_builder::*; pub use self::query_builder::*; @@ -34,7 +36,10 @@ pub use self::table_ref_builder::*; pub trait GenericBuilder: QueryBuilder + SchemaBuilder {} -pub trait SchemaBuilder: TableBuilder + IndexBuilder + ForeignKeyBuilder {} +pub trait SchemaBuilder: + TableBuilder + IndexBuilder + ForeignKeyBuilder + ConstraintBuilder +{ +} pub trait QuotedBuilder { /// The type of quote the builder uses. diff --git a/src/backend/postgres/index.rs b/src/backend/postgres/index.rs index a025b7592..c7f66e3c0 100644 --- a/src/backend/postgres/index.rs +++ b/src/backend/postgres/index.rs @@ -184,7 +184,7 @@ impl IndexBuilder for PostgresQueryBuilder { } impl PostgresQueryBuilder { - fn prepare_include_columns(&self, columns: &[DynIden], sql: &mut impl SqlWriter) { + pub(crate) fn prepare_include_columns(&self, columns: &[DynIden], sql: &mut impl SqlWriter) { sql.write_str("INCLUDE (").unwrap(); let mut cols = columns.iter(); From 80aa31e7b80617f0831e01a58aaaf8d6e2d5d222 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:45:38 +0300 Subject: [PATCH 03/17] Add `AddConstraint` variant to `TableAlterOption` and some test for `Table::alter` --- src/backend/mysql/table.rs | 10 ++++++++++ src/backend/postgres/table.rs | 10 ++++++++++ src/backend/sqlite/table.rs | 2 ++ src/lib.rs | 2 ++ src/table/alter.rs | 9 +++++++-- tests/mysql/table.rs | 12 ++++++++++++ tests/postgres/table.rs | 21 +++++++++++++++++++++ tests/sqlite/table.rs | 9 +++++++++ 8 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/backend/mysql/table.rs b/src/backend/mysql/table.rs index 832c64bf9..6b3435ac1 100644 --- a/src/backend/mysql/table.rs +++ b/src/backend/mysql/table.rs @@ -220,6 +220,16 @@ impl TableBuilder for MysqlQueryBuilder { TableAlterOption::DropConstraint(name) => { sql.write_str("DROP CONSTRAINT ").unwrap(); self.prepare_iden(name, sql); + TableAlterOption::AddConstraint(constraint) => { + let create = ConstraintCreateStatement { + constraint: constraint.to_owned(), + table: None, + }; + self.prepare_constraint_create_statement_internal( + &create, + sql, + ConstraintMode::TableAlter, + ); } }; } diff --git a/src/backend/postgres/table.rs b/src/backend/postgres/table.rs index 015672dff..2b9a806b0 100644 --- a/src/backend/postgres/table.rs +++ b/src/backend/postgres/table.rs @@ -206,6 +206,16 @@ impl TableBuilder for PostgresQueryBuilder { TableAlterOption::DropConstraint(name) => { sql.write_str("DROP CONSTRAINT ").unwrap(); self.prepare_iden(name, sql); + TableAlterOption::AddConstraint(constraint) => { + let create = ConstraintCreateStatement { + constraint: constraint.to_owned(), + table: None, + }; + self.prepare_constraint_create_statement_internal( + &create, + sql, + ConstraintMode::TableAlter, + ); } } } diff --git a/src/backend/sqlite/table.rs b/src/backend/sqlite/table.rs index 798b1d50c..2512e0d0e 100644 --- a/src/backend/sqlite/table.rs +++ b/src/backend/sqlite/table.rs @@ -78,6 +78,8 @@ impl TableBuilder for SqliteQueryBuilder { } TableAlterOption::DropConstraint(_) => { panic!("Sqlite does not support dropping constraints from existing tables"); + TableAlterOption::AddConstraint(_) => { + panic!("Sqlite does not support modification of constraints to existing tables"); } } } diff --git a/src/lib.rs b/src/lib.rs index 8867dab02..54a217c23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -974,6 +974,7 @@ #[cfg(feature = "audit")] pub mod audit; pub mod backend; +pub mod constraint; pub mod error; pub mod expr; pub mod extension; @@ -996,6 +997,7 @@ pub mod value; pub mod tests_cfg; pub use backend::*; +pub use constraint::*; pub use expr::*; pub use foreign_key::*; pub use func::*; diff --git a/src/table/alter.rs b/src/table/alter.rs index 04b7a7424..d8eaa78c9 100644 --- a/src/table/alter.rs +++ b/src/table/alter.rs @@ -1,6 +1,6 @@ use crate::{ - ColumnDef, IntoColumnDef, SchemaStatementBuilder, TableForeignKey, backend::SchemaBuilder, - types::*, + ColumnDef, IntoColumnDef, SchemaStatementBuilder, TableConstraint, TableForeignKey, + backend::SchemaBuilder, types::*, }; use inherent::inherent; @@ -61,6 +61,7 @@ pub enum TableAlterOption { AddForeignKey(TableForeignKey), DropForeignKey(DynIden), DropConstraint(DynIden), + AddConstraint(TableConstraint), } impl TableAlterStatement { @@ -421,6 +422,10 @@ impl TableAlterStatement { T: IntoIden, { self.add_alter_option(TableAlterOption::DropConstraint(name.into_iden())) + + /// Add a constraint to existing table + pub fn add_constraint(&mut self, constraint: &TableConstraint) -> &mut Self { + self.add_alter_option(TableAlterOption::AddConstraint(constraint.to_owned())) } fn add_alter_option(&mut self, alter_option: TableAlterOption) -> &mut Self { diff --git a/tests/mysql/table.rs b/tests/mysql/table.rs index f44148326..1ae60fb64 100644 --- a/tests/mysql/table.rs +++ b/tests/mysql/table.rs @@ -412,6 +412,18 @@ fn alter_7() { ); } +#[test] +fn alter_8() { + // https://dbfiddle.uk/4YIIpn-G + assert_eq!( + Table::alter() + .table(Font::Table) + .add_constraint(&TableConstraint::new().primary().col(Font::Id)) + .to_string(MysqlQueryBuilder), + [r#"ALTER TABLE `font`"#, r#"ADD PRIMARY KEY (`id`)"#,].join(" ") + ); +} + #[test] fn create_with_check_constraint() { assert_eq!( diff --git a/tests/postgres/table.rs b/tests/postgres/table.rs index 15e6fa9f1..8aa6f6c42 100644 --- a/tests/postgres/table.rs +++ b/tests/postgres/table.rs @@ -521,6 +521,27 @@ fn alter_10() { ); } +#[test] +fn alter_11() { + // https://dbfiddle.uk/jxh_KhXE + assert_eq!( + Table::alter() + .table(Font::Table) + .add_constraint( + &TableConstraint::new() + .primary() + .constraint_name("PK_2e303c3a712662f1fc2a4d0aad6") + .col(Font::Id) + ) + .to_string(PostgresQueryBuilder), + [ + r#"ALTER TABLE "font""#, + r#"ADD CONSTRAINT "PK_2e303c3a712662f1fc2a4d0aad6" PRIMARY KEY ("id")"#, + ] + .join(" ") + ); +} + #[test] fn rename_1() { assert_eq!( diff --git a/tests/sqlite/table.rs b/tests/sqlite/table.rs index 564034b46..dbd3b68d2 100644 --- a/tests/sqlite/table.rs +++ b/tests/sqlite/table.rs @@ -677,6 +677,15 @@ fn alter_7() { .to_string(SqliteQueryBuilder); } +#[test] +#[should_panic(expected = "Sqlite does not support modification of constraints to existing tables")] +fn alter_8() { + let _ = Table::alter() + .table(Font::Table) + .add_constraint(&TableConstraint::new()) + .to_string(SqliteQueryBuilder); +} + #[test] fn create_with_check_constraint() { assert_eq!( From 2f040be7c4fc0b34f4b36c3beeb2d452b703a157 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:47:48 +0300 Subject: [PATCH 04/17] Impl `ConstraintBuilder` for `MysqlQueryBuilder` --- src/backend/mysql/constraint.rs | 63 +++++++++++++++++++++++++++++++++ src/backend/mysql/mod.rs | 1 + 2 files changed, 64 insertions(+) create mode 100644 src/backend/mysql/constraint.rs diff --git a/src/backend/mysql/constraint.rs b/src/backend/mysql/constraint.rs new file mode 100644 index 000000000..e90588ba6 --- /dev/null +++ b/src/backend/mysql/constraint.rs @@ -0,0 +1,63 @@ +use super::*; + +impl ConstraintBuilder for MysqlQueryBuilder { + fn prepare_constraint_create_statement_internal( + &self, + create: &ConstraintCreateStatement, + sql: &mut impl SqlWriter, + mode: ConstraintMode, + ) { + let Some(constraint_type) = &create.constraint.constraint_type else { + panic!("No constraint type found"); + }; + + if mode == ConstraintMode::Alter { + sql.write_str("ALTER TABLE ").unwrap(); + if let Some(table) = &create.table { + self.prepare_table_ref_table_stmt(table, sql); + sql.write_str(" ").unwrap(); + } + } + + sql.write_str("ADD ").unwrap(); + + match constraint_type { + ConstraintCreateStatementType::Check(check) => { + self.prepare_check_constraint(check, sql) + } + value => { + if let Some(constraint_name) = &create.constraint.name { + sql.write_str("CONSTRAINT ").unwrap(); + sql.write_char(self.quote().left()).unwrap(); + sql.write_str(constraint_name).unwrap(); + sql.write_char(self.quote().right()).unwrap(); + sql.write_str(" ").unwrap(); + } + + match value { + ConstraintCreateStatementType::PrimaryKey => { + sql.write_str("PRIMARY KEY ").unwrap() + } + ConstraintCreateStatementType::Unique => { + sql.write_str("UNIQUE KEY ").unwrap(); + } + _ => unreachable!(), + } + + if let Some(index_name) = &create.constraint.index.name { + sql.write_char(self.quote().left()).unwrap(); + sql.write_str(index_name).unwrap(); + sql.write_char(self.quote().right()).unwrap(); + sql.write_str(" ").unwrap(); + } + + self.prepare_index_type(&create.constraint.index_type, sql); + if matches!(create.constraint.index_type, Some(IndexType::FullText)) { + sql.write_str(" ").unwrap(); + } + + self.prepare_index_columns(&create.constraint.index.columns, sql); + } + } + } +} diff --git a/src/backend/mysql/mod.rs b/src/backend/mysql/mod.rs index dc699f1f0..04833b1e8 100644 --- a/src/backend/mysql/mod.rs +++ b/src/backend/mysql/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod constraint; pub(crate) mod foreign_key; pub(crate) mod index; pub(crate) mod query; From a4dda5175d2ad12a0825553d354adbcb0ab58768 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:47:57 +0300 Subject: [PATCH 05/17] Impl `ConstraintBuilder` for `PostgresQueryBuilder` --- src/backend/postgres/constraint.rs | 69 ++++++++++++++++++++++++++++++ src/backend/postgres/mod.rs | 1 + 2 files changed, 70 insertions(+) create mode 100644 src/backend/postgres/constraint.rs diff --git a/src/backend/postgres/constraint.rs b/src/backend/postgres/constraint.rs new file mode 100644 index 000000000..4c882d2af --- /dev/null +++ b/src/backend/postgres/constraint.rs @@ -0,0 +1,69 @@ +use super::*; + +impl ConstraintBuilder for PostgresQueryBuilder { + fn prepare_constraint_create_statement_internal( + &self, + create: &ConstraintCreateStatement, + sql: &mut impl SqlWriter, + mode: ConstraintMode, + ) { + let Some(constraint_type) = &create.constraint.constraint_type else { + panic!("No constraint type found"); + }; + + if mode == ConstraintMode::Alter { + sql.write_str("ALTER TABLE ").unwrap(); + if let Some(table) = &create.table { + self.prepare_table_ref_table_stmt(table, sql); + sql.write_str(" ").unwrap(); + } + } + + sql.write_str("ADD ").unwrap(); + + match constraint_type { + ConstraintCreateStatementType::Check(check) => { + self.prepare_check_constraint(check, sql) + } + value => { + if let Some(name) = &create.constraint.name { + sql.write_str("CONSTRAINT ").unwrap(); + sql.write_char(self.quote().left()).unwrap(); + sql.write_str(name).unwrap(); + sql.write_char(self.quote().right()).unwrap(); + sql.write_str(" ").unwrap(); + } + + match value { + ConstraintCreateStatementType::PrimaryKey => { + sql.write_str("PRIMARY KEY ").unwrap() + } + ConstraintCreateStatementType::Unique => { + sql.write_str("UNIQUE ").unwrap(); + } + _ => unreachable!(), + } + + if let Some(using_index) = &create.constraint.using_index { + sql.write_str("USING INDEX ").unwrap(); + self.prepare_iden(using_index, sql); + sql.write_str(" ").unwrap(); + } + + if create.constraint.nulls_not_distinct { + sql.write_str("NULLS NOT DISTINCT ").unwrap(); + } + + self.prepare_index_columns(&create.constraint.index.columns, sql); + + if !create.constraint.include_columns.is_empty() { + sql.write_str(" ").unwrap(); + self.prepare_include_columns(&create.constraint.include_columns, sql); + } + + // Used only with `EXCLUDE` constraint + // self.prepare_filter(&create.r#where, sql); + } + }; + } +} diff --git a/src/backend/postgres/mod.rs b/src/backend/postgres/mod.rs index 8e775c73c..2a773c694 100644 --- a/src/backend/postgres/mod.rs +++ b/src/backend/postgres/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod constraint; pub(crate) mod extension; pub(crate) mod foreign_key; pub(crate) mod index; From fb17635d560b7e70d01c2e135845d29cc9d5d1db Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:48:07 +0300 Subject: [PATCH 06/17] Impl `ConstraintBuilder` for `SqliteQueryBuilder` --- src/backend/sqlite/constraint.rs | 12 ++++++++++++ src/backend/sqlite/mod.rs | 1 + 2 files changed, 13 insertions(+) create mode 100644 src/backend/sqlite/constraint.rs diff --git a/src/backend/sqlite/constraint.rs b/src/backend/sqlite/constraint.rs new file mode 100644 index 000000000..20ec4019c --- /dev/null +++ b/src/backend/sqlite/constraint.rs @@ -0,0 +1,12 @@ +use super::*; + +impl ConstraintBuilder for SqliteQueryBuilder { + fn prepare_constraint_create_statement_internal( + &self, + _create: &ConstraintCreateStatement, + _sql: &mut impl SqlWriter, + _mode: ConstraintMode, + ) { + panic!("Sqlite does not support modification of constraints to existing tables"); + } +} diff --git a/src/backend/sqlite/mod.rs b/src/backend/sqlite/mod.rs index 71dbba1b8..118049248 100644 --- a/src/backend/sqlite/mod.rs +++ b/src/backend/sqlite/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod constraint; pub(crate) mod foreign_key; pub(crate) mod index; pub(crate) mod query; From c1058528c21742c576e39d9084620b290e3fcde7 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:48:45 +0300 Subject: [PATCH 07/17] Add small constraint build tests --- src/backend/mysql/table.rs | 1 + src/backend/postgres/table.rs | 1 + src/backend/sqlite/table.rs | 1 + src/table/alter.rs | 3 +- tests/mysql/constraint.rs | 76 +++++++++++++++++++++++++++++++++++ tests/mysql/mod.rs | 1 + tests/postgres/constraint.rs | 58 ++++++++++++++++++++++++++ tests/postgres/mod.rs | 1 + tests/sqlite/constraint.rs | 1 + tests/sqlite/mod.rs | 1 + 10 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 tests/mysql/constraint.rs create mode 100644 tests/postgres/constraint.rs create mode 100644 tests/sqlite/constraint.rs diff --git a/src/backend/mysql/table.rs b/src/backend/mysql/table.rs index 6b3435ac1..0c358d606 100644 --- a/src/backend/mysql/table.rs +++ b/src/backend/mysql/table.rs @@ -220,6 +220,7 @@ impl TableBuilder for MysqlQueryBuilder { TableAlterOption::DropConstraint(name) => { sql.write_str("DROP CONSTRAINT ").unwrap(); self.prepare_iden(name, sql); + } TableAlterOption::AddConstraint(constraint) => { let create = ConstraintCreateStatement { constraint: constraint.to_owned(), diff --git a/src/backend/postgres/table.rs b/src/backend/postgres/table.rs index 2b9a806b0..43e3962e9 100644 --- a/src/backend/postgres/table.rs +++ b/src/backend/postgres/table.rs @@ -206,6 +206,7 @@ impl TableBuilder for PostgresQueryBuilder { TableAlterOption::DropConstraint(name) => { sql.write_str("DROP CONSTRAINT ").unwrap(); self.prepare_iden(name, sql); + } TableAlterOption::AddConstraint(constraint) => { let create = ConstraintCreateStatement { constraint: constraint.to_owned(), diff --git a/src/backend/sqlite/table.rs b/src/backend/sqlite/table.rs index 2512e0d0e..4f0462c69 100644 --- a/src/backend/sqlite/table.rs +++ b/src/backend/sqlite/table.rs @@ -78,6 +78,7 @@ impl TableBuilder for SqliteQueryBuilder { } TableAlterOption::DropConstraint(_) => { panic!("Sqlite does not support dropping constraints from existing tables"); + } TableAlterOption::AddConstraint(_) => { panic!("Sqlite does not support modification of constraints to existing tables"); } diff --git a/src/table/alter.rs b/src/table/alter.rs index d8eaa78c9..8d94f5c95 100644 --- a/src/table/alter.rs +++ b/src/table/alter.rs @@ -422,7 +422,8 @@ impl TableAlterStatement { T: IntoIden, { self.add_alter_option(TableAlterOption::DropConstraint(name.into_iden())) - + } + /// Add a constraint to existing table pub fn add_constraint(&mut self, constraint: &TableConstraint) -> &mut Self { self.add_alter_option(TableAlterOption::AddConstraint(constraint.to_owned())) diff --git a/tests/mysql/constraint.rs b/tests/mysql/constraint.rs new file mode 100644 index 000000000..0ea2d10bd --- /dev/null +++ b/tests/mysql/constraint.rs @@ -0,0 +1,76 @@ +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn create_1() { + assert_eq!( + Constraint::create() + .primary() + .constraint_name("PK_2e303c3a712662f1fc2a4d0aad6") + .table(Font::Table) + .col(Font::Id) + .to_string(MysqlQueryBuilder), + [ + r#"ALTER TABLE `font` ADD CONSTRAINT `PK_2e303c3a712662f1fc2a4d0aad6`"#, + r#"PRIMARY KEY (`id`)"#, + ] + .join(" ") + ); +} + +#[test] +fn create_2() { + assert_eq!( + Constraint::create() + .unique() + .constraint_name("UQ_2e303c3a712662f1fc2a4d0aad6") + .table(Font::Table) + .col(Font::Name) + .to_string(MysqlQueryBuilder), + [ + r#"ALTER TABLE `font` ADD CONSTRAINT `UQ_2e303c3a712662f1fc2a4d0aad6`"#, + r#"UNIQUE KEY (`name`)"#, + ] + .join(" ") + ); +} + +#[test] +fn create_3() { + assert_eq!( + Constraint::create() + .unique() + .constraint_name("UQ_2e303c3a712662f1fc2a4d0aad6") + .index_name("idx_2e303c3a712662f1fc2a4d0aad6") + .table(Font::Table) + .col(Font::Name) + .to_string(MysqlQueryBuilder), + [ + r#"ALTER TABLE `font` ADD CONSTRAINT `UQ_2e303c3a712662f1fc2a4d0aad6`"#, + r#"UNIQUE KEY `idx_2e303c3a712662f1fc2a4d0aad6` (`name`)"#, + ] + .join(" ") + ); +} + +#[test] +fn create_4() { + assert_eq!( + Constraint::create() + .check(("id_range", Expr::col(Glyph::Id).lt(20))) + .table(Glyph::Table) + .to_string(MysqlQueryBuilder), + [r#"ALTER TABLE `glyph` ADD CONSTRAINT `id_range` CHECK (`id` < 20)"#,].join(" ") + ); +} + +#[test] +fn create_5() { + assert_eq!( + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .table(Glyph::Table) + .to_string(MysqlQueryBuilder), + [r#"ALTER TABLE `glyph` ADD CHECK (`id` < 20)"#,].join(" ") + ); +} diff --git a/tests/mysql/mod.rs b/tests/mysql/mod.rs index a0fbe63e6..fc3a212c6 100644 --- a/tests/mysql/mod.rs +++ b/tests/mysql/mod.rs @@ -1,6 +1,7 @@ use sea_query::{extension::mysql::*, tests_cfg::*, *}; mod explain; +mod constraint; mod foreign_key; mod index; mod query; diff --git a/tests/postgres/constraint.rs b/tests/postgres/constraint.rs new file mode 100644 index 000000000..6b94440ad --- /dev/null +++ b/tests/postgres/constraint.rs @@ -0,0 +1,58 @@ +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn create_1() { + assert_eq!( + Constraint::create() + .primary() + .constraint_name("PK_2e303c3a712662f1fc2a4d0aad6") + .table(Font::Table) + .col(Font::Id) + .to_string(PostgresQueryBuilder), + [ + r#"ALTER TABLE "font" ADD CONSTRAINT "PK_2e303c3a712662f1fc2a4d0aad6""#, + r#"PRIMARY KEY ("id")"#, + ] + .join(" ") + ); +} + +#[test] +fn create_2() { + assert_eq!( + Constraint::create() + .unique() + .constraint_name("UQ_2e303c3a712662f1fc2a4d0aad6") + .table(Font::Table) + .col(Font::Name) + .to_string(PostgresQueryBuilder), + [ + r#"ALTER TABLE "font" ADD CONSTRAINT "UQ_2e303c3a712662f1fc2a4d0aad6""#, + r#"UNIQUE ("name")"#, + ] + .join(" ") + ); +} + +#[test] +fn create_3() { + assert_eq!( + Constraint::create() + .check(("id_range", Expr::col(Glyph::Id).lt(20))) + .table(Glyph::Table) + .to_string(PostgresQueryBuilder), + [r#"ALTER TABLE "glyph" ADD CONSTRAINT "id_range" CHECK ("id" < 20)"#,].join(" ") + ); +} + +#[test] +fn create_4() { + assert_eq!( + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .table(Glyph::Table) + .to_string(PostgresQueryBuilder), + [r#"ALTER TABLE "glyph" ADD CHECK ("id" < 20)"#,].join(" ") + ); +} diff --git a/tests/postgres/mod.rs b/tests/postgres/mod.rs index 8a159fb8a..dfc03b204 100644 --- a/tests/postgres/mod.rs +++ b/tests/postgres/mod.rs @@ -1,6 +1,7 @@ use sea_query::{tests_cfg::*, *}; mod explain; +mod constraint; mod foreign_key; mod index; mod query; diff --git a/tests/sqlite/constraint.rs b/tests/sqlite/constraint.rs new file mode 100644 index 000000000..6f80e448c --- /dev/null +++ b/tests/sqlite/constraint.rs @@ -0,0 +1 @@ +// Sqlite does not support modification of constraints to existing tables diff --git a/tests/sqlite/mod.rs b/tests/sqlite/mod.rs index d30f15503..a12814139 100644 --- a/tests/sqlite/mod.rs +++ b/tests/sqlite/mod.rs @@ -1,6 +1,7 @@ use sea_query::{tests_cfg::*, *}; mod explain; +mod constraint; mod foreign_key; mod index; mod query; From 22945de7986c96def51531111c98411b844a16cd Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:41:07 +0300 Subject: [PATCH 08/17] Lint and fmt changes --- src/constraint/drop.rs | 1 + src/constraint/mod.rs | 4 ++-- tests/mysql/mod.rs | 2 +- tests/postgres/mod.rs | 2 +- tests/sqlite/mod.rs | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/constraint/drop.rs b/src/constraint/drop.rs index e69de29bb..8b1378917 100644 --- a/src/constraint/drop.rs +++ b/src/constraint/drop.rs @@ -0,0 +1 @@ + diff --git a/src/constraint/mod.rs b/src/constraint/mod.rs index 837382605..57e1a39ff 100644 --- a/src/constraint/mod.rs +++ b/src/constraint/mod.rs @@ -3,7 +3,7 @@ //! # Usage //! //! - Table Constraint Create, see [`ConstraintCreateStatement`] -//! - Table Constraint Drop, see [`ConstraintDropStatement`] +/* //! - Table Constraint Drop, see [`ConstraintDropStatement`] */ mod common; mod create; @@ -11,7 +11,7 @@ mod drop; pub use common::*; pub use create::*; -pub use drop::*; +/* pub use drop::*; */ /// Shorthand for constructing constraint statement #[derive(Debug, Clone)] diff --git a/tests/mysql/mod.rs b/tests/mysql/mod.rs index fc3a212c6..790e156f0 100644 --- a/tests/mysql/mod.rs +++ b/tests/mysql/mod.rs @@ -1,7 +1,7 @@ use sea_query::{extension::mysql::*, tests_cfg::*, *}; -mod explain; mod constraint; +mod explain; mod foreign_key; mod index; mod query; diff --git a/tests/postgres/mod.rs b/tests/postgres/mod.rs index dfc03b204..720d3c9ce 100644 --- a/tests/postgres/mod.rs +++ b/tests/postgres/mod.rs @@ -1,7 +1,7 @@ use sea_query::{tests_cfg::*, *}; -mod explain; mod constraint; +mod explain; mod foreign_key; mod index; mod query; diff --git a/tests/sqlite/mod.rs b/tests/sqlite/mod.rs index a12814139..e0eab65a1 100644 --- a/tests/sqlite/mod.rs +++ b/tests/sqlite/mod.rs @@ -1,7 +1,7 @@ use sea_query::{tests_cfg::*, *}; -mod explain; mod constraint; +mod explain; mod foreign_key; mod index; mod query; From 808d98bf4838fbb7a51bab09e67f461d385911b1 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:37:35 +0300 Subject: [PATCH 09/17] Remove ambiguity from constraint check naming. Comment `ConditionalStatement` for `ConstraintCreateStatement` --- src/constraint/common.rs | 19 ++++++++++++++----- src/constraint/create.rs | 13 ++++++------- tests/mysql/constraint.rs | 11 ++++++++++- tests/postgres/constraint.rs | 11 ++++++++++- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/constraint/common.rs b/src/constraint/common.rs index 3c11793e3..593b9a2cd 100644 --- a/src/constraint/common.rs +++ b/src/constraint/common.rs @@ -1,4 +1,4 @@ -use crate::{Check, ConditionHolder, IndexType, IntoIndexColumn, TableIndex, types::*}; +use crate::{Check, ConditionHolder, Expr, IndexType, IntoIndexColumn, TableIndex, types::*}; /// Specification of a constraint #[derive(Default, Debug, Clone)] @@ -24,7 +24,11 @@ impl TableConstraint { where T: Into, { - self.name = Some(name.into()); + let name = name.into(); + if let Some(ConstraintCreateStatementType::Check(check)) = &mut self.constraint_type { + check.name = Some(name.clone().into()); + } + self.name = Some(name); self } @@ -59,11 +63,16 @@ impl TableConstraint { } /// Set constraint as check - pub fn check(&mut self, check: T) -> &mut Self + pub fn check(&mut self, expr: T) -> &mut Self where - T: Into, + T: Into, { - self.constraint_type = Some(ConstraintCreateStatementType::Check(check.into())); + self.constraint_type = Some(ConstraintCreateStatementType::Check( + match self.name.clone() { + Some(name) => Check::named(name, expr), + None => Check::unnamed(expr), + }, + )); self } diff --git a/src/constraint/create.rs b/src/constraint/create.rs index 7e612edc2..1f01915e6 100644 --- a/src/constraint/create.rs +++ b/src/constraint/create.rs @@ -1,9 +1,6 @@ use inherent::inherent; -use crate::{ - Check, ConditionalStatement, IndexType, IntoCondition, IntoIndexColumn, TableConstraint, - TableIndex, -}; +use crate::{Expr, IndexType, IntoIndexColumn, TableConstraint, TableIndex}; use crate::{SchemaStatementBuilder, backend::SchemaBuilder, types::*}; #[derive(Default, Debug, Clone)] @@ -67,11 +64,11 @@ impl ConstraintCreateStatement { } /// Set constraint as check - pub fn check(&mut self, check: T) -> &mut Self + pub fn check(&mut self, expr: T) -> &mut Self where - T: Into, + T: Into, { - self.constraint.check(check); + self.constraint.check(expr); self } @@ -142,6 +139,7 @@ impl SchemaStatementBuilder for ConstraintCreateStatement { T: SchemaBuilder; } +/* For future EXCLUDE constraint support impl ConditionalStatement for ConstraintCreateStatement { fn and_or_where(&mut self, condition: LogicalChainOper) -> &mut Self { self.constraint.r#where.add_and_or(condition); @@ -158,3 +156,4 @@ impl ConditionalStatement for ConstraintCreateStatement { self } } +*/ diff --git a/tests/mysql/constraint.rs b/tests/mysql/constraint.rs index 0ea2d10bd..5f649e368 100644 --- a/tests/mysql/constraint.rs +++ b/tests/mysql/constraint.rs @@ -57,7 +57,16 @@ fn create_3() { fn create_4() { assert_eq!( Constraint::create() - .check(("id_range", Expr::col(Glyph::Id).lt(20))) + .constraint_name("id_range") + .check(Expr::col(Glyph::Id).lt(20)) + .table(Glyph::Table) + .to_string(MysqlQueryBuilder), + [r#"ALTER TABLE `glyph` ADD CONSTRAINT `id_range` CHECK (`id` < 20)"#,].join(" ") + ); + assert_eq!( + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .constraint_name("id_range") .table(Glyph::Table) .to_string(MysqlQueryBuilder), [r#"ALTER TABLE `glyph` ADD CONSTRAINT `id_range` CHECK (`id` < 20)"#,].join(" ") diff --git a/tests/postgres/constraint.rs b/tests/postgres/constraint.rs index 6b94440ad..67ef06795 100644 --- a/tests/postgres/constraint.rs +++ b/tests/postgres/constraint.rs @@ -39,7 +39,16 @@ fn create_2() { fn create_3() { assert_eq!( Constraint::create() - .check(("id_range", Expr::col(Glyph::Id).lt(20))) + .constraint_name("id_range") + .check(Expr::col(Glyph::Id).lt(20)) + .table(Glyph::Table) + .to_string(PostgresQueryBuilder), + [r#"ALTER TABLE "glyph" ADD CONSTRAINT "id_range" CHECK ("id" < 20)"#,].join(" ") + ); + assert_eq!( + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .constraint_name("id_range") .table(Glyph::Table) .to_string(PostgresQueryBuilder), [r#"ALTER TABLE "glyph" ADD CONSTRAINT "id_range" CHECK ("id" < 20)"#,].join(" ") From e0b2274d732ecd5885b88215646d1c6d0e411629 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:06:16 +0300 Subject: [PATCH 10/17] Forbid invalid usages for `PostgresQueryBuilder` --- src/backend/postgres/constraint.rs | 39 +++++++++++++++++-- src/constraint/common.rs | 2 +- src/constraint/create.rs | 2 +- tests/postgres/constraint.rs | 60 ++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/backend/postgres/constraint.rs b/src/backend/postgres/constraint.rs index 4c882d2af..ed076c4f2 100644 --- a/src/backend/postgres/constraint.rs +++ b/src/backend/postgres/constraint.rs @@ -11,6 +11,37 @@ impl ConstraintBuilder for PostgresQueryBuilder { panic!("No constraint type found"); }; + assert!( + create.constraint.index_type.is_none(), + "Postgres does not support index types in ADD CONSTRAINT" + ); + if create.constraint.using_index.is_some() { + assert!( + create.constraint.index.columns.is_empty() + && create.constraint.include_columns.is_empty() + && !create.constraint.nulls_not_distinct, + "Postgres does not support combining USING INDEX with columns or index options" + ); + } + if matches!(constraint_type, ConstraintCreateStatementType::Check(_)) { + assert!( + create.constraint.using_index.is_none(), + "Postgres does not support USING INDEX on CHECK constraints" + ); + assert!( + create.constraint.index.columns.is_empty() + && create.constraint.include_columns.is_empty() + && !create.constraint.nulls_not_distinct, + "Postgres does not support index options on CHECK constraints" + ); + } else { + assert!( + !matches!(constraint_type, ConstraintCreateStatementType::PrimaryKey) + || !create.constraint.nulls_not_distinct, + "Postgres does not support NULLS NOT DISTINCT on PRIMARY KEY constraints" + ); + } + if mode == ConstraintMode::Alter { sql.write_str("ALTER TABLE ").unwrap(); if let Some(table) = &create.table { @@ -25,7 +56,7 @@ impl ConstraintBuilder for PostgresQueryBuilder { ConstraintCreateStatementType::Check(check) => { self.prepare_check_constraint(check, sql) } - value => { + ConstraintCreateStatementType::PrimaryKey | ConstraintCreateStatementType::Unique => { if let Some(name) = &create.constraint.name { sql.write_str("CONSTRAINT ").unwrap(); sql.write_char(self.quote().left()).unwrap(); @@ -34,14 +65,14 @@ impl ConstraintBuilder for PostgresQueryBuilder { sql.write_str(" ").unwrap(); } - match value { + match constraint_type { ConstraintCreateStatementType::PrimaryKey => { - sql.write_str("PRIMARY KEY ").unwrap() + sql.write_str("PRIMARY KEY ").unwrap(); } ConstraintCreateStatementType::Unique => { sql.write_str("UNIQUE ").unwrap(); } - _ => unreachable!(), + ConstraintCreateStatementType::Check(_) => unreachable!(), } if let Some(using_index) = &create.constraint.using_index { diff --git a/src/constraint/common.rs b/src/constraint/common.rs index 593b9a2cd..0266cae8d 100644 --- a/src/constraint/common.rs +++ b/src/constraint/common.rs @@ -93,7 +93,7 @@ impl TableConstraint { self } - /// Set index type. Only available on MySQL. + /// Use an existing index for the constraint. Only available on Postgres. pub fn using_index(&mut self, using_index: T) -> &mut Self where T: IntoIden, diff --git a/src/constraint/create.rs b/src/constraint/create.rs index 1f01915e6..9f8b8e8ec 100644 --- a/src/constraint/create.rs +++ b/src/constraint/create.rs @@ -89,7 +89,7 @@ impl ConstraintCreateStatement { self } - /// Set index type. Only available on MySQL. + /// Use an existing index for the constraint. Only available on Postgres. pub fn using_index(&mut self, using_index: T) -> &mut Self where T: IntoIden, diff --git a/tests/postgres/constraint.rs b/tests/postgres/constraint.rs index 67ef06795..94a78fbf5 100644 --- a/tests/postgres/constraint.rs +++ b/tests/postgres/constraint.rs @@ -65,3 +65,63 @@ fn create_4() { [r#"ALTER TABLE "glyph" ADD CHECK ("id" < 20)"#,].join(" ") ); } + +#[test] +#[should_panic( + expected = "Postgres does not support combining USING INDEX with columns or index options" +)] +fn create_5() { + Constraint::create() + .unique() + .constraint_name("font_name_key") + .using_index("idx_font_name") + .col(Font::Name) + .table(Font::Table) + .to_string(PostgresQueryBuilder); +} + +#[test] +#[should_panic( + expected = "Postgres does not support NULLS NOT DISTINCT on PRIMARY KEY constraints" +)] +fn create_6() { + Constraint::create() + .primary() + .constraint_name("font_pkey") + .nulls_not_distinct() + .col(Font::Id) + .table(Font::Table) + .to_string(PostgresQueryBuilder); +} + +#[test] +#[should_panic(expected = "Postgres does not support index types in ADD CONSTRAINT")] +fn create_7() { + Constraint::create() + .unique() + .constraint_name("font_name_key") + .index_type(IndexType::Hash) + .col(Font::Name) + .table(Font::Table) + .to_string(PostgresQueryBuilder); +} + +#[test] +#[should_panic(expected = "Postgres does not support USING INDEX on CHECK constraints")] +fn create_8() { + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .using_index("idx_glyph_id") + .table(Glyph::Table) + .to_string(PostgresQueryBuilder); +} + +#[test] +#[should_panic(expected = "Postgres does not support index options on CHECK constraints")] +fn create_9() { + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .include(Glyph::Aspect) + .table(Glyph::Table) + .to_string(PostgresQueryBuilder); +} From 862f80c09bff917fcbba7ecea4222586efd6a075 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:43:58 +0300 Subject: [PATCH 11/17] Forbid invalid usages for `MysqlQueryBuilder` --- src/backend/mysql/constraint.rs | 32 ++++++++++++++++++++++ tests/mysql/constraint.rs | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/backend/mysql/constraint.rs b/src/backend/mysql/constraint.rs index e90588ba6..1865eee12 100644 --- a/src/backend/mysql/constraint.rs +++ b/src/backend/mysql/constraint.rs @@ -11,6 +11,38 @@ impl ConstraintBuilder for MysqlQueryBuilder { panic!("No constraint type found"); }; + assert!( + create.constraint.using_index.is_none(), + "MySQL does not support USING INDEX in ADD CONSTRAINT" + ); + assert!( + !create.constraint.nulls_not_distinct, + "MySQL does not support NULLS NOT DISTINCT in ADD CONSTRAINT" + ); + assert!( + create.constraint.include_columns.is_empty(), + "MySQL does not support INCLUDE columns in ADD CONSTRAINT" + ); + if matches!(constraint_type, ConstraintCreateStatementType::Check(_)) { + assert!( + create.constraint.index.name.is_none() + && create.constraint.index.columns.is_empty(), + "MySQL does not support index options on CHECK constraints" + ); + } else { + assert!( + !matches!(constraint_type, ConstraintCreateStatementType::PrimaryKey) + || create.constraint.index.name.is_none(), + "MySQL does not support index names on PRIMARY KEY constraints" + ); + if let Some(index_type) = &create.constraint.index_type { + assert!( + matches!(index_type, IndexType::BTree | IndexType::Hash), + "MySQL supports only BTREE or HASH index types in ADD CONSTRAINT UNIQUE/PRIMARY KEY" + ); + } + } + if mode == ConstraintMode::Alter { sql.write_str("ALTER TABLE ").unwrap(); if let Some(table) = &create.table { diff --git a/tests/mysql/constraint.rs b/tests/mysql/constraint.rs index 5f649e368..64795f43f 100644 --- a/tests/mysql/constraint.rs +++ b/tests/mysql/constraint.rs @@ -83,3 +83,50 @@ fn create_5() { [r#"ALTER TABLE `glyph` ADD CHECK (`id` < 20)"#,].join(" ") ); } + +#[test] +#[should_panic(expected = "MySQL does not support USING INDEX in ADD CONSTRAINT")] +fn create_6() { + Constraint::create() + .unique() + .constraint_name("font_name_key") + .using_index("idx_font_name") + .table(Font::Table) + .to_string(MysqlQueryBuilder); +} + +#[test] +#[should_panic(expected = "MySQL does not support index names on PRIMARY KEY constraints")] +fn create_7() { + Constraint::create() + .primary() + .constraint_name("font_pkey") + .index_name("idx_font_id") + .col(Font::Id) + .table(Font::Table) + .to_string(MysqlQueryBuilder); +} + +#[test] +#[should_panic(expected = "MySQL does not support index options on CHECK constraints")] +fn create_8() { + Constraint::create() + .check(Expr::col(Glyph::Id).lt(20)) + .col(Glyph::Id) + .table(Glyph::Table) + .to_string(MysqlQueryBuilder); +} + +#[test] +#[should_panic( + expected = "MySQL supports only BTREE or HASH index types in ADD CONSTRAINT UNIQUE/PRIMARY KEY" +)] +fn create_9() { + Constraint::create() + .unique() + .constraint_name("font_name_key") + .full_text() + .col(Font::Name) + .table(Font::Table) + .to_string(MysqlQueryBuilder); +} From 1ecb20e2ac553cfda01fca44e1ef63cc8a34c068 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 01:45:04 +0300 Subject: [PATCH 12/17] Improve error message for CHECK constraint --- src/backend/mysql/constraint.rs | 2 +- src/backend/postgres/constraint.rs | 2 +- tests/mysql/constraint.rs | 2 +- tests/postgres/constraint.rs | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend/mysql/constraint.rs b/src/backend/mysql/constraint.rs index 1865eee12..4f399a333 100644 --- a/src/backend/mysql/constraint.rs +++ b/src/backend/mysql/constraint.rs @@ -27,7 +27,7 @@ impl ConstraintBuilder for MysqlQueryBuilder { assert!( create.constraint.index.name.is_none() && create.constraint.index.columns.is_empty(), - "MySQL does not support index options on CHECK constraints" + "MySQL does not support columns or index options on CHECK constraints" ); } else { assert!( diff --git a/src/backend/postgres/constraint.rs b/src/backend/postgres/constraint.rs index ed076c4f2..e473e4004 100644 --- a/src/backend/postgres/constraint.rs +++ b/src/backend/postgres/constraint.rs @@ -32,7 +32,7 @@ impl ConstraintBuilder for PostgresQueryBuilder { create.constraint.index.columns.is_empty() && create.constraint.include_columns.is_empty() && !create.constraint.nulls_not_distinct, - "Postgres does not support index options on CHECK constraints" + "Postgres does not support columns or index options on CHECK constraints" ); } else { assert!( diff --git a/tests/mysql/constraint.rs b/tests/mysql/constraint.rs index 64795f43f..f0e40fa88 100644 --- a/tests/mysql/constraint.rs +++ b/tests/mysql/constraint.rs @@ -108,7 +108,7 @@ fn create_7() { } #[test] -#[should_panic(expected = "MySQL does not support index options on CHECK constraints")] +#[should_panic(expected = "MySQL does not support columns or index options on CHECK constraints")] fn create_8() { Constraint::create() .check(Expr::col(Glyph::Id).lt(20)) diff --git a/tests/postgres/constraint.rs b/tests/postgres/constraint.rs index 94a78fbf5..16576dc45 100644 --- a/tests/postgres/constraint.rs +++ b/tests/postgres/constraint.rs @@ -117,7 +117,9 @@ fn create_8() { } #[test] -#[should_panic(expected = "Postgres does not support index options on CHECK constraints")] +#[should_panic( + expected = "Postgres does not support columns or index options on CHECK constraints" +)] fn create_9() { Constraint::create() .check(Expr::col(Glyph::Id).lt(20)) From c2f9d1697e7bc508fc7a19e780f7c38dd001aa1d Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:14:09 +0300 Subject: [PATCH 13/17] Add `ConstraintStatement` to `SchemaStatement` --- src/schema.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/schema.rs b/src/schema.rs index 1e923dd8d..38016d7be 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,6 +1,9 @@ //! Schema definition & alternations statements -use crate::{ForeignKeyStatement, IndexStatement, TableStatement, backend::SchemaBuilder}; +use crate::{ + ConstraintStatement, ForeignKeyStatement, IndexStatement, TableStatement, + backend::SchemaBuilder, +}; #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] @@ -9,6 +12,7 @@ pub enum SchemaStatement { TableStatement(TableStatement), IndexStatement(IndexStatement), ForeignKeyStatement(ForeignKeyStatement), + ConstraintStatement(ConstraintStatement), } pub trait SchemaStatementBuilder { From e002493f59bd55b6a7df97a56dadbdb3f45e8008 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:27:54 +0300 Subject: [PATCH 14/17] Add docs for `add_constraint` --- src/table/alter.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/table/alter.rs b/src/table/alter.rs index 8d94f5c95..41b4dadae 100644 --- a/src/table/alter.rs +++ b/src/table/alter.rs @@ -424,7 +424,29 @@ impl TableAlterStatement { self.add_alter_option(TableAlterOption::DropConstraint(name.into_iden())) } - /// Add a constraint to existing table + /// Add a constraint to an existing table. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let table = Table::alter() + /// .table(Font::Table) + /// .add_constraint(&TableConstraint::new().primary().col(Font::Id)) + /// .to_owned(); + /// + /// assert_eq!( + /// table.to_string(MysqlQueryBuilder), + /// r#"ALTER TABLE `font` ADD PRIMARY KEY (`id`)"# + /// ); + /// assert_eq!( + /// table.to_string(PostgresQueryBuilder), + /// r#"ALTER TABLE "font" ADD PRIMARY KEY ("id")"# + /// ); + /// + /// // Sqlite does not support adding constraints to existing tables + /// ``` pub fn add_constraint(&mut self, constraint: &TableConstraint) -> &mut Self { self.add_alter_option(TableAlterOption::AddConstraint(constraint.to_owned())) } From fe53af72dad59b7bbee55a56887bf064b816155c Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:39:26 +0300 Subject: [PATCH 15/17] Add getters for `TableConstraint` and `ConstraintCreateStatement` --- src/constraint/common.rs | 55 ++++++++++++++++++++++++++++++++++++++++ src/constraint/create.rs | 10 ++++---- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/constraint/common.rs b/src/constraint/common.rs index 0266cae8d..b8dcd09e0 100644 --- a/src/constraint/common.rs +++ b/src/constraint/common.rs @@ -111,10 +111,65 @@ impl TableConstraint { self } + pub fn get_name(&self) -> Option<&str> { + self.name.as_deref() + } + + pub fn get_index_name(&self) -> Option<&str> { + self.index.get_name() + } + + pub fn get_columns(&self) -> Vec { + self.index.get_column_names() + } + + pub fn get_check(&self) -> Option<&Check> { + match &self.constraint_type { + Some(ConstraintCreateStatementType::Check(check)) => Some(check), + _ => None, + } + } + + pub fn is_primary_key(&self) -> bool { + matches!( + self.constraint_type, + Some(ConstraintCreateStatementType::PrimaryKey) + ) + } + + pub fn is_unique_key(&self) -> bool { + matches!( + self.constraint_type, + Some(ConstraintCreateStatementType::Unique) + ) + } + + pub fn is_check(&self) -> bool { + matches!( + self.constraint_type, + Some(ConstraintCreateStatementType::Check(_)) + ) + } + pub fn is_nulls_not_distinct(&self) -> bool { self.nulls_not_distinct } + pub fn get_index_type(&self) -> Option<&IndexType> { + self.index_type.as_ref() + } + + pub fn get_using_index(&self) -> Option<&DynIden> { + self.using_index.as_ref() + } + + pub fn get_include_columns(&self) -> Vec { + self.include_columns + .iter() + .map(|col| col.to_string()) + .collect() + } + pub fn get_index_spec(&self) -> &TableIndex { &self.index } diff --git a/src/constraint/create.rs b/src/constraint/create.rs index 9f8b8e8ec..6c84e90cf 100644 --- a/src/constraint/create.rs +++ b/src/constraint/create.rs @@ -1,6 +1,6 @@ use inherent::inherent; -use crate::{Expr, IndexType, IntoIndexColumn, TableConstraint, TableIndex}; +use crate::{Expr, IndexType, IntoIndexColumn, TableConstraint}; use crate::{SchemaStatementBuilder, backend::SchemaBuilder, types::*}; #[derive(Default, Debug, Clone)] @@ -107,12 +107,12 @@ impl ConstraintCreateStatement { self } - pub fn is_nulls_not_distinct(&self) -> bool { - self.constraint.is_nulls_not_distinct() + pub fn get_table(&self) -> Option<&TableRef> { + self.table.as_ref() } - pub fn get_index_spec(&self) -> &TableIndex { - self.constraint.get_index_spec() + pub fn get_constraint(&self) -> &TableConstraint { + &self.constraint } pub fn take(&mut self) -> Self { From 0e90c0a8dfbf2483a70d0161d8833d349ffdf3fc Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:47:33 +0300 Subject: [PATCH 16/17] Add docs for `ConstraintCreateStatement` --- src/constraint/create.rs | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/constraint/create.rs b/src/constraint/create.rs index 6c84e90cf..c38ef2c41 100644 --- a/src/constraint/create.rs +++ b/src/constraint/create.rs @@ -3,6 +3,87 @@ use inherent::inherent; use crate::{Expr, IndexType, IntoIndexColumn, TableConstraint}; use crate::{SchemaStatementBuilder, backend::SchemaBuilder, types::*}; +/// Create a constraint for an existing table. Unsupported by Sqlite +/// +/// # Examples +/// +/// Primary key +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let constraint = Constraint::create() +/// .primary() +/// .constraint_name("PK_2e303c3a712662f1fc2a4d0aad6") +/// .table(Font::Table) +/// .col(Font::Id) +/// .to_owned(); +/// +/// assert_eq!( +/// constraint.to_string(MysqlQueryBuilder), +/// [ +/// r#"ALTER TABLE `font` ADD CONSTRAINT `PK_2e303c3a712662f1fc2a4d0aad6`"#, +/// r#"PRIMARY KEY (`id`)"#, +/// ] +/// .join(" ") +/// ); +/// assert_eq!( +/// constraint.to_string(PostgresQueryBuilder), +/// [ +/// r#"ALTER TABLE "font" ADD CONSTRAINT "PK_2e303c3a712662f1fc2a4d0aad6""#, +/// r#"PRIMARY KEY ("id")"#, +/// ] +/// .join(" ") +/// ); +/// ``` +/// +/// Unique constraint +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let constraint = Constraint::create() +/// .unique() +/// .constraint_name("UQ_2e303c3a712662f1fc2a4d0aad6") +/// .table(Font::Table) +/// .col(Font::Name) +/// .to_owned(); +/// +/// assert_eq!( +/// constraint.to_string(MysqlQueryBuilder), +/// [ +/// r#"ALTER TABLE `font` ADD CONSTRAINT `UQ_2e303c3a712662f1fc2a4d0aad6`"#, +/// r#"UNIQUE KEY (`name`)"#, +/// ] +/// .join(" ") +/// ); +/// assert_eq!( +/// constraint.to_string(PostgresQueryBuilder), +/// [ +/// r#"ALTER TABLE "font" ADD CONSTRAINT "UQ_2e303c3a712662f1fc2a4d0aad6""#, +/// r#"UNIQUE ("name")"#, +/// ] +/// .join(" ") +/// ); +/// ``` +/// +/// Check constraint +/// ``` +/// use sea_query::{tests_cfg::*, *}; +/// +/// let constraint = Constraint::create() +/// .constraint_name("id_range") +/// .check(Expr::col(Glyph::Id).lt(20)) +/// .table(Glyph::Table) +/// .to_owned(); +/// +/// assert_eq!( +/// constraint.to_string(MysqlQueryBuilder), +/// r#"ALTER TABLE `glyph` ADD CONSTRAINT `id_range` CHECK (`id` < 20)"# +/// ); +/// assert_eq!( +/// constraint.to_string(PostgresQueryBuilder), +/// r#"ALTER TABLE "glyph" ADD CONSTRAINT "id_range" CHECK ("id" < 20)"# +/// ); +/// ``` #[derive(Default, Debug, Clone)] pub struct ConstraintCreateStatement { pub(crate) table: Option, From af819db1ce1dbe508644d6d39f706b7069aa4453 Mon Sep 17 00:00:00 2001 From: Desiders <47452083+Desiders@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:55:14 +0300 Subject: [PATCH 17/17] Fix incorrect docs for `ConstraintStatement` --- src/constraint/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraint/mod.rs b/src/constraint/mod.rs index 57e1a39ff..ca7878472 100644 --- a/src/constraint/mod.rs +++ b/src/constraint/mod.rs @@ -17,7 +17,7 @@ pub use create::*; #[derive(Debug, Clone)] pub struct Constraint; -/// All available types of index statement +/// All available types of constraint statement #[derive(Debug, Clone)] #[non_exhaustive] pub enum ConstraintStatement {