From a643cd74dcdb35b08a8741ed1c9aab19ac1149f3 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 18 Jun 2026 19:52:01 +0800 Subject: [PATCH] fix(structure): support postgres-like foreign keys --- .../structure/TableStructureEditor.vue | 10 ++--- .../src/lib/tableStructureCapabilities.ts | 12 +++++- .../src/table_structure_sql/dialect.rs | 5 +++ .../src/table_structure_sql/foreign_keys.rs | 35 +++++++++-------- .../dbx-core/src/table_structure_sql/tests.rs | 39 +++++++++++++++++++ 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index ae0c93e150..4dd8fd016c 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -716,7 +716,7 @@ function canDropIndex(index: EditableStructureIndex): boolean { return !!index.original && !index.isPrimary && structureCapabilities.value.dropIndex; } -const canEditMysqlForeignKeys = computed(() => structureDialect.value === "mysql"); +const canEditForeignKeys = computed(() => structureCapabilities.value.foreignKey); const canEditMysqlTriggers = computed(() => structureDialect.value === "mysql"); function generatedForeignKeyName(column = ""): string { @@ -736,7 +736,7 @@ function generatedForeignKeyName(column = ""): string { } function addForeignKey() { - if (!canEditMysqlForeignKeys.value) return; + if (!canEditForeignKeys.value) return; activeTab.value = "foreignKeys"; foreignKeys.value.push({ id: `new:${uuid()}`, @@ -761,7 +761,7 @@ function toggleDropForeignKey(foreignKey: EditableStructureForeignKey) { } function canEditForeignKeyDraft(foreignKey: EditableStructureForeignKey): boolean { - return canEditMysqlForeignKeys.value && !foreignKey.markedForDrop; + return canEditForeignKeys.value && !foreignKey.markedForDrop; } function addTrigger() { @@ -864,7 +864,7 @@ function addItemForActiveTab(): boolean { addIndex(); return true; } - if (activeTab.value === "foreignKeys" && canEditMysqlForeignKeys.value) { + if (activeTab.value === "foreignKeys" && canEditForeignKeys.value) { addForeignKey(); return true; } @@ -1027,7 +1027,7 @@ watch(activeTab, (tab) => { {{ t("structureEditor.addIndex") }} - diff --git a/apps/desktop/src/lib/tableStructureCapabilities.ts b/apps/desktop/src/lib/tableStructureCapabilities.ts index 56e8321221..d7fa0329f0 100644 --- a/apps/desktop/src/lib/tableStructureCapabilities.ts +++ b/apps/desktop/src/lib/tableStructureCapabilities.ts @@ -22,6 +22,7 @@ export interface TableStructureCapabilities { indexInclude: boolean; indexFilter: boolean; indexComment: boolean; + foreignKey: boolean; } const unsupportedCapabilities: TableStructureCapabilities = { @@ -44,6 +45,7 @@ const unsupportedCapabilities: TableStructureCapabilities = { indexInclude: false, indexFilter: false, indexComment: false, + foreignKey: false, }; function capabilities(overrides: Partial): TableStructureCapabilities { @@ -68,6 +70,7 @@ const mysqlCapabilities = capabilities({ indexType: true, indexComment: true, alterPrimaryKey: true, + foreignKey: true, }); const gbaseCapabilities = capabilities({ @@ -98,6 +101,7 @@ const postgresCapabilities = capabilities({ indexFilter: true, indexComment: true, alterPrimaryKey: true, + foreignKey: true, }); const redshiftCapabilities = capabilities({ @@ -264,6 +268,12 @@ const questdbCapabilities = capabilities({ indexFilter: false, indexComment: false, alterPrimaryKey: false, + foreignKey: false, +}); + +const firebirdCapabilities = capabilities({ + ...postgresCapabilities, + foreignKey: false, }); const capabilityByType: Partial> = { @@ -284,7 +294,7 @@ const capabilityByType: Partial highgo: postgresCapabilities, vastbase: postgresCapabilities, kingbase: postgresCapabilities, - firebird: postgresCapabilities, + firebird: firebirdCapabilities, sqlite: sqliteCapabilities, rqlite: sqliteCapabilities, turso: sqliteCapabilities, diff --git a/crates/dbx-core/src/table_structure_sql/dialect.rs b/crates/dbx-core/src/table_structure_sql/dialect.rs index 0ff5f66cfd..54293034df 100644 --- a/crates/dbx-core/src/table_structure_sql/dialect.rs +++ b/crates/dbx-core/src/table_structure_sql/dialect.rs @@ -34,6 +34,7 @@ pub(super) struct TableStructureCapabilities { pub(super) index_filter: bool, pub(super) index_comment: bool, pub(super) alter_primary_key: bool, + pub(super) foreign_key: bool, } impl Default for TableStructureCapabilities { @@ -54,6 +55,7 @@ impl Default for TableStructureCapabilities { index_filter: false, index_comment: false, alter_primary_key: false, + foreign_key: false, } } } @@ -82,6 +84,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru index_type: true, index_comment: true, alter_primary_key: true, + foreign_key: true, ..base }, Some(DatabaseType::Gbase) => TableStructureCapabilities { @@ -115,6 +118,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru index_filter: true, index_comment: true, alter_primary_key: true, + foreign_key: true, ..base }, Some(DatabaseType::Questdb) => TableStructureCapabilities { @@ -133,6 +137,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru index_filter: false, index_comment: false, alter_primary_key: false, + foreign_key: false, }, Some(DatabaseType::Redshift | DatabaseType::Vertica) => TableStructureCapabilities { dialect: StructureDialect::Postgres, diff --git a/crates/dbx-core/src/table_structure_sql/foreign_keys.rs b/crates/dbx-core/src/table_structure_sql/foreign_keys.rs index e183a2e229..22668af5dd 100644 --- a/crates/dbx-core/src/table_structure_sql/foreign_keys.rs +++ b/crates/dbx-core/src/table_structure_sql/foreign_keys.rs @@ -8,18 +8,20 @@ pub(super) fn build_foreign_key_sql(options: &TableStructureSqlOptions, warnings } let database_label = database_label(options.database_type); - if super::dialect::capabilities_for(options.database_type).dialect != StructureDialect::Mysql { + let capabilities = super::dialect::capabilities_for(options.database_type); + let dialect = capabilities.dialect; + if !capabilities.foreign_key { warnings.push(format!("Editing foreign keys is not supported for {database_label} from this editor.")); return Vec::new(); } - let table = qualified_table(StructureDialect::Mysql, options.schema.as_deref(), &options.table_name); + let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name); let mut statements = Vec::new(); for foreign_key in &options.foreign_keys { if foreign_key.marked_for_drop { if let Some(original) = &foreign_key.original { - statements.push(drop_foreign_key_sql(&table, &original.name)); + statements.push(drop_foreign_key_sql(dialect, &table, &original.name)); } continue; } @@ -28,10 +30,10 @@ pub(super) fn build_foreign_key_sql(options: &TableStructureSqlOptions, warnings if !has_foreign_key_change(foreign_key, original) { continue; } - statements.push(drop_foreign_key_sql(&table, &original.name)); + statements.push(drop_foreign_key_sql(dialect, &table, &original.name)); } - if let Some(sql) = create_foreign_key_sql(&table, foreign_key, warnings) { + if let Some(sql) = create_foreign_key_sql(dialect, &table, foreign_key, warnings) { statements.push(sql); } } @@ -49,11 +51,16 @@ fn has_foreign_key_change(foreign_key: &EditableStructureForeignKey, original: & || normalize_action(&foreign_key.on_delete) != normalize_action(original.on_delete.as_deref().unwrap_or("")) } -fn drop_foreign_key_sql(table: &str, name: &str) -> String { - format!("ALTER TABLE {table} DROP FOREIGN KEY {};", quote_ident(StructureDialect::Mysql, name)) +fn drop_foreign_key_sql(dialect: StructureDialect, table: &str, name: &str) -> String { + match dialect { + StructureDialect::Mysql => format!("ALTER TABLE {table} DROP FOREIGN KEY {};", quote_ident(dialect, name)), + StructureDialect::Postgres => format!("ALTER TABLE {table} DROP CONSTRAINT {};", quote_ident(dialect, name)), + _ => unreachable!("foreign key SQL requested for unsupported dialect"), + } } fn create_foreign_key_sql( + dialect: StructureDialect, table: &str, foreign_key: &EditableStructureForeignKey, warnings: &mut Vec, @@ -72,20 +79,16 @@ fn create_foreign_key_sql( } let ref_target = if clean(&foreign_key.ref_schema).is_empty() { - quote_ident(StructureDialect::Mysql, &ref_table) + quote_ident(dialect, &ref_table) } else { - format!( - "{}.{}", - quote_ident(StructureDialect::Mysql, &foreign_key.ref_schema), - quote_ident(StructureDialect::Mysql, &ref_table) - ) + format!("{}.{}", quote_ident(dialect, &foreign_key.ref_schema), quote_ident(dialect, &ref_table)) }; let mut sql = format!( "ALTER TABLE {table} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {ref_target} ({}", - quote_ident(StructureDialect::Mysql, &name), - columns.iter().map(|column| quote_ident(StructureDialect::Mysql, column)).collect::>().join(", "), - ref_columns.iter().map(|column| quote_ident(StructureDialect::Mysql, column)).collect::>().join(", ") + quote_ident(dialect, &name), + columns.iter().map(|column| quote_ident(dialect, column)).collect::>().join(", "), + ref_columns.iter().map(|column| quote_ident(dialect, column)).collect::>().join(", ") ); sql.push(')'); diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index e5e1b9a954..e1ec3863e5 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -117,6 +117,45 @@ fn builds_mysql_column_and_index_changes() { ); } +#[test] +fn builds_highgo_foreign_key_changes_with_postgres_syntax() { + let mut old_fk = foreign_key("orders_user_id_fkey", "user_id", "users", "id"); + old_fk.marked_for_drop = true; + old_fk.original = Some(ForeignKeyInfo { + name: "orders_user_id_fkey".to_string(), + column: "user_id".to_string(), + ref_schema: Some("public".to_string()), + ref_table: "users".to_string(), + ref_column: "id".to_string(), + on_update: None, + on_delete: None, + }); + let mut new_fk = foreign_key("orders_account_id_fkey", "account_id", "accounts", "id"); + new_fk.ref_schema = "crm".to_string(); + new_fk.on_delete = "CASCADE".to_string(); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Highgo), + schema: Some("public".to_string()), + table_name: "orders".to_string(), + columns: Vec::new(), + indexes: Vec::new(), + foreign_keys: vec![old_fk, new_fk], + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "ALTER TABLE \"public\".\"orders\" DROP CONSTRAINT \"orders_user_id_fkey\";", + "ALTER TABLE \"public\".\"orders\" ADD CONSTRAINT \"orders_account_id_fkey\" FOREIGN KEY (\"account_id\") REFERENCES \"crm\".\"accounts\" (\"id\") ON DELETE CASCADE;", + ] + ); +} + #[test] fn builds_informix_column_and_index_changes() { let mut renamed = column("display_name");