fix(structure): support postgres-like foreign keys

This commit is contained in:
t8y2
2026-06-18 19:52:01 +08:00
parent 545d203b59
commit a643cd74dc
5 changed files with 79 additions and 22 deletions
@@ -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) => {
<Plus :class="structureIconClass" />
{{ t("structureEditor.addIndex") }}
</Button>
<Button v-if="activeTab === 'foreignKeys'" size="sm" :class="structureToolbarButtonClass" :disabled="!canEditMysqlForeignKeys" @click="addForeignKey">
<Button v-if="activeTab === 'foreignKeys'" size="sm" :class="structureToolbarButtonClass" :disabled="!canEditForeignKeys" @click="addForeignKey">
<Plus :class="structureIconClass" />
{{ t("structureEditor.addForeignKey") }}
</Button>
@@ -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>): 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<Record<DatabaseType, TableStructureCapabilities>> = {
@@ -284,7 +294,7 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
highgo: postgresCapabilities,
vastbase: postgresCapabilities,
kingbase: postgresCapabilities,
firebird: postgresCapabilities,
firebird: firebirdCapabilities,
sqlite: sqliteCapabilities,
rqlite: sqliteCapabilities,
turso: sqliteCapabilities,
@@ -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<DatabaseType>) -> 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<DatabaseType>) -> 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<DatabaseType>) -> TableStru
index_filter: false,
index_comment: false,
alter_primary_key: false,
foreign_key: false,
},
Some(DatabaseType::Redshift | DatabaseType::Vertica) => TableStructureCapabilities {
dialect: StructureDialect::Postgres,
@@ -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<String>,
@@ -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::<Vec<_>>().join(", "),
ref_columns.iter().map(|column| quote_ident(StructureDialect::Mysql, column)).collect::<Vec<_>>().join(", ")
quote_ident(dialect, &name),
columns.iter().map(|column| quote_ident(dialect, column)).collect::<Vec<_>>().join(", "),
ref_columns.iter().map(|column| quote_ident(dialect, column)).collect::<Vec<_>>().join(", ")
);
sql.push(')');
@@ -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::<String>::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");