1059 lines
47 KiB
Rust
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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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,
|
|
¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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,
|
|
¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_name)
|
|
));
|
|
} else if !original_default(column).is_empty() {
|
|
statements.push(format!(
|
|
"ALTER TABLE {table} MODIFY COLUMN {} REMOVE DEFAULT;",
|
|
quote_ident(StructureDialect::ClickHouse, ¤t_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, ¤t_name)
|
|
));
|
|
}
|
|
} else {
|
|
statements.push(format!(
|
|
"ALTER TABLE {table} MODIFY COLUMN {} {next_type}{position_clause};",
|
|
quote_ident(StructureDialect::ClickHouse, ¤t_name)
|
|
));
|
|
}
|
|
} else if !position_clause.is_empty() {
|
|
statements.push(format!(
|
|
"ALTER TABLE {table} MODIFY COLUMN {} {next_type}{position_clause};",
|
|
quote_ident(StructureDialect::ClickHouse, ¤t_name)
|
|
));
|
|
}
|
|
if clean(&column.comment) != original_comment(column) {
|
|
statements.push(format!(
|
|
"ALTER TABLE {table} COMMENT COLUMN {} {};",
|
|
quote_ident(StructureDialect::ClickHouse, ¤t_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("")))
|
|
}
|