fix(dameng): use ALTER TABLE constraints for unique indexes

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-05 11:16:43 +08:00
1 parent a19a77c6df
commit 124819e1fa
27 files changed
+466 -10

No files matched your search

@@ -1,6 +1,6 @@
use super::comments::build_sqlserver_index_comment_sql;
use super::dialect::{capabilities_for, database_label, database_type_for_dialect, dialect_label, StructureDialect};
use super::types::{EditableStructureIndex, TableStructureSqlOptions};
use super::types::{EditableStructureIndex, IndexInfo, TableStructureSqlOptions};
use super::util::{clean, qualified_table, quote_ident, quote_new_ident, quote_string};
use crate::models::connection::DatabaseType;
@@ -33,6 +33,10 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut
warnings.push(format!("Primary index \"{}\" cannot be dropped from this editor.", original.name));
continue;
}
if is_dameng_constraint_backed(options.database_type, original) {
statements.push(build_dameng_drop_constraint_sql(dialect, &table, &original.name));
continue;
}
statements.push(build_drop_index_sql(
options.database_type,
dialect,
@@ -56,6 +60,18 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut
warnings.push(format!("Primary index \"{}\" cannot be edited from this editor.", original.name));
continue;
}
if is_dameng_constraint_backed(options.database_type, original) {
statements.extend(build_dameng_constraint_index_edit(
options,
dialect,
&table,
index,
original,
capabilities.index_concurrent,
warnings,
));
continue;
}
let or_replace =
options.database_type == Some(DatabaseType::Dameng) && clean(&index.name) == clean(&original.name);
if !or_replace {
@@ -103,6 +119,77 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut
statements
}
/// Dameng builds the index behind a PRIMARY KEY / UNIQUE constraint as a "virtual" index
/// owned by that constraint. Index-level DDL against such an index — `DROP INDEX` as much as
/// `CREATE OR REPLACE INDEX` — is rejected with a misleading "no permission to drop index"
/// error even for the owning user (#7959); it can only be changed through
/// `ALTER TABLE ... ADD/DROP CONSTRAINT`, the same way Dameng primary keys are already
/// handled in `columns.rs`.
///
/// A "real" unique index (`CREATE UNIQUE INDEX`, which is also what this editor emits for a
/// newly created unique index) has no constraint behind it, is not reported as
/// constraint-backed by introspection, and keeps the index-level path — constraint DDL would
/// fail on it with "constraint does not exist".
fn is_dameng_constraint_backed(database_type: Option<DatabaseType>, original: &IndexInfo) -> bool {
database_type == Some(DatabaseType::Dameng) && original.constraint_backed
}
fn build_dameng_drop_constraint_sql(dialect: StructureDialect, table: &str, constraint_name: &str) -> String {
format!("ALTER TABLE {table} DROP CONSTRAINT {};", quote_ident(dialect, constraint_name))
}
/// Rewrites an edit of a constraint-backed Dameng index as constraint DDL: drop the
/// constraint, then re-add it as `UNIQUE` (still unique) or replace it with an ordinary
/// index (downgraded to a plain index, where index-level DDL is fine again because the
/// constraint is gone).
fn build_dameng_constraint_index_edit(
options: &TableStructureSqlOptions,
dialect: StructureDialect,
table: &str,
index: &EditableStructureIndex,
original: &IndexInfo,
concurrently_supported: bool,
warnings: &mut Vec<String>,
) -> Vec<String> {
let name = clean(&index.name);
let columns: Vec<String> =
index.columns.iter().map(|column| clean(column)).filter(|column| !column.is_empty()).collect();
// Same guard as `build_create_index_statements`: an empty name or an empty column list
// cannot produce a valid replacement (`validate_draft` already reports it as a warning).
// Emitting only the DROP would silently delete the constraint, so skip the edit entirely.
if name.is_empty() || columns.is_empty() {
return Vec::new();
}
let mut statements = vec![build_dameng_drop_constraint_sql(dialect, table, &original.name)];
if !index.is_unique {
statements.extend(build_create_index_statements(
options.database_type,
dialect,
table,
index,
warnings,
options.schema.as_deref(),
&options.table_name,
false,
concurrently_supported,
false,
));
return statements;
}
// `build_create_index_statements` would honor BITMAP for Dameng, but a unique constraint
// always builds a normal index behind itself, so the type cannot be carried over.
if normalized_index_type(index) == "BITMAP" {
warnings.push(format!(
"Index type BITMAP is ignored for unique index \"{name}\": Dameng enforces it with a unique constraint, whose index cannot be a bitmap index."
));
}
let cols = columns.iter().map(|column| quote_ident(dialect, column)).collect::<Vec<_>>().join(", ");
statements.push(format!("ALTER TABLE {table} ADD CONSTRAINT {} UNIQUE ({cols});", quote_ident(dialect, &name)));
statements
}
pub(super) fn has_existing_index_change(index: &EditableStructureIndex) -> bool {
let Some(original) = &index.original else {
return false;
@@ -2029,6 +2029,7 @@ mod tests {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
}),
marked_for_drop: true,
});
@@ -143,10 +143,20 @@ fn existing_index(name: &str, columns: &[&str], is_unique: bool) -> EditableStru
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
index
}
/// A unique index that is the object *behind* a UNIQUE constraint (Dameng's "virtual" index,
/// `ALL_CONSTRAINTS.CONSTRAINT_TYPE = 'U'`), as opposed to a standalone ("real") unique index
/// created with `CREATE UNIQUE INDEX`, which `existing_index(.., true)` still models.
fn constraint_backed_unique_index(name: &str, columns: &[&str]) -> EditableStructureIndex {
let mut index = existing_index(name, columns, true);
index.original.as_mut().unwrap().constraint_backed = true;
index
}
fn existing_primary_index(name: &str, columns: &[&str]) -> EditableStructureIndex {
let mut index = existing_index(name, columns, true);
index.is_primary = true;
@@ -236,6 +246,7 @@ fn builds_mysql_column_and_index_changes() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let mut email_index = index("uniq_users_email", &["email"]);
email_index.is_unique = true;
@@ -287,16 +298,43 @@ fn dameng_replaces_same_name_index_before_validating_uniqueness() {
}
#[test]
fn dameng_replaces_same_name_unique_index_with_normal_index() {
let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], true);
changed.is_unique = false;
fn dameng_unique_index_column_change_drops_and_readds_constraint() {
// #7959: the original index is the one behind a UNIQUE constraint. Editing its columns
// while keeping it unique must go through ALTER TABLE ... DROP/ADD CONSTRAINT, not
// CREATE OR REPLACE UNIQUE INDEX — Dameng rejects that with "no permission to drop index"
// because the index is constraint-owned.
let mut changed = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
changed.columns = vec!["EMAIL".to_string(), "TENANT_ID".to_string()];
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec!["CREATE OR REPLACE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\");"]
vec![
"ALTER TABLE \"APP\".\"USERS\" DROP CONSTRAINT \"IDX_USERS_EMAIL\";",
"ALTER TABLE \"APP\".\"USERS\" ADD CONSTRAINT \"IDX_USERS_EMAIL\" UNIQUE (\"EMAIL\", \"TENANT_ID\");",
]
);
}
#[test]
fn dameng_replaces_same_name_unique_index_with_normal_index() {
let mut changed = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
changed.is_unique = false;
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
// The original index backs a unique constraint (#7959): downgrading it to a plain index
// has to drop that constraint first, then create an ordinary index — `CREATE OR REPLACE
// INDEX` against a constraint-backed index is rejected by Dameng.
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"APP\".\"USERS\" DROP CONSTRAINT \"IDX_USERS_EMAIL\";",
"CREATE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\");",
]
);
}
@@ -360,6 +398,122 @@ fn dameng_renamed_index_keeps_drop_then_create_path() {
);
}
#[test]
fn dameng_renamed_unique_index_uses_constraint_ddl_not_drop_index() {
// Same root cause as #7959: a renamed constraint-backed index still has to go through
// DROP/ADD CONSTRAINT rather than DROP INDEX, since DROP INDEX against a
// constraint-backed index is rejected by Dameng regardless of whether the name changes.
let mut changed = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
changed.name = "IDX_USERS_LOGIN".to_string();
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"APP\".\"USERS\" DROP CONSTRAINT \"IDX_USERS_EMAIL\";",
"ALTER TABLE \"APP\".\"USERS\" ADD CONSTRAINT \"IDX_USERS_LOGIN\" UNIQUE (\"EMAIL\");",
]
);
}
#[test]
fn dameng_standalone_unique_index_keeps_index_level_ddl() {
// A unique index created with CREATE UNIQUE INDEX (Dameng's "real" index — which is also
// what this editor emits for a new unique index) has no constraint behind it: it is absent
// from ALL_CONSTRAINTS, so DROP CONSTRAINT would fail with "constraint does not exist".
// Editing it must stay on the index-level path, otherwise DBX breaks the lifecycle of the
// unique indexes it creates itself.
let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], true);
changed.columns = vec!["EMAIL".to_string(), "TENANT_ID".to_string()];
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec!["CREATE OR REPLACE UNIQUE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\", \"TENANT_ID\");"]
);
let mut renamed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], true);
renamed.name = "IDX_USERS_LOGIN".to_string();
let renamed_result =
build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), renamed));
assert_eq!(renamed_result.warnings, Vec::<String>::new());
assert_eq!(
renamed_result.statements,
vec![
"DROP INDEX \"APP\".\"IDX_USERS_EMAIL\";",
"CREATE UNIQUE INDEX \"IDX_USERS_LOGIN\" ON \"APP\".\"USERS\" (\"EMAIL\");",
]
);
}
#[test]
fn dameng_dropping_constraint_backed_unique_index_drops_the_constraint() {
// Deleting the index of #7959 is broken the same way: DROP INDEX is rejected for a
// constraint-owned index, so the delete has to drop the constraint instead.
let mut dropped = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
dropped.marked_for_drop = true;
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), dropped));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE \"APP\".\"USERS\" DROP CONSTRAINT \"IDX_USERS_EMAIL\";"]);
// A standalone unique index keeps DROP INDEX.
let mut standalone = existing_index("IDX_USERS_EMAIL", &["EMAIL"], true);
standalone.marked_for_drop = true;
let standalone_result =
build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), standalone));
assert_eq!(standalone_result.warnings, Vec::<String>::new());
assert_eq!(standalone_result.statements, vec!["DROP INDEX \"APP\".\"IDX_USERS_EMAIL\";"]);
}
#[test]
fn dameng_constraint_backed_unique_index_edit_reports_unusable_bitmap_type() {
// BITMAP is honored by CREATE INDEX for Dameng, but a unique constraint always builds a
// normal index behind itself, so the requested type cannot be carried over silently.
let mut changed = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
changed.index_type = "bitmap".to_string();
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
assert_eq!(
result.warnings,
vec![
"Index type BITMAP is ignored for unique index \"IDX_USERS_EMAIL\": Dameng enforces it with a unique constraint, whose index cannot be a bitmap index."
.to_string()
]
);
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"APP\".\"USERS\" DROP CONSTRAINT \"IDX_USERS_EMAIL\";",
"ALTER TABLE \"APP\".\"USERS\" ADD CONSTRAINT \"IDX_USERS_EMAIL\" UNIQUE (\"EMAIL\");",
]
);
}
#[test]
fn dameng_constraint_backed_unique_index_edit_without_columns_emits_nothing() {
// The empty-column guard of `build_create_index_statements`: dropping the constraint
// without a valid replacement would silently delete it, so the edit is skipped entirely
// and only `validate_draft`'s warning remains.
let mut changed = constraint_backed_unique_index("IDX_USERS_EMAIL", &["EMAIL"]);
changed.columns = vec![" ".to_string()];
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed));
assert_eq!(result.warnings, vec!["Index \"IDX_USERS_EMAIL\" needs at least one column.".to_string()]);
assert!(result.statements.is_empty(), "{:?}", result.statements);
}
#[test]
fn dameng_primary_and_unchanged_indexes_keep_existing_behavior() {
let unchanged = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false);
@@ -891,6 +1045,7 @@ fn builds_informix_column_and_index_changes() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let mut email_index = index("uniq_users_email", &["email"]);
email_index.is_unique = true;
@@ -1339,6 +1494,7 @@ fn iris_drop_index_includes_table_name() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let result = build_table_structure_change_sql(TableStructureSqlOptions {
@@ -1651,6 +1807,7 @@ fn gbase8a_uses_limited_mysql_ddl() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let result = build_table_structure_change_sql(TableStructureSqlOptions {
@@ -2362,6 +2519,7 @@ fn qualifies_attached_sqlite_table_and_index_changes() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let email_index = index("idx_users_email", &["email"]);
@@ -6952,6 +7110,7 @@ fn oscar_drop_index_with_schema_qualifier() {
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
let result = build_table_structure_change_sql(TableStructureSqlOptions {
@@ -7443,6 +7602,7 @@ fn gaussdb_m_existing_index(
comment: None,
key_is_expression: Vec::new(),
column_opclasses: vec![],
constraint_backed: false,
});
idx
}
@@ -7814,6 +7974,7 @@ fn postgres_index_opclass_from_original_when_editable_empty() {
comment: None,
key_is_expression: vec![false],
column_opclasses: vec![Some("gin_trgm_ops".to_string())],
constraint_backed: false,
});
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Postgres, Some("public"), idx));
@@ -7840,6 +8001,7 @@ fn postgres_index_no_rebuild_when_original_has_opclass_and_edit_is_identical() {
comment: None,
key_is_expression: vec![false],
column_opclasses: vec![Some("gin_trgm_ops".to_string())],
constraint_backed: false,
});
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Postgres, Some("public"), idx));
@@ -7865,6 +8027,7 @@ fn postgres_index_rebuild_when_opclass_changed() {
comment: None,
key_is_expression: vec![false],
column_opclasses: vec![None], // was default, now gin_trgm_ops
constraint_backed: false,
});
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Postgres, Some("public"), idx));
@@ -7963,6 +8126,7 @@ fn postgres_expression_index_appends_opclass() {
comment: None,
key_is_expression: vec![true],
column_opclasses: vec![None],
constraint_backed: false,
});
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Postgres, Some("public"), idx));
@@ -8008,6 +8172,7 @@ fn postgres_expression_index_opclass_round_trips_from_indclass() {
comment: None,
key_is_expression: vec![true],
column_opclasses: vec![Some("public.gin_trgm_ops".to_string())],
constraint_backed: false,
});
let result = build_table_structure_change_sql(index_change_options(DatabaseType::Postgres, Some("public"), idx));
@@ -133,6 +133,13 @@ pub struct IndexInfo {
/// Parallel to `columns`: operator class name for each key column, if non-default.
#[serde(default)]
pub column_opclasses: Vec<Option<String>>,
/// Round-tripped from `crate::types::IndexInfo`: `true` when the introspected index is
/// the object behind a PRIMARY KEY / UNIQUE constraint. Dameng only accepts constraint
/// level DDL for those (#7959); a standalone unique index keeps the index-level path.
/// Defaults to `false`, so a payload from an older client (or a source that does not
/// report it) behaves exactly as before.
#[serde(default)]
pub constraint_backed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]