fix(dameng): use ALTER TABLE constraints for unique indexes
This commit is contained in:
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)]
|
||||
|
||||
Reference in new issue
Block a user