diff --git a/apps/desktop/src/components/editor/DangerConfirmDialog.vue b/apps/desktop/src/components/editor/DangerConfirmDialog.vue index cab88ae3c9..4b18a893e0 100644 --- a/apps/desktop/src/components/editor/DangerConfirmDialog.vue +++ b/apps/desktop/src/components/editor/DangerConfirmDialog.vue @@ -76,6 +76,7 @@ function onConfirm() {

{{ message || t("dangerDialog.message") }}

{{ detailsText }}

+
- + + + diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index ecda4a8444..c803eb9800 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -118,6 +118,7 @@ import { buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, + supportsDropTableCascade, supportsSchemaComment, type DropTableChildObjectSqlOptions, type DropObjectSqlOptions, @@ -1714,6 +1715,7 @@ const renameObjectName = ref(""); const renameObjectError = ref(""); const renameObjectPreviewSql = ref(""); const dropTablePreviewSql = ref(""); +const dropTableCascade = ref(false); const emptyTablePreviewSql = ref(""); const truncateTablePreviewSql = ref(""); const dropObjectPreviewSql = ref(""); @@ -2442,17 +2444,25 @@ const canEditSchemaComment = computed(() => { return props.node.type === "schema" && !!props.node.database && !config?.read_only && supportsSchemaComment(effectiveDatabaseTypeForConnection(config)); }); -function tableAdminSqlOptions(): TableAdminSqlOptions { - return { +const canDropTableCascade = computed(() => props.node.type === "table" && supportsDropTableCascade(currentDatabaseType())); + +function tableAdminSqlOptions(options?: { cascade?: boolean }): TableAdminSqlOptions { + const result: TableAdminSqlOptions = { databaseType: currentDatabaseType(), schema: props.node.schema, tableName: props.node.label, }; + if (options?.cascade) result.cascade = true; + return result; +} + +function dropTableSqlOptions(): TableAdminSqlOptions { + return tableAdminSqlOptions({ cascade: canDropTableCascade.value && dropTableCascade.value }); } async function refreshDropTablePreviewSql() { dropTablePreviewSql.value = ""; - dropTablePreviewSql.value = await buildDropTableSql(tableAdminSqlOptions()).catch(() => ""); + dropTablePreviewSql.value = await buildDropTableSql(dropTableSqlOptions()).catch(() => ""); } async function refreshEmptyTablePreviewSql() { @@ -2466,6 +2476,7 @@ async function refreshTruncateTablePreviewSql() { } function dropTable() { + dropTableCascade.value = false; void refreshDropTablePreviewSql(); showDropTableConfirm.value = true; } @@ -2480,7 +2491,7 @@ async function confirmDropTable() { if (!node.connectionId || !node.database) return; try { await connectionStore.ensureConnected(node.connectionId); - const sql = dropTablePreviewSql.value || (await buildDropTableSql(tableAdminSqlOptions())); + const sql = dropTablePreviewSql.value || (await buildDropTableSql(dropTableSqlOptions())); await api.executeQuery(node.connectionId, node.database, sql, node.schema); toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000); closeDroppedTableObjectTabsForNode(node); @@ -5177,7 +5188,17 @@ function treeItemMenuItems(): ContextMenuItem[] { - + + + diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 6edf7a505b..293b085a56 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1399,6 +1399,8 @@ export default { renameObjectSuccess: '"{oldName}" renamed to "{newName}"', confirmDropTableTitle: "Drop Table", confirmDropTableMessage: 'Are you sure you want to drop "{name}"? This will permanently delete the table and all its data.', + dropTableCascade: "Use CASCADE", + dropTableCascadeHint: "Also drop dependent objects such as views. Leave off to use PostgreSQL's default RESTRICT behavior.", confirmEmptyTableTitle: "Empty Table", confirmEmptyTableMessage: 'Are you sure you want to delete all data from "{name}"?', confirmTruncateTableTitle: "Truncate Table", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 92e3c66e08..936cc6256d 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1357,6 +1357,8 @@ export default withEnglishFallback({ renameObjectSuccess: '"{oldName}" renombrado a "{newName}"', confirmDropTableTitle: "Eliminar tabla", confirmDropTableMessage: '¿Estás seguro de que deseas eliminar "{name}"? Esto borrará permanentemente la tabla y todos sus datos.', + dropTableCascade: "Usar CASCADE", + dropTableCascadeHint: "También elimina objetos dependientes, como vistas. Desactívalo para usar el comportamiento RESTRICT predeterminado de PostgreSQL.", confirmEmptyTableTitle: "Vaciar tabla", confirmEmptyTableMessage: '¿Estás seguro de que deseas eliminar todos los datos de "{name}"?', confirmTruncateTableTitle: "Truncar tabla", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index af77be1ca9..69b4da48c7 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1355,6 +1355,8 @@ export default withEnglishFallback({ renameObjectSuccess: '"{oldName}" rinominato in "{newName}"', confirmDropTableTitle: "Elimina Tabella", confirmDropTableMessage: 'Sei sicuro di voler eliminare (DROP) la tabella "{name}"? Questa operazione eliminerà permanentemente la tabella e tutti i suoi dati.', + dropTableCascade: "Usa CASCADE", + dropTableCascadeHint: "Elimina anche gli oggetti dipendenti, come le viste. Lascia disattivato per usare il comportamento RESTRICT predefinito di PostgreSQL.", confirmEmptyTableTitle: "Svuota Tabella", confirmEmptyTableMessage: 'Sei sicuro di voler eliminare tutti i dati da "{name}"?', confirmTruncateTableTitle: "Tronca Tabella", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b1b289f502..e822922451 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1353,6 +1353,8 @@ export default withEnglishFallback({ renameObjectSuccess: "「{oldName}」の名前を「{newName}」に変更しました", confirmDropTableTitle: "テーブルを削除", confirmDropTableMessage: "本当にテーブル「{name}」をドロップ(削除)しますか?テーブルとすべてのデータが永久に削除されます。", + dropTableCascade: "CASCADE を使用", + dropTableCascadeHint: "ビューなど、このテーブルに依存するオブジェクトも削除します。オフの場合は PostgreSQL の既定の RESTRICT を使用します。", confirmEmptyTableTitle: "テーブルを空にする", confirmEmptyTableMessage: "本当に「{name}」のすべてのデータを削除しますか?", confirmTruncateTableTitle: "テーブルをトランケート", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 69c73a1ff8..a7e528c9a9 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1356,6 +1356,8 @@ export default withEnglishFallback({ renameObjectSuccess: '"{oldName}" renomeado para "{newName}"', confirmDropTableTitle: "Remover Tabela", confirmDropTableMessage: 'Tem certeza de que deseja remover "{name}"? Isso excluirá permanentemente a tabela e todos os seus dados.', + dropTableCascade: "Usar CASCADE", + dropTableCascadeHint: "Também remove objetos dependentes, como views. Deixe desmarcado para usar o comportamento RESTRICT padrão do PostgreSQL.", confirmEmptyTableTitle: "Esvaziar Tabela", confirmEmptyTableMessage: 'Tem certeza de que deseja excluir todos os dados de "{name}"?', confirmTruncateTableTitle: "Truncar Tabela", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 5b8273c9a7..1337ff7acd 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1401,6 +1401,8 @@ export default withEnglishFallback({ renameObjectSuccess: "已将「{oldName}」重命名为「{newName}」", confirmDropTableTitle: "删除表", confirmDropTableMessage: "确定要删除「{name}」吗?这将永久删除表及其所有数据。", + dropTableCascade: "使用 CASCADE 强制删除", + dropTableCascadeHint: "同时删除依赖该表的对象(如视图)。未勾选时使用 PostgreSQL 默认的 RESTRICT。", confirmEmptyTableTitle: "清空数据", confirmEmptyTableMessage: "确定要删除「{name}」中的所有数据吗?", confirmTruncateTableTitle: "截断表", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 39452edb2f..62958cff7c 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1356,6 +1356,8 @@ export default withEnglishFallback({ renameObjectSuccess: "已將「{oldName}」重新命名為「{newName}」", confirmDropTableTitle: "刪除資料表", confirmDropTableMessage: "確定要刪除「{name}」嗎?這將永久刪除資料表及其所有資料。", + dropTableCascade: "使用 CASCADE 強制刪除", + dropTableCascadeHint: "同時刪除依賴此資料表的物件(例如檢視)。未勾選時使用 PostgreSQL 預設的 RESTRICT。", confirmEmptyTableTitle: "清空資料", confirmEmptyTableMessage: "確定要刪除「{name}」中的所有資料嗎?", confirmTruncateTableTitle: "截斷資料表", diff --git a/apps/desktop/src/lib/database/dbAdminSql.ts b/apps/desktop/src/lib/database/dbAdminSql.ts index 150fe7e748..17689f07da 100644 --- a/apps/desktop/src/lib/database/dbAdminSql.ts +++ b/apps/desktop/src/lib/database/dbAdminSql.ts @@ -12,6 +12,7 @@ export interface TableAdminSqlOptions { databaseType?: DatabaseType; schema?: string | null; tableName: string; + cascade?: boolean; } export type TableChildObjectType = "COLUMN" | "INDEX" | "FOREIGN_KEY" | "TRIGGER"; @@ -85,6 +86,12 @@ export function buildTruncateTableSql(options: TableAdminSqlOptions): Promise { return api.buildDropDatabaseSql(options); } diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs index 408b57f93e..b5bd9a43ca 100644 --- a/crates/dbx-core/src/db_admin_sql.rs +++ b/crates/dbx-core/src/db_admin_sql.rs @@ -88,6 +88,8 @@ pub struct TableAdminSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cascade: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -322,7 +324,29 @@ pub fn build_drop_table_sql(options: TableAdminSqlOptions) -> String { } else if matches!(options.database_type, Some(DatabaseType::InfluxDb)) { return format!("DROP MEASUREMENT {};", table); } - format!("DROP TABLE {table};") + // CASCADE is valid for PostgreSQL-family dialects; keep default RESTRICT behavior elsewhere. + let cascade = if options.cascade.unwrap_or(false) && supports_drop_table_cascade(options.database_type) { + " CASCADE" + } else { + "" + }; + format!("DROP TABLE {table}{cascade};") +} + +fn supports_drop_table_cascade(database_type: Option) -> bool { + matches!( + database_type, + Some( + DatabaseType::Postgres + | DatabaseType::Redshift + | DatabaseType::Gaussdb + | DatabaseType::Kwdb + | DatabaseType::Kingbase + | DatabaseType::Highgo + | DatabaseType::Vastbase + | DatabaseType::OpenGauss + ) + ) } pub fn build_drop_table_child_object_sql(options: DropTableChildObjectSqlOptions) -> Result { @@ -1018,8 +1042,27 @@ mod tests { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "events".to_string(), + cascade: None, }; assert_eq!(build_drop_table_sql(options.clone()), "DROP TABLE \"public\".\"events\";"); + assert_eq!( + build_drop_table_sql(TableAdminSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("public".to_string()), + table_name: "events".to_string(), + cascade: Some(true), + }), + "DROP TABLE \"public\".\"events\" CASCADE;" + ); + assert_eq!( + build_drop_table_sql(TableAdminSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "events".to_string(), + cascade: Some(true), + }), + "DROP TABLE `events`;" + ); assert_eq!(build_empty_table_sql(options.clone()), "DELETE FROM \"public\".\"events\";"); assert_eq!(build_truncate_table_sql(options), "TRUNCATE TABLE \"public\".\"events\";"); assert_eq!( @@ -1027,6 +1070,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), schema: None, table_name: "PresetSubjectInfo".to_string(), + cascade: None, }), "ALTER TABLE `PresetSubjectInfo` DELETE WHERE 1 = 1;" ); @@ -1035,6 +1079,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), schema: None, table_name: "PresetSubjectInfo".to_string(), + cascade: None, }), "TRUNCATE TABLE `PresetSubjectInfo`;" ); @@ -1043,6 +1088,7 @@ mod tests { database_type: Some(DatabaseType::Bigquery), schema: None, table_name: "events".to_string(), + cascade: None, }), "DELETE FROM `events` WHERE TRUE;" ); @@ -1051,6 +1097,7 @@ mod tests { database_type: Some(DatabaseType::Cassandra), schema: None, table_name: "events".to_string(), + cascade: None, }), "TRUNCATE TABLE \"events\";" ); @@ -1059,6 +1106,7 @@ mod tests { database_type: Some(DatabaseType::DuckDb), schema: None, table_name: "events".to_string(), + cascade: None, }), "DELETE FROM \"events\";" ); @@ -1067,6 +1115,7 @@ mod tests { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "DCU_101".to_string(), + cascade: None, }), "DELETE TIMESERIES root.test.DCU_101.*;" ); @@ -1075,6 +1124,7 @@ mod tests { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "root.test.DCU_101".to_string(), + cascade: None, }), "DELETE FROM root.test.DCU_101.*;" ); @@ -1083,6 +1133,7 @@ mod tests { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "DCU_101".to_string(), + cascade: None, }), "DELETE FROM root.test.DCU_101.*;" ); @@ -1092,6 +1143,7 @@ mod tests { database_type: Some(DatabaseType::Questdb), schema: None, table_name: "table_sample".to_string(), + cascade: None, }), "TRUNCATE TABLE `table_sample`;" ); @@ -1100,6 +1152,7 @@ mod tests { database_type: Some(DatabaseType::Questdb), schema: None, table_name: "table_sample".to_string(), + cascade: None, }), "TRUNCATE TABLE `table_sample`;" );