Files
dbx/crates/dbx-sql/src/table_structure_sql/column_alter.rs
T

1059 lines
47 KiB
Rust

use super::column_format::{
clickhouse_column_type, column_data_type, column_definition, has_dameng_identity,
is_dameng_identity_compatible_type, is_mysql_character_data_type, original_is_mysql_generated_column,
original_mysql_generated_clause,
};
use super::columns::build_drop_column_sql;
use super::comments::build_sqlserver_column_comment_sql_for_profile;
use super::dialect::{capabilities_for, database_label, StructureDialect};
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
use super::util::{
clean, format_default_for_sql, is_protected_manticore_id_column, normalize_default, original_comment,
original_default, qualified_table, quote_ident, quote_string,
};
use crate::table_structure_sql::ColumnExtra;
pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> TableStructureSqlResult {
let capabilities = capabilities_for(options.database_type, options.driver_profile.as_deref());
let dialect = capabilities.dialect;
let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name);
let database_label = database_label(options.database_type);
let mut warnings = Vec::new();
let mut statements = Vec::new();
if options.column.marked_for_drop {
let Some(original) = &options.column.original else {
warnings.push("No original column info available.".to_string());
return TableStructureSqlResult { statements, warnings };
};
if !capabilities.drop_column {
warnings.push(format!("Dropping columns is not supported for {database_label} from this editor."));
return TableStructureSqlResult { statements, warnings };
}
if original.is_primary_key {
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
return TableStructureSqlResult { statements, warnings };
}
if is_protected_manticore_id_column(dialect, &original.name) {
warnings.push("Manticore Search id column cannot be dropped from this editor.".to_string());
return TableStructureSqlResult { statements, warnings };
}
statements.push(build_drop_column_sql(dialect, &table, &original.name));
return TableStructureSqlResult { statements, warnings };
}
let Some(original) = &options.column.original else {
warnings.push(
"This column has no original state — ALTER statements are only available for existing columns.".to_string(),
);
return TableStructureSqlResult { statements, warnings };
};
if !has_existing_column_attribute_change(&options.column) && !has_column_extra_change(&options.column) {
warnings.push("No changes detected for this column.".to_string());
return TableStructureSqlResult { statements, warnings };
}
let has_rename = options.column.name != original.name;
let has_comment_change = clean(&options.column.comment) != original_comment(&options.column);
let has_attribute_change = options.column.data_type.trim() != original.data_type.trim()
|| options.column.is_nullable != original.is_nullable
|| normalize_default(Some(&options.column.default_value)) != original_default(&options.column)
|| (has_comment_change && capabilities.comment)
|| (is_mysql_character_data_type(&options.column.data_type)
&& (options.column.character_set.trim() != original.character_set.as_deref().unwrap_or("")
|| options.column.collation.trim() != original.collation.as_deref().unwrap_or("")));
if has_comment_change && !capabilities.comment {
warnings.push(format!(
"Column comments are not supported for {database_label} from this editor; the comment change for \"{}\" was ignored.",
original.name
));
}
if has_rename && !capabilities.rename_column {
warnings.push(format!("Renaming columns is not supported for {database_label} from this editor."));
}
if has_attribute_change && !capabilities.alter_existing_column && dialect != StructureDialect::Sqlite {
warnings.push(format!("Editing existing columns is not supported for {database_label} yet."));
}
if (has_rename && !capabilities.rename_column)
|| (has_attribute_change && !capabilities.alter_existing_column && dialect != StructureDialect::Sqlite)
{
return TableStructureSqlResult { statements, warnings };
}
if dialect == StructureDialect::Mysql
&& original_is_mysql_generated_column(&options.column)
&& original_mysql_generated_clause(&options.column).is_none()
{
warnings.push(format!(
"Column \"{}\" is generated, but its generation expression could not be loaded; no ALTER statement was generated to avoid removing the generated-column definition.",
original.name
));
return TableStructureSqlResult { statements, warnings };
}
if !has_rename && !has_attribute_change && !has_column_extra_change(&options.column) {
return TableStructureSqlResult { statements, warnings };
}
match dialect {
StructureDialect::Mysql => statements.extend(build_mysql_existing_column_sql(&table, &options.column, "")),
StructureDialect::Doris => statements.extend(build_doris_existing_column_sql(&table, &options.column, "")),
StructureDialect::Postgres => statements.extend(build_postgres_existing_column_sql(&table, &options.column)),
StructureDialect::Oracle => {
if options.database_type == Some(crate::models::connection::DatabaseType::Iris) {
statements.extend(build_iris_existing_column_sql(&table, &options.column));
} else if options.database_type == Some(crate::models::connection::DatabaseType::Xugu) {
statements.extend(build_xugu_existing_column_sql(&table, &options.column));
} else {
statements.extend(build_oracle_like_existing_column_sql(dialect, &table, &options.column))
}
}
StructureDialect::Dameng => {
statements.extend(build_dameng_existing_column_sql(&table, &options.column, true, &mut warnings))
}
StructureDialect::Oscar => statements.extend(build_oscar_existing_column_sql(dialect, &table, &options.column)),
StructureDialect::H2 => statements.extend(build_h2_existing_column_sql(&table, &options.column)),
StructureDialect::ClickHouse => {
statements.extend(build_clickhouse_existing_column_sql(&table, &options.column, ""))
}
StructureDialect::Informix => statements.extend(build_informix_existing_column_sql(&table, &options.column)),
StructureDialect::SqlServer => statements.extend(build_sqlserver_existing_column_sql(
&table,
&options.column,
options.schema.as_deref(),
&options.table_name,
options.driver_profile.as_deref(),
&mut warnings,
)),
StructureDialect::Sqlite => {
statements.extend(build_sqlite_existing_column_sql(&table, &options.column, &mut warnings))
}
_ => warnings.push(format!("Editing existing columns is not supported for {database_label} yet.")),
}
TableStructureSqlResult { statements, warnings }
}
fn is_column_extra_empty(extra: &ColumnExtra) -> bool {
!extra.auto_increment.unwrap_or(false)
&& !extra.on_update_current_timestamp.unwrap_or(false)
&& extra.identity.is_none()
&& !extra.manticore_indexed.unwrap_or(false)
&& !extra.manticore_stored.unwrap_or(false)
&& !extra.manticore_attribute.unwrap_or(false)
&& !extra.manticore_secondary_index.unwrap_or(false)
}
fn original_manticore_extra_flags(extra: &str) -> (bool, bool, bool, bool) {
let lower = extra.to_lowercase();
let mut tokens = lower.split_whitespace();
let first = tokens.next();
let is_generated_column = first.is_some_and(|token| token == "generated")
|| matches!(first, Some("virtual" | "stored" | "persistent"))
&& tokens.next().is_some_and(|token| token == "generated");
if is_generated_column {
return (false, false, false, false);
}
(
lower.split_whitespace().any(|token| token == "indexed"),
lower.split_whitespace().any(|token| token == "stored"),
lower.split_whitespace().any(|token| token == "attribute"),
lower.contains("secondary_index='1'")
|| lower.contains("secondary_index=\"1\"")
|| lower.contains("secondary_index=1"),
)
}
fn original_has_auto_increment(extra: &str) -> bool {
let lower = extra.to_lowercase();
lower.contains("auto_increment") || lower.contains("autoincrement")
}
fn original_has_identity(extra: &str) -> bool {
extra.to_lowercase().contains("identity")
}
fn sqlserver_identity_values(extra: &str) -> Option<(Option<i64>, Option<i64>)> {
let lower = extra.to_lowercase();
let identity_index = lower.find("identity")?;
let after_identity = extra.get(identity_index + "identity".len()..)?.trim_start();
if !after_identity.starts_with('(') {
return Some((None, None));
}
let close_index = after_identity.find(')')?;
let args = &after_identity[1..close_index];
let mut parts = args.split(',').map(|part| part.trim().parse::<i64>().ok());
Some((parts.next().flatten(), parts.next().flatten()))
}
fn sqlserver_identity_matches_original(identity: &super::types::ColumnIdentity, original_extra: &str) -> bool {
let Some((original_seed, original_increment)) = sqlserver_identity_values(original_extra) else {
return false;
};
identity.seed.unwrap_or(1) == original_seed.unwrap_or(1)
&& identity.increment.unwrap_or(1) == original_increment.unwrap_or(1)
}
fn parse_i64_after_phrase(value: &str, lower: &str, phrase: &str) -> Option<i64> {
let start = lower.find(phrase)? + phrase.len();
let rest = value.get(start..)?.trim_start();
let token = rest.split(|ch: char| ch.is_whitespace() || ch == ')' || ch == ',').find(|part| !part.is_empty())?;
token.parse::<i64>().ok()
}
fn postgres_identity_values(extra: &str) -> Option<(String, Option<i64>, Option<i64>)> {
let lower = extra.to_lowercase();
let generation = if lower.contains("generated always as identity") {
"ALWAYS".to_string()
} else if lower.contains("generated by default as identity") {
"BY DEFAULT".to_string()
} else {
return None;
};
let seed = parse_i64_after_phrase(extra, &lower, "start with");
let increment = parse_i64_after_phrase(extra, &lower, "increment by");
Some((generation, seed, increment))
}
fn identity_matches_original(identity: &super::types::ColumnIdentity, original_extra: &str) -> bool {
if let Some((generation, original_seed, original_increment)) = postgres_identity_values(original_extra) {
return identity.generation.as_deref().unwrap_or("BY DEFAULT").eq_ignore_ascii_case(&generation)
&& identity.seed.unwrap_or(1) == original_seed.unwrap_or(1)
&& identity.increment.unwrap_or(1) == original_increment.unwrap_or(1);
}
sqlserver_identity_matches_original(identity, original_extra)
}
#[derive(Clone, Copy)]
enum DamengIdentityTransition {
None,
Add { seed: i64, increment: i64 },
Drop,
ParametersChanged,
}
fn dameng_identity_transition(column: &EditableStructureColumn) -> DamengIdentityTransition {
let Some(original) = &column.original else {
return DamengIdentityTransition::None;
};
let original_extra = original.extra.as_deref().unwrap_or("");
let original_identity = original_has_identity(original_extra);
let current_identity = has_dameng_identity(column);
match (original_identity, current_identity) {
(false, false) => DamengIdentityTransition::None,
(false, true) => {
let identity = column.extra.as_ref().and_then(|extra| extra.identity.as_ref());
DamengIdentityTransition::Add {
seed: identity.and_then(|identity| identity.seed).unwrap_or(1),
increment: identity.and_then(|identity| identity.increment).unwrap_or(1),
}
}
(true, false) => DamengIdentityTransition::Drop,
(true, true) => match column.extra.as_ref().and_then(|extra| extra.identity.as_ref()) {
Some(identity) if !identity_matches_original(identity, original_extra) => {
DamengIdentityTransition::ParametersChanged
}
_ => DamengIdentityTransition::None,
},
}
}
pub(super) fn dameng_drops_identity(column: &EditableStructureColumn) -> bool {
matches!(dameng_identity_transition(column), DamengIdentityTransition::Drop)
}
pub(super) fn validate_dameng_existing_identity_change(
column: &EditableStructureColumn,
warnings: &mut Vec<String>,
) -> bool {
match dameng_identity_transition(column) {
DamengIdentityTransition::Add { increment, .. } => {
if !is_dameng_identity_compatible_type(&column.data_type) {
warnings.push(format!(
"Dameng identity column \"{}\" must use tinyint, smallint, int, integer, bigint, number, numeric, or decimal/dec with scale 0.",
column.name
));
} else if column.is_nullable {
warnings.push(format!(
"Dameng identity column \"{}\" must be NOT NULL before identity can be enabled.",
column.name
));
} else if increment == 0 {
warnings.push(format!("Dameng identity column \"{}\" increment cannot be 0.", column.name));
} else {
return true;
}
false
}
DamengIdentityTransition::ParametersChanged => {
warnings.push(format!(
"Changing Dameng IDENTITY seed or increment for existing column \"{}\" is not supported from this editor.",
column.name
));
false
}
DamengIdentityTransition::None | DamengIdentityTransition::Drop => true,
}
}
pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool {
let Some(original) = &column.original else { return false };
let current_extra = column.extra.as_ref();
match (current_extra, original.extra.as_deref()) {
// Neither has extra → no change
(None, None | Some("")) => false,
// Current extra is empty (all None) → changed only if the original had effective extra
(Some(curr), None | Some("")) if is_column_extra_empty(curr) => false,
(Some(curr), Some(orig)) if is_column_extra_empty(curr) => {
let (indexed, stored, attribute, secondary_index) = original_manticore_extra_flags(orig);
let orig_lower = orig.to_lowercase();
original_has_auto_increment(orig)
|| orig_lower.contains("on update")
|| original_has_identity(orig)
|| indexed
|| stored
|| attribute
|| secondary_index
}
// Extra added or removed
(Some(_), None | Some("")) => true,
(None, Some(_)) => true,
// Both have extra → check auto_increment and on_update_current_timestamp flags
(Some(curr), Some(orig)) => {
let orig_lower = orig.to_lowercase();
let curr_has_ai = curr.auto_increment.unwrap_or(false);
let orig_has_identity = original_has_identity(orig);
let orig_has_ai = original_has_auto_increment(orig) || (orig_has_identity && curr_has_ai);
let curr_has_on_update = curr.on_update_current_timestamp.unwrap_or(false);
let orig_has_on_update = orig_lower.contains("on update");
let identity_changed = match (&curr.identity, orig_has_identity) {
(Some(identity), true) => !identity_matches_original(identity, orig),
(Some(_), false) => true,
(None, true) => !curr_has_ai,
(None, false) => false,
};
let curr_manticore = (
curr.manticore_indexed.unwrap_or(false),
curr.manticore_stored.unwrap_or(false),
curr.manticore_attribute.unwrap_or(false),
curr.manticore_secondary_index.unwrap_or(false),
);
let orig_manticore = original_manticore_extra_flags(orig);
curr_has_ai != orig_has_ai
|| curr_has_on_update != orig_has_on_update
|| identity_changed
|| curr_manticore != orig_manticore
}
}
}
pub(super) fn build_mysql_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
position_clause: &str,
) -> Vec<String> {
let operation = build_mysql_existing_column_clause(column, position_clause);
vec![format!("ALTER TABLE {table} {operation};")]
}
pub(super) fn build_mysql_existing_column_clause(column: &EditableStructureColumn, position_clause: &str) -> String {
let original_name = column.original.as_ref().map(|original| original.name.as_str()).unwrap_or(&column.name);
let operation = if column.name == original_name {
format!("MODIFY COLUMN {}", column_definition(StructureDialect::Mysql, column))
} else {
format!(
"CHANGE COLUMN {} {}",
quote_ident(StructureDialect::Mysql, original_name),
column_definition(StructureDialect::Mysql, column)
)
};
format!("{operation}{position_clause}")
}
pub(super) fn build_doris_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
position_clause: &str,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let mut current_column = column.clone();
if column.name != original.name {
// Doris follows its own lightweight schema-change grammar: no MySQL CHANGE and no TO keyword.
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} {};",
quote_ident(StructureDialect::Doris, &original.name),
quote_ident(StructureDialect::Doris, &column.name)
));
current_column.name = column.name.clone();
}
let type_changed = column.data_type.trim() != original.data_type.trim();
let nullable_changed = column.is_nullable != original.is_nullable;
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
let comment_changed = clean(&column.comment) != original_comment(column);
if type_changed || nullable_changed || default_changed || comment_changed || !position_clause.is_empty() {
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {}{position_clause};",
column_definition(StructureDialect::Doris, &current_column)
));
}
statements
}
pub(super) fn build_postgres_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
build_postgres_like_existing_column_sql(table, column, false)
}
pub(super) fn build_xugu_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
// Xugu shares PostgreSQL's per-attribute ALTER flow, but its type clause omits TYPE entirely.
build_postgres_like_existing_column_sql(table, column, true)
}
fn build_postgres_like_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
use_xugu_type_syntax: bool,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let current_name = &column.name;
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(StructureDialect::Postgres, &original.name),
quote_ident(StructureDialect::Postgres, &column.name)
));
}
let type_changed = column.data_type.trim() != original.data_type.trim();
if type_changed {
let column_name = quote_ident(StructureDialect::Postgres, current_name);
let data_type = column_data_type(StructureDialect::Postgres, column);
if use_xugu_type_syntax {
statements.push(format!("ALTER TABLE {table} ALTER COLUMN {column_name} {data_type};"));
} else {
let current_default = normalize_default(Some(&column.default_value));
let mut alterations = Vec::new();
if !original_default(column).is_empty() {
alterations.push(format!("ALTER COLUMN {column_name} DROP DEFAULT"));
}
// PostgreSQL assignment casts do not cover many valid explicit conversions. USING keeps
// type changes generic for built-ins, arrays, and domains while still rejecting bad data.
alterations.push(format!("ALTER COLUMN {column_name} TYPE {data_type} USING {column_name}::{data_type}"));
if !current_default.is_empty() {
alterations.push(format!(
"ALTER COLUMN {column_name} SET DEFAULT {}",
format_default_for_sql(StructureDialect::Postgres, &column.data_type, &current_default)
));
}
// USING is not applied to defaults. Keeping all three actions in one ALTER TABLE makes
// an incompatible restored default fail atomically instead of leaving it dropped.
statements.push(format!("ALTER TABLE {table} {};", alterations.join(", ")));
}
}
if column.is_nullable != original.is_nullable {
let action = if column.is_nullable { "DROP NOT NULL" } else { "SET NOT NULL" };
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {action};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if (!type_changed || use_xugu_type_syntax)
&& normalize_default(Some(&column.default_value)) != original_default(column)
{
let default_value = normalize_default(Some(&column.default_value));
let action = if default_value.is_empty() {
"DROP DEFAULT".to_string()
} else {
format!(
"SET DEFAULT {}",
format_default_for_sql(StructureDialect::Postgres, &column.data_type, &default_value)
)
};
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {action};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if clean(&column.comment) != original_comment(column) {
let comment_value =
if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
statements.push(format!(
"COMMENT ON COLUMN {table}.{} IS {comment_value};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
statements
}
pub(super) fn build_informix_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let dialect = StructureDialect::Informix;
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"RENAME COLUMN {table}.{} TO {};",
quote_ident(dialect, &original.name),
quote_ident(dialect, &column.name)
));
current_name = column.name.clone();
}
let type_changed = column.data_type.trim() != original.data_type.trim();
let nullable_changed = column.is_nullable != original.is_nullable;
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
if type_changed || nullable_changed || default_changed {
let mut parts = vec![quote_ident(dialect, &current_name), column_data_type(dialect, column)];
if column.is_nullable {
parts.push("NULL".to_string());
} else {
parts.push("NOT NULL".to_string());
}
let default_value = normalize_default(Some(&column.default_value));
if !default_value.is_empty() {
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
} else if default_changed {
parts.push("DEFAULT NULL".to_string());
}
statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
}
statements
}
pub(super) fn build_oracle_like_existing_column_sql(
dialect: StructureDialect,
table: &str,
column: &EditableStructureColumn,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(dialect, &original.name),
quote_ident(dialect, &column.name)
));
current_name = column.name.clone();
}
let type_changed = column.data_type.trim() != original.data_type.trim();
let nullable_changed = column.is_nullable != original.is_nullable;
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
if type_changed || nullable_changed || default_changed {
let data_type = column_data_type(dialect, column);
let mut parts = vec![quote_ident(dialect, &current_name), data_type];
let default_value = normalize_default(Some(&column.default_value));
if !default_value.is_empty() {
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
} else if default_changed {
// User cleared the default — explicitly drop it.
parts.push("DEFAULT NULL".to_string());
}
if nullable_changed {
if column.is_nullable {
parts.push("NULL".to_string());
} else {
parts.push("NOT NULL".to_string());
}
}
statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
}
if clean(&column.comment) != original_comment(column) {
let comment_value =
if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
statements
.push(format!("COMMENT ON COLUMN {table}.{} IS {comment_value};", quote_ident(dialect, &current_name)));
}
statements
}
pub(super) fn build_dameng_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
emit_identity_drop: bool,
warnings: &mut Vec<String>,
) -> Vec<String> {
let transition = dameng_identity_transition(column);
if !validate_dameng_existing_identity_change(column, warnings) {
return Vec::new();
}
let mut statements = Vec::new();
if emit_identity_drop && matches!(transition, DamengIdentityTransition::Drop) {
statements.push(format!("ALTER TABLE {table} DROP IDENTITY;"));
}
statements.extend(build_oracle_like_existing_column_sql(StructureDialect::Dameng, table, column));
if let DamengIdentityTransition::Add { seed, increment } = transition {
statements.push(format!(
"ALTER TABLE {table} ADD COLUMN {} IDENTITY({seed}, {increment});",
quote_ident(StructureDialect::Dameng, &column.name)
));
}
statements
}
pub(super) fn build_iris_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let dialect = StructureDialect::Oracle;
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} RENAME {};",
quote_ident(dialect, &original.name),
quote_ident(dialect, &column.name)
));
current_name = column.name.clone();
}
let type_changed = column.data_type.trim() != original.data_type.trim();
let nullable_changed = column.is_nullable != original.is_nullable;
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
if type_changed || nullable_changed || default_changed {
let mut parts = vec![quote_ident(dialect, &current_name), column_data_type(dialect, column)];
let default_value = normalize_default(Some(&column.default_value));
if !default_value.is_empty() {
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
} else if default_changed {
parts.push("DEFAULT NULL".to_string());
}
if nullable_changed {
parts.push(if column.is_nullable { "NULL".to_string() } else { "NOT NULL".to_string() });
}
statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
}
statements
}
/// 神通 Oscar 的 ALTER 已有列 SQL。
///
/// 神通的 `ALTER TABLE ... MODIFY` 语法与 Oracle 有重要差异(实测 v7.0.8):
/// 带圆括号的 `MODIFY (col TYPE [DEFAULT ...])` 不允许出现 `NULL`/`NOT NULL`,否则
/// parser 报 `syntax error at or near "NULL"`。要改可空性必须用不带括号、不带类型的
/// `MODIFY col NOT NULL` / `MODIFY col NULL` 单独一条。因此类型/默认值变更与可空性
/// 变更需拆成两条语句,而不能像 Oracle/Dameng 那样合并进单个 `MODIFY (...)`。
pub(super) fn build_oscar_existing_column_sql(
dialect: StructureDialect,
table: &str,
column: &EditableStructureColumn,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(dialect, &original.name),
quote_ident(dialect, &column.name)
));
current_name = column.name.clone();
}
let type_changed = column.data_type.trim() != original.data_type.trim();
let nullable_changed = column.is_nullable != original.is_nullable;
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
// 类型或默认值变更:带括号的 MODIFY 只允许 "col TYPE [DEFAULT ...]",不含 NULL/NOT NULL。
if type_changed || default_changed {
let data_type = column_data_type(dialect, column);
let mut parts = vec![quote_ident(dialect, &current_name), data_type];
let default_value = normalize_default(Some(&column.default_value));
if !default_value.is_empty() {
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
} else if default_changed {
// User cleared the default — explicitly drop it. MODIFY (col DEFAULT NULL) 合法。
parts.push("DEFAULT NULL".to_string());
}
statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
}
// 可空性变更:神通要求不带括号、不带类型的单独 MODIFY,否则 parser 报错。
if nullable_changed {
let nullability = if column.is_nullable { "NULL" } else { "NOT NULL" };
statements.push(format!("ALTER TABLE {table} MODIFY {} {};", quote_ident(dialect, &current_name), nullability));
}
if clean(&column.comment) != original_comment(column) {
let comment_value =
if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
statements
.push(format!("COMMENT ON COLUMN {table}.{} IS {comment_value};", quote_ident(dialect, &current_name)));
}
statements
}
pub(super) fn build_sqlserver_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
schema: Option<&str>,
table_name: &str,
driver_profile: Option<&str>,
warnings: &mut Vec<String>,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let dialect = StructureDialect::SqlServer;
let mut statements = Vec::new();
let mut current_name = original.name.clone();
let has_column_definition_change =
column.data_type.trim() != original.data_type.trim() || column.is_nullable != original.is_nullable;
let default_value = normalize_default(Some(&column.default_value));
let has_default_change = default_value != original_default(column);
let has_old_default = original
.column_default
.as_deref()
.is_some_and(|value| !value.trim().is_empty() && !value.trim().eq_ignore_ascii_case("null"));
if has_sqlserver_identity_change(column) {
warnings.push(format!(
"Changing SQL Server IDENTITY for existing column \"{}\" is not supported from this editor.",
original.name
));
}
// Rename column via sp_rename
if column.name != original.name {
let full_obj_path = format!("{table}.{}", quote_ident(dialect, &original.name));
statements.push(format!(
"EXEC sp_rename '{full_obj_path}', '{new_name}', 'COLUMN';",
full_obj_path = full_obj_path.replace('\'', "''"),
new_name = column.name.replace('\'', "''")
));
current_name = column.name.clone();
}
// SQL Server default constraints are separate objects that can block ALTER COLUMN.
// Preserve the exact constraint name and expression when the default itself is unchanged.
if has_column_definition_change && has_old_default && !has_default_change {
statements.push(build_sqlserver_alter_column_preserving_default_sql(
table,
&current_name,
&column_data_type(dialect, column),
column.is_nullable,
));
} else {
if has_column_definition_change && has_old_default {
statements.push(build_sqlserver_drop_default_constraint_sql(table, &current_name));
}
if has_column_definition_change {
let null_clause = if column.is_nullable { "NULL" } else { "NOT NULL" };
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {} {null_clause};",
quote_ident(dialect, &current_name),
column_data_type(dialect, column)
));
}
}
// Default value changes via ADD/DROP CONSTRAINT.
if has_default_change {
if has_old_default && !has_column_definition_change {
statements.push(build_sqlserver_drop_default_constraint_sql(table, &current_name));
}
if !default_value.is_empty() {
let constraint_name = format!("DF_{}_{}", table_name.trim(), current_name.trim());
statements.push(format!(
"ALTER TABLE {table} ADD CONSTRAINT {} DEFAULT {} FOR {};",
quote_ident(dialect, &constraint_name),
format_default_for_sql(StructureDialect::SqlServer, &column.data_type, &default_value),
quote_ident(dialect, &current_name)
));
}
}
// Column comment changes via extended properties
if clean(&column.comment) != original_comment(column) {
statements.extend(build_sqlserver_column_comment_sql_for_profile(
table,
schema,
table_name,
&current_name,
&column.comment,
driver_profile,
));
}
statements
}
pub fn build_sqlserver_alter_column_preserving_default_sql(
table: &str,
column_name: &str,
data_type: &str,
is_nullable: bool,
) -> String {
let dialect = StructureDialect::SqlServer;
let sql_var = sqlserver_default_constraint_sql_var(table, column_name);
let name_var = format!("{sql_var}_name");
let definition_var = format!("{sql_var}_definition");
let table_literal = table.replace('\'', "''");
let column_literal = column_name.replace('\'', "''");
let quoted_column = quote_ident(dialect, column_name);
let quoted_column_literal = quoted_column.replace('\'', "''");
let null_clause = if is_nullable { "NULL" } else { "NOT NULL" };
format!(
"DECLARE {sql_var} NVARCHAR(MAX), {name_var} sysname, {definition_var} NVARCHAR(MAX); \
SELECT TOP (1) {name_var} = dc.name, {definition_var} = dc.definition \
FROM sys.default_constraints AS dc \
WHERE dc.parent_object_id = OBJECT_ID(N'{table_literal}') AND dc.parent_column_id = COLUMNPROPERTY(OBJECT_ID(N'{table_literal}'), N'{column_literal}', 'ColumnId'); \
IF {name_var} IS NOT NULL BEGIN SET {sql_var} = N'ALTER TABLE {table_literal} DROP CONSTRAINT ' + QUOTENAME({name_var}); EXEC sp_executesql {sql_var}; END; \
ALTER TABLE {table} ALTER COLUMN {quoted_column} {data_type} {null_clause}; \
IF {name_var} IS NOT NULL BEGIN SET {sql_var} = N'ALTER TABLE {table_literal} ADD CONSTRAINT ' + QUOTENAME({name_var}) + N' DEFAULT ' + {definition_var} + N' FOR {quoted_column_literal}'; EXEC sp_executesql {sql_var}; END;",
data_type = data_type,
)
}
pub fn build_sqlserver_drop_default_constraint_sql(table: &str, column_name: &str) -> String {
let sql_var = sqlserver_default_constraint_sql_var(table, column_name);
let table_literal = table.replace('\'', "''");
let column_literal = column_name.replace('\'', "''");
format!(
"DECLARE {sql_var} NVARCHAR(MAX); \
SELECT TOP (1) {sql_var} = N'ALTER TABLE {table_literal} DROP CONSTRAINT ' + QUOTENAME(dc.name) \
FROM sys.default_constraints AS dc \
WHERE dc.parent_object_id = OBJECT_ID(N'{table_literal}') AND dc.parent_column_id = COLUMNPROPERTY(OBJECT_ID(N'{table_literal}'), N'{column_literal}', 'ColumnId'); \
IF {sql_var} IS NOT NULL EXEC sp_executesql {sql_var};"
)
}
fn sqlserver_default_constraint_sql_var(table: &str, column_name: &str) -> String {
let mut hash = 0x811c_9dc5u32;
for byte in table.bytes().chain([0]).chain(column_name.bytes()) {
hash ^= u32::from(byte);
hash = hash.wrapping_mul(0x0100_0193);
}
format!("@dbx_default_sql_{hash:08x}")
}
fn has_sqlserver_identity_change(column: &EditableStructureColumn) -> bool {
let Some(original) = &column.original else {
return false;
};
let original_extra = original.extra.as_deref().unwrap_or("");
let original_identity = original_has_identity(original_extra);
let current_extra = column.extra.as_ref();
let current_identity =
current_extra.is_some_and(|extra| extra.auto_increment.unwrap_or(false) || extra.identity.is_some());
if current_identity != original_identity {
return true;
}
if !current_identity {
return false;
}
current_extra
.and_then(|extra| extra.identity.as_ref())
.is_some_and(|identity| !sqlserver_identity_matches_original(identity, original_extra))
}
pub(super) fn build_h2_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} RENAME TO {};",
quote_ident(StructureDialect::H2, &original.name),
quote_ident(StructureDialect::H2, &column.name)
));
current_name = column.name.clone();
}
if column.data_type.trim() != original.data_type.trim() {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} SET DATA TYPE {};",
quote_ident(StructureDialect::H2, &current_name),
column_data_type(StructureDialect::H2, column)
));
}
if column.is_nullable != original.is_nullable {
let action = if column.is_nullable { "DROP NOT NULL" } else { "SET NOT NULL" };
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {action};",
quote_ident(StructureDialect::H2, &current_name)
));
}
if normalize_default(Some(&column.default_value)) != original_default(column) {
let default_value = normalize_default(Some(&column.default_value));
let action = if default_value.is_empty() {
"DROP DEFAULT".to_string()
} else {
format!("SET DEFAULT {}", format_default_for_sql(StructureDialect::H2, &column.data_type, &default_value))
};
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {action};",
quote_ident(StructureDialect::H2, &current_name)
));
}
if clean(&column.comment) != original_comment(column) {
let comment_value =
if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
statements.push(format!(
"COMMENT ON COLUMN {table}.{} IS {comment_value};",
quote_ident(StructureDialect::H2, &current_name)
));
}
statements
}
pub(super) fn build_clickhouse_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
position_clause: &str,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let mut current_name = original.name.clone();
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(StructureDialect::ClickHouse, &original.name),
quote_ident(StructureDialect::ClickHouse, &column.name)
));
current_name = column.name.clone();
}
let next_type = clickhouse_column_type(column);
if next_type != original.data_type.trim()
|| normalize_default(Some(&column.default_value)) != original_default(column)
{
let default_value = normalize_default(Some(&column.default_value));
if !default_value.is_empty() {
let default_sql = format_default_for_sql(StructureDialect::ClickHouse, &column.data_type, &default_value);
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {} {next_type} DEFAULT {default_sql}{position_clause};",
quote_ident(StructureDialect::ClickHouse, &current_name)
));
} else if !original_default(column).is_empty() {
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {} REMOVE DEFAULT;",
quote_ident(StructureDialect::ClickHouse, &current_name)
));
if next_type != original.data_type.trim() || !position_clause.is_empty() {
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {} {next_type}{position_clause};",
quote_ident(StructureDialect::ClickHouse, &current_name)
));
}
} else {
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {} {next_type}{position_clause};",
quote_ident(StructureDialect::ClickHouse, &current_name)
));
}
} else if !position_clause.is_empty() {
statements.push(format!(
"ALTER TABLE {table} MODIFY COLUMN {} {next_type}{position_clause};",
quote_ident(StructureDialect::ClickHouse, &current_name)
));
}
if clean(&column.comment) != original_comment(column) {
statements.push(format!(
"ALTER TABLE {table} COMMENT COLUMN {} {};",
quote_ident(StructureDialect::ClickHouse, &current_name),
quote_string(&clean(&column.comment))
));
}
statements
}
pub(super) fn build_sqlite_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
warnings: &mut Vec<String>,
) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
// Tri-state: an unset auto_increment flag inherits the original value; only an
// explicit toggle (SQLite cannot add or drop AUTOINCREMENT without a rebuild)
// counts as an unsupported change.
let original_auto_increment = original.extra.as_deref().is_some_and(|extra| {
let lower = extra.to_ascii_lowercase();
lower.contains("auto_increment")
|| lower.contains("autoincrement")
|| lower.contains("identity")
|| lower.contains("serial")
});
let auto_increment_changed =
column.extra.as_ref().and_then(|e| e.auto_increment).is_some_and(|value| value != original_auto_increment);
let unsupported_change = column.data_type.trim() != original.data_type.trim()
|| column.is_nullable != original.is_nullable
|| normalize_default(Some(&column.default_value)) != original_default(column)
|| clean(&column.comment) != original_comment(column)
|| auto_increment_changed;
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(StructureDialect::Sqlite, &original.name),
quote_ident(StructureDialect::Sqlite, &column.name)
));
}
if unsupported_change {
warnings.push(format!(
"SQLite cannot safely alter existing column \"{}\" without rebuilding the table.",
original.name
));
}
statements
}
pub(super) fn build_questdb_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let current_name = &column.name;
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(StructureDialect::Questdb, &original.name),
quote_ident(StructureDialect::Questdb, &column.name)
));
}
if column.data_type.trim() != original.data_type.trim() {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} TYPE {};",
quote_ident(StructureDialect::Questdb, current_name),
column_data_type(StructureDialect::Questdb, column)
));
}
statements
}
pub(super) fn has_existing_column_attribute_change(column: &EditableStructureColumn) -> bool {
let Some(original) = &column.original else {
return false;
};
column.name != original.name
|| column.data_type.trim() != original.data_type.trim()
|| column.is_nullable != original.is_nullable
|| normalize_default(Some(&column.default_value)) != original_default(column)
|| clean(&column.comment) != original_comment(column)
|| (is_mysql_character_data_type(&column.data_type)
&& (column.character_set.trim() != original.character_set.as_deref().unwrap_or("")
|| column.collation.trim() != original.collation.as_deref().unwrap_or("")))
}