feat(oracle): support adding primary keys to existing tables

Closes #6922
This commit is contained in:
t8y2 committed 2026-08-22 12:24:02 +00:00
1 parent 946677a579
commit a1e94d8228
8 files changed
+252 -23

No files matched your search

+5 -1
View File
@@ -22,7 +22,7 @@ pub use types::*;
use crate::models::connection::DatabaseType;
use columns::build_column_sql;
use columns::{build_column_sql, validate_primary_key_change_scope};
use comments::build_table_comment_sql;
use foreign_keys::build_foreign_key_sql;
use indexes::build_index_sql;
@@ -35,6 +35,10 @@ pub fn build_table_structure_change_sql(mut options: TableStructureSqlOptions) -
if options.is_gaussdb_m_mode {
options.database_type = Some(DatabaseType::Mysql);
}
let primary_key_errors = validate_primary_key_change_scope(&options);
if !primary_key_errors.is_empty() {
return TableStructureSqlResult { statements: Vec::new(), warnings: primary_key_errors };
}
// Fail closed: an unsupported concurrent-index request (existing index, or
// partitioned parent table) must never degrade into blocking index DDL
// behind the caller's back, so the whole plan is refused up front.
@@ -346,6 +346,32 @@ fn primary_key_change(options: &TableStructureSqlOptions) -> Option<PrimaryKeyCh
Some(PrimaryKeyChange { old_ids, new_ids, new_names })
}
fn unsupported_primary_key_change_warning(options: &TableStructureSqlOptions, change: &PrimaryKeyChange<'_>) -> String {
let action = if change.old_ids.is_empty() { "Adding" } else { "Changing" };
format!("{action} primary keys is not supported for {} from this editor.", database_label(options.database_type))
}
pub(super) fn validate_primary_key_change_scope(options: &TableStructureSqlOptions) -> Vec<String> {
if let Some(original) = options
.columns
.iter()
.find(|column| column.marked_for_drop && was_primary_key_column(column))
.and_then(|column| column.original.as_ref())
{
return vec![format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name)];
}
let Some(change) = primary_key_change(options) else { return Vec::new() };
let capabilities = capabilities_for(options.database_type);
let supported =
if change.old_ids.is_empty() { capabilities.add_primary_key } else { capabilities.alter_primary_key };
if supported {
Vec::new()
} else {
vec![unsupported_primary_key_change_warning(options, &change)]
}
}
fn mysql_auto_increment_touches_primary_key(column: &EditableStructureColumn, change: &PrimaryKeyChange<'_>) -> bool {
column.extra.as_ref().is_some_and(|extra| extra.auto_increment.unwrap_or(false))
&& (change.old_ids.contains(column.id.as_str()) || change.new_ids.contains(column.id.as_str()))
@@ -363,11 +389,10 @@ pub(super) fn build_primary_key_sql(
// columns is not a PK change. A draft that drops a PK column is also rejected here.
let Some(change) = primary_key_change(options) else { return Vec::new() };
if !capabilities.alter_primary_key {
warnings.push(format!(
"Changing primary keys is not supported for {} from this editor.",
database_label(options.database_type)
));
let supported =
if change.old_ids.is_empty() { capabilities.add_primary_key } else { capabilities.alter_primary_key };
if !supported {
warnings.push(unsupported_primary_key_change_warning(options, &change));
return Vec::new();
}
@@ -43,6 +43,7 @@ pub(super) struct TableStructureCapabilities {
pub(super) index_filter: bool,
pub(super) index_comment: bool,
pub(super) index_concurrent: bool,
pub(super) add_primary_key: bool,
pub(super) alter_primary_key: bool,
pub(super) foreign_key: bool,
}
@@ -65,6 +66,7 @@ impl Default for TableStructureCapabilities {
index_filter: false,
index_comment: false,
index_concurrent: false,
add_primary_key: false,
alter_primary_key: false,
foreign_key: false,
}
@@ -85,6 +87,7 @@ pub(super) fn gaussdb_m_capabilities() -> TableStructureCapabilities {
rebuild_index: true,
index_type: true,
index_comment: true,
add_primary_key: true,
alter_primary_key: true,
..TableStructureCapabilities::default()
}
@@ -112,6 +115,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
rebuild_index: true,
index_type: true,
index_comment: true,
add_primary_key: true,
alter_primary_key: true,
foreign_key: true,
..base
@@ -158,6 +162,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
index_include: true,
index_filter: true,
index_comment: true,
add_primary_key: true,
alter_primary_key: true,
foreign_key: true,
..base
@@ -188,6 +193,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
index_filter: false,
index_comment: false,
index_concurrent: false,
add_primary_key: false,
alter_primary_key: false,
foreign_key: false,
},
@@ -249,6 +255,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
drop_index: true,
rebuild_index: true,
index_type: true,
add_primary_key: true,
alter_primary_key: true,
..base
},
@@ -265,6 +272,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
drop_index: true,
rebuild_index: true,
index_type: true,
add_primary_key: true,
alter_primary_key: true,
..base
},
@@ -293,6 +301,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
drop_index: true,
rebuild_index: true,
index_type: true,
add_primary_key: true,
foreign_key: true,
..base
},
@@ -3522,6 +3522,115 @@ fn dameng_blocks_dropping_former_primary_key_column() {
assert!(result.warnings.iter().any(|warning| warning.contains("Primary key column")));
}
#[test]
fn oracle_sets_not_null_before_adding_primary_key() {
let mut id = existing_pk_column("id", "NUMBER", false, true);
id.original.as_mut().unwrap().is_nullable = true;
let result =
build_table_structure_change_sql(structure_change_options(DatabaseType::Oracle, Some("HR"), "users", vec![id]));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"HR\".\"users\" MODIFY (\"id\" NUMBER NOT NULL);",
"ALTER TABLE \"HR\".\"users\" ADD PRIMARY KEY (\"id\");",
]
);
}
#[test]
fn oracle_adds_composite_primary_key_in_draft_order() {
let mut tenant_id = existing_pk_column("tenant_id", "NUMBER", false, true);
tenant_id.id = "tenant_id".to_string();
let mut code = existing_pk_column("code", "VARCHAR2(50)", false, true);
code.id = "code".to_string();
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Oracle,
Some("HR"),
"users",
vec![code, tenant_id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE \"HR\".\"users\" ADD PRIMARY KEY (\"code\", \"tenant_id\");"]);
}
#[test]
fn oracle_adds_new_primary_key_column_before_adding_constraint() {
let mut code = column("code");
code.data_type = "VARCHAR2(50)".to_string();
code.is_nullable = false;
code.is_primary_key = true;
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Oracle,
Some("HR"),
"users",
vec![code],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"HR\".\"users\" ADD (\"code\" VARCHAR2(50));",
"ALTER TABLE \"HR\".\"users\" ADD PRIMARY KEY (\"code\");",
]
);
}
#[test]
fn oracle_rejects_existing_primary_key_changes_without_partial_sql() {
let uncheck =
vec![existing_pk_column("id", "NUMBER", true, false), existing_pk_column("name", "VARCHAR2(50)", false, false)];
let replacement = vec![
existing_pk_column("id", "NUMBER", true, false),
existing_pk_column("code", "VARCHAR2(50)", false, true),
existing_pk_column("name", "VARCHAR2(50)", false, false),
];
let second_key = vec![
existing_pk_column("id", "NUMBER", true, true),
existing_pk_column("code", "VARCHAR2(50)", false, true),
existing_pk_column("name", "VARCHAR2(50)", false, false),
];
for (case, columns) in [("uncheck", uncheck), ("replacement", replacement), ("second key", second_key)] {
let mut options = structure_change_options(DatabaseType::Oracle, Some("HR"), "users", columns);
options.indexes = vec![index("idx_users_name", &["name"])];
let result = build_table_structure_change_sql(options);
assert!(result.statements.is_empty(), "{case} must not emit partial SQL: {:?}", result.statements);
assert_eq!(result.warnings.len(), 1, "unexpected {case} warnings: {:?}", result.warnings);
assert!(
result.warnings[0].contains("Changing primary keys"),
"unexpected {case} warning: {:?}",
result.warnings
);
}
}
#[test]
fn oracle_compatible_engines_do_not_inherit_oracle_primary_key_add() {
for database_type in [DatabaseType::OceanbaseOracle, DatabaseType::Iris] {
let columns = vec![
existing_pk_column("id", "NUMBER", false, true),
existing_pk_column("name", "VARCHAR2(50)", false, false),
];
let mut options = structure_change_options(database_type, Some("APP"), "users", columns);
options.indexes = vec![index("idx_users_name", &["name"])];
let result = build_table_structure_change_sql(options);
assert!(result.statements.is_empty(), "{database_type:?} must not emit partial SQL: {:?}", result.statements);
assert_eq!(result.warnings.len(), 1, "unexpected {database_type:?} warnings: {:?}", result.warnings);
assert!(result.warnings[0].contains("Adding primary keys"));
}
}
#[test]
fn oracle_uncheck_primary_key_and_drop_column_does_not_emit_drop_column() {
// alter_primary_key is false for Oracle: unchecking PK must not unlock DROP COLUMN without a PK drop.