diff --git a/apps/desktop/src/lib/tableStructureCapabilities.ts b/apps/desktop/src/lib/tableStructureCapabilities.ts index e8b0c9e78d..3a10220b31 100644 --- a/apps/desktop/src/lib/tableStructureCapabilities.ts +++ b/apps/desktop/src/lib/tableStructureCapabilities.ts @@ -66,6 +66,7 @@ const mysqlCapabilities = capabilities({ dropIndex: true, rebuildIndex: true, indexType: true, + indexComment: true, alterPrimaryKey: true, }); diff --git a/crates/dbx-core/src/schema_diff.rs b/crates/dbx-core/src/schema_diff.rs index 29c433aaf6..989bad6e5c 100644 --- a/crates/dbx-core/src/schema_diff.rs +++ b/crates/dbx-core/src/schema_diff.rs @@ -550,8 +550,21 @@ pub fn diff_triggers(source: &[TriggerInfo], target: &[TriggerInfo]) -> Vec bool { + matches!( + db_type, + DatabaseType::Mysql + | DatabaseType::Doris + | DatabaseType::StarRocks + | DatabaseType::Goldendb + | DatabaseType::Sundb + | DatabaseType::Databend + | DatabaseType::Gbase + ) +} + fn quote_id(name: &str, db_type: DatabaseType) -> String { - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { format!("`{}`", name.replace('`', "``")) } else { format!("\"{}\"", name.replace('"', "\"\"")) @@ -566,7 +579,7 @@ fn column_def(col: &ColumnInfo, db_type: DatabaseType) -> String { if let Some(default) = &col.column_default { definition.push_str(&format!(" DEFAULT {default}")); } - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { if let Some(comment) = &col.comment { definition.push_str(&format!(" COMMENT {}", comment_literal(comment))); } @@ -583,7 +596,7 @@ fn qualified_name(name: &str, db_type: DatabaseType, schema: Option<&str>) -> St fn drop_index_sql(table_name: &str, index_name: &str, db_type: DatabaseType, schema: Option<&str>) -> String { let table = qualified_name(table_name, db_type, schema); let index = qualified_name(index_name, db_type, schema); - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { format!("DROP INDEX {} ON {table};", quote_id(index_name, db_type)) } else { format!("DROP INDEX IF EXISTS {index};") @@ -605,6 +618,8 @@ fn create_index_sql(table_name: &str, index: &IndexInfo, db_type: DatabaseType, } else { String::new() }; + let mysql_using = + if !index_type.is_empty() && is_mysql_like(db_type) { format!(" USING {index_type}") } else { String::new() }; let included_columns = index.included_columns.clone().unwrap_or_default(); let include_clause = if !included_columns.is_empty() && matches!(db_type, DatabaseType::Postgres | DatabaseType::SqlServer) { @@ -618,16 +633,29 @@ fn create_index_sql(table_name: &str, index: &IndexInfo, db_type: DatabaseType, let supports_where = matches!(db_type, DatabaseType::Postgres | DatabaseType::SqlServer | DatabaseType::Sqlite); let filter = if supports_where { index.filter.as_deref().unwrap_or_default() } else { "" }; let filter_clause = if filter.is_empty() { String::new() } else { format!(" WHERE {filter}") }; - format!( - "CREATE {unique}{type_prefix}INDEX {} ON {table}{using_clause} ({columns}){include_clause}{filter_clause};", - quote_id(&index.name, db_type) - ) + let comment = index.comment.as_deref().unwrap_or(""); + let comment_clause = if !comment.trim().is_empty() && is_mysql_like(db_type) { + format!(" COMMENT {}", comment_literal(comment)) + } else { + String::new() + }; + if is_mysql_like(db_type) { + format!( + "CREATE {unique}{type_prefix}INDEX {}{mysql_using} ON {table} ({columns}){comment_clause};", + quote_id(&index.name, db_type) + ) + } else { + format!( + "CREATE {unique}{type_prefix}INDEX {} ON {table}{using_clause} ({columns}){include_clause}{filter_clause};", + quote_id(&index.name, db_type) + ) + } } fn drop_foreign_key_sql(table_name: &str, fk_name: &str, db_type: DatabaseType, schema: Option<&str>) -> String { let table = qualified_name(table_name, db_type, schema); let fk = quote_id(fk_name, db_type); - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { format!("ALTER TABLE {table} DROP FOREIGN KEY {fk};") } else { format!("ALTER TABLE {table} DROP CONSTRAINT {fk};") @@ -661,7 +689,7 @@ fn column_comment_sql( db_type: DatabaseType, schema: Option<&str>, ) -> String { - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { return format!( "-- Column comment for {column_name}: use ALTER TABLE ... MODIFY COLUMN to set comment in MySQL" ); @@ -672,7 +700,7 @@ fn column_comment_sql( fn table_comment_sql(table_name: &str, comment: &str, db_type: DatabaseType, schema: Option<&str>) -> String { let table = qualified_name(table_name, db_type, schema); - if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) { + if is_mysql_like(db_type) { format!("ALTER TABLE {table} COMMENT = {};", comment_literal(comment)) } else { format!("COMMENT ON TABLE {table} IS {};", comment_literal(comment)) @@ -681,7 +709,7 @@ fn table_comment_sql(table_name: &str, comment: &str, db_type: DatabaseType, sch pub fn generate_schema_sync_sql(diffs: &[TableDiff], db_type: DatabaseType, schema: Option<&str>) -> String { let mut lines = Vec::new(); - let is_mysql = matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks); + let is_mysql = is_mysql_like(db_type); for diff in diffs { let table = qualified_name(&diff.name, db_type, schema); diff --git a/crates/dbx-core/src/table_structure_sql/dialect.rs b/crates/dbx-core/src/table_structure_sql/dialect.rs index d693181f79..41df3b1abc 100644 --- a/crates/dbx-core/src/table_structure_sql/dialect.rs +++ b/crates/dbx-core/src/table_structure_sql/dialect.rs @@ -78,6 +78,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru drop_index: true, rebuild_index: true, index_type: true, + index_comment: true, alter_primary_key: true, ..base }, diff --git a/crates/dbx-core/src/table_structure_sql/indexes.rs b/crates/dbx-core/src/table_structure_sql/indexes.rs index 457cddbfc3..82d7e33fb2 100644 --- a/crates/dbx-core/src/table_structure_sql/indexes.rs +++ b/crates/dbx-core/src/table_structure_sql/indexes.rs @@ -169,6 +169,12 @@ pub(super) fn build_create_index_statements( } else { String::new() }; + let comment = clean(&index.comment); + let comment_clause = if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::Mysql { + format!(" COMMENT {}", quote_string(&comment)) + } else { + String::new() + }; let filter = clean(&index.filter); let supports_where = capabilities.index_filter && matches!(dialect, StructureDialect::Postgres | StructureDialect::SqlServer | StructureDialect::Sqlite); @@ -180,17 +186,18 @@ pub(super) fn build_create_index_statements( ) } else { format!( - "CREATE {unique}{type_prefix}INDEX {}{using_clause} ON {table} ({cols}){include_clause}{where_clause};", + "CREATE {unique}{type_prefix}INDEX {}{using_clause} ON {table} ({cols}){include_clause}{where_clause}{comment_clause};", quote_ident(dialect, &name) ) }; let mut statements = vec![create_sql]; - let comment = clean(&index.comment); if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::Postgres { statements.push(format!("COMMENT ON INDEX {} IS {};", quote_ident(dialect, &name), quote_string(&comment))); } else if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::SqlServer { statements.extend(build_sqlserver_index_comment_sql(table, schema, table_name, &name, &comment)); + } else if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::Mysql { + // MySQL comment is embedded inline in the CREATE INDEX statement above } else if !comment.is_empty() && capabilities.index_comment { warnings.push(format!("Index comments are not supported for {} from this editor.", dialect_label(dialect))); } diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index 074963bee6..7c6dede93d 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -117,6 +117,61 @@ fn builds_mysql_column_and_index_changes() { ); } +#[test] +fn mysql_create_index_with_comment() { + let mut col = column("name"); + col.data_type = "varchar(120)".to_string(); + let mut idx = index("idx_users_name", &["name"]); + idx.comment = "Search index".to_string(); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: vec![idx], + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "ALTER TABLE `users` ADD COLUMN `name` varchar(120);", + "CREATE INDEX `idx_users_name` ON `users` (`name`) COMMENT 'Search index';", + ] + ); +} + +#[test] +fn mysql_create_unique_index_with_comment_and_btree() { + let mut idx = index("uniq_users_email", &["email"]); + idx.is_unique = true; + idx.index_type = "BTREE".to_string(); + idx.comment = "Unique email index".to_string(); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: Vec::new(), + indexes: vec![idx], + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["CREATE UNIQUE INDEX `uniq_users_email` USING BTREE ON `users` (`email`) COMMENT 'Unique email index';",] + ); +} + #[test] fn mysql_add_timestamp_column_drops_invalid_precision() { let mut created_at = column("created_at");