diff --git a/crates/dbx-core/src/table_structure_sql/create_table.rs b/crates/dbx-core/src/table_structure_sql/create_table.rs index 01222b9ad5..9b1775afe3 100644 --- a/crates/dbx-core/src/table_structure_sql/create_table.rs +++ b/crates/dbx-core/src/table_structure_sql/create_table.rs @@ -182,6 +182,7 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr &mut warnings, options.schema.as_deref(), &options.table_name, + false, )); } diff --git a/crates/dbx-core/src/table_structure_sql/indexes.rs b/crates/dbx-core/src/table_structure_sql/indexes.rs index 7bff0e00ae..57ade0c1d2 100644 --- a/crates/dbx-core/src/table_structure_sql/indexes.rs +++ b/crates/dbx-core/src/table_structure_sql/indexes.rs @@ -47,13 +47,17 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut warnings.push(format!("Primary index \"{}\" cannot be edited from this editor.", original.name)); continue; } - statements.push(build_drop_index_sql( - options.database_type, - dialect, - &table, - options.schema.as_deref(), - &original.name, - )); + let or_replace = + options.database_type == Some(DatabaseType::Dameng) && clean(&index.name) == clean(&original.name); + if !or_replace { + statements.push(build_drop_index_sql( + options.database_type, + dialect, + &table, + options.schema.as_deref(), + &original.name, + )); + } statements.extend(build_create_index_statements( dialect, &table, @@ -61,6 +65,7 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut warnings, options.schema.as_deref(), &options.table_name, + or_replace, )); continue; } @@ -76,6 +81,7 @@ pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut warnings, options.schema.as_deref(), &options.table_name, + false, )); } @@ -161,6 +167,7 @@ pub(super) fn build_create_index_statements( warnings: &mut Vec, schema: Option<&str>, table_name: &str, + or_replace: bool, ) -> Vec { let capabilities = capabilities_for(database_type_for_dialect(dialect)); let name = clean(&index.name); @@ -171,6 +178,7 @@ pub(super) fn build_create_index_statements( } let unique = if index.is_unique { "UNIQUE " } else { "" }; + let replace = if or_replace { "OR REPLACE " } else { "" }; let cols = columns .iter() .map(|column| { @@ -236,11 +244,11 @@ pub(super) fn build_create_index_statements( if dialect == StructureDialect::Sqlite { quote_ident(dialect, table_name) } else { table.to_string() }; let create_sql = if dialect == StructureDialect::Postgres { format!( - "CREATE {unique}{type_prefix}INDEX {index_name} ON {create_table}{using_clause} ({cols}){include_clause}{where_clause};" + "CREATE {replace}{unique}{type_prefix}INDEX {index_name} ON {create_table}{using_clause} ({cols}){include_clause}{where_clause};" ) } else { format!( - "CREATE {unique}{type_prefix}INDEX {index_name}{using_clause} ON {create_table} ({cols}){include_clause}{where_clause}{comment_clause};" + "CREATE {replace}{unique}{type_prefix}INDEX {index_name}{using_clause} ON {create_table} ({cols}){include_clause}{where_clause}{comment_clause};" ) }; let mut statements = vec![create_sql]; diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index 35037064db..cd277d9685 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -78,6 +78,40 @@ fn index(name: &str, columns: &[&str]) -> EditableStructureIndex { } } +fn existing_index(name: &str, columns: &[&str], is_unique: bool) -> EditableStructureIndex { + let mut index = index(name, columns); + index.is_unique = is_unique; + index.original = Some(IndexInfo { + name: name.to_string(), + columns: columns.iter().map(|column| column.to_string()).collect(), + is_unique, + is_primary: false, + filter: None, + index_type: None, + included_columns: None, + comment: None, + }); + index +} + +fn index_change_options( + database_type: DatabaseType, + schema: Option<&str>, + index: EditableStructureIndex, +) -> TableStructureSqlOptions { + TableStructureSqlOptions { + database_type: Some(database_type), + schema: schema.map(str::to_string), + table_name: "USERS".to_string(), + columns: Vec::new(), + indexes: vec![index], + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + } +} + fn foreign_key(name: &str, column: &str, ref_table: &str, ref_column: &str) -> EditableStructureForeignKey { EditableStructureForeignKey { id: name.to_string(), @@ -163,6 +197,158 @@ fn builds_mysql_column_and_index_changes() { ); } +#[test] +fn dameng_replaces_same_name_index_before_validating_uniqueness() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.name = " IDX_USERS_EMAIL ".to_string(); + changed.is_unique = true; + + let mut options = index_change_options(DatabaseType::Dameng, Some("SYSDBA"), changed); + options.table_name = "DBX_6002_USERS".to_string(); + let result = build_table_structure_change_sql(options); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE OR REPLACE UNIQUE INDEX \"IDX_USERS_EMAIL\" ON \"SYSDBA\".\"DBX_6002_USERS\" (\"EMAIL\");"] + ); +} + +#[test] +fn dameng_replaces_same_name_unique_index_with_normal_index() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], true); + changed.is_unique = false; + + let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed)); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE OR REPLACE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\");"] + ); +} + +#[test] +fn dameng_replaces_same_name_index_when_columns_change() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.columns = vec!["LOGIN".to_string(), "EMAIL".to_string()]; + + let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed)); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE OR REPLACE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"LOGIN\", \"EMAIL\");"] + ); +} + +#[test] +fn dameng_replaces_same_name_index_when_type_changes_to_bitmap() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.index_type = "bitmap".to_string(); + + let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed)); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE OR REPLACE BITMAP INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\");"] + ); +} + +#[test] +fn dameng_replaces_same_name_index_without_unsupported_comment_clause() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.comment = "New comment".to_string(); + changed.original.as_mut().unwrap().comment = Some("Old comment".to_string()); + + let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed)); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE OR REPLACE INDEX \"IDX_USERS_EMAIL\" ON \"APP\".\"USERS\" (\"EMAIL\");"] + ); +} + +#[test] +fn dameng_renamed_index_keeps_drop_then_create_path() { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.name = "IDX_USERS_LOGIN".to_string(); + + let result = build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), changed)); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "DROP INDEX \"APP\".\"IDX_USERS_EMAIL\";", + "CREATE INDEX \"IDX_USERS_LOGIN\" ON \"APP\".\"USERS\" (\"EMAIL\");", + ] + ); +} + +#[test] +fn dameng_primary_and_unchanged_indexes_keep_existing_behavior() { + let unchanged = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + let unchanged_result = + build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), unchanged)); + assert_eq!(unchanged_result.warnings, Vec::::new()); + assert!(unchanged_result.statements.is_empty()); + + let mut primary = existing_index("PK_USERS", &["ID"], true); + primary.columns.push("TENANT_ID".to_string()); + primary.original.as_mut().unwrap().is_primary = true; + let primary_result = + build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), primary)); + assert!(primary_result.statements.is_empty()); + assert_eq!(primary_result.warnings, vec!["Primary index \"PK_USERS\" cannot be edited from this editor."]); +} + +#[test] +fn dameng_new_and_dropped_indexes_do_not_use_or_replace() { + let new_index = index("IDX_USERS_LOGIN", &["LOGIN"]); + let new_result = + build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), new_index)); + assert_eq!(new_result.warnings, Vec::::new()); + assert_eq!(new_result.statements, vec!["CREATE INDEX \"IDX_USERS_LOGIN\" ON \"APP\".\"USERS\" (\"LOGIN\");"]); + + let mut dropped = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + dropped.marked_for_drop = true; + let drop_result = + build_table_structure_change_sql(index_change_options(DatabaseType::Dameng, Some("APP"), dropped)); + assert_eq!(drop_result.warnings, Vec::::new()); + assert_eq!(drop_result.statements, vec!["DROP INDEX \"APP\".\"IDX_USERS_EMAIL\";"]); +} + +#[test] +fn non_dameng_index_rebuilds_do_not_use_or_replace() { + for database_type in [ + DatabaseType::Mysql, + DatabaseType::Postgres, + DatabaseType::Sqlite, + DatabaseType::SqlServer, + DatabaseType::Oracle, + DatabaseType::Oscar, + DatabaseType::H2, + DatabaseType::Informix, + DatabaseType::Iris, + ] { + let mut changed = existing_index("IDX_USERS_EMAIL", &["EMAIL"], false); + changed.is_unique = true; + let result = build_table_structure_change_sql(index_change_options(database_type, None, changed)); + + assert_eq!(result.warnings, Vec::::new(), "database type: {database_type:?}"); + assert_eq!(result.statements.len(), 2, "database type: {database_type:?}"); + assert!(result.statements[0].starts_with("DROP INDEX "), "database type: {database_type:?}"); + assert!(result.statements[1].starts_with("CREATE UNIQUE INDEX "), "database type: {database_type:?}"); + assert!( + result.statements.iter().all(|statement| !statement.contains("OR REPLACE")), + "database type: {database_type:?}" + ); + } +} + #[test] fn builds_xugu_type_change_with_native_syntax() { let mut code = column("code");