diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index acb36b5ee..b5b52e96e 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -52,7 +52,7 @@ import { isSchemaAware } from "@/lib/database/databaseCapabilities"; import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport"; import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { buildTableSelectSql } from "@/lib/table/tableSelectSql"; -import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql"; +import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, supportsTruncateTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql"; import { useToast } from "@/composables/useToast"; import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor"; import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql"; @@ -134,6 +134,7 @@ const showDropConfirm = ref(false); const dropTarget = ref(null); const dropPreviewSql = ref(""); const dropTableCascade = ref(false); +const batchDropCascade = ref(false); const showRenameDialog = ref(false); const renameTarget = ref(null); const renameInput = ref(""); @@ -142,6 +143,7 @@ const renamePreviewSqlText = ref(""); const showTruncateConfirm = ref(false); const truncateTarget = ref(null); const truncatePreviewSql = ref(""); +const truncateTableCascade = ref(false); const showEmptyConfirm = ref(false); const emptyTarget = ref(null); const emptyPreviewSql = ref(""); @@ -156,6 +158,9 @@ const selectedTableIds = ref>(new Set()); const expandedPartitionParentIds = ref>(new Set()); const showBatchDropConfirm = ref(false); const batchDropPreviewSql = ref(""); +const showBatchTruncateConfirm = ref(false); +const batchTruncatePreviewSql = ref(""); +const batchTruncateCascade = ref(false); // Paste table dialog state const showPasteDialog = ref(false); const pasteTableMode = ref("structure-and-data"); @@ -179,6 +184,7 @@ const { addTask: addExportTask } = useExportTracker(); const needsSchema = computed(() => isSchemaAware(props.connection.db_type) && !connectionUsesDatabaseObjectTreeMode(props.connection)); const canDropTargetCascade = computed(() => dropTarget.value?.type === "TABLE" && supportsDropTableCascade(effectiveDatabaseType.value)); +const canTruncateTargetCascade = computed(() => !!truncateTarget.value && supportsTruncateTableCascade(effectiveDatabaseType.value)); const tableCount = computed(() => rows.value.filter((row) => row.type === "TABLE").length); const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW").length); const materializedViewCount = computed(() => rows.value.filter((row) => row.type === "MATERIALIZED_VIEW").length); @@ -249,6 +255,8 @@ const selectedTableRows = computed(() => { return selectableRows.value.filter((row) => ids.has(row.id)); }); const selectedTableCount = computed(() => selectedTableRows.value.length); +const canBatchDropCascade = computed(() => selectedTableCount.value > 0 && supportsDropTableCascade(effectiveDatabaseType.value)); +const canBatchTruncateCascade = computed(() => selectedTableCount.value > 0 && supportsTruncateTableCascade(effectiveDatabaseType.value)); const allVisibleTablesSelected = computed(() => visibleSelectableRows.value.length > 0 && visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id))); function iconFor(row: ObjectBrowserRow) { @@ -798,8 +806,9 @@ async function fetchSortedTableRowsForDrop(): Promise { async function refreshBatchDropPreviewSql() { const statements: string[] = []; const sortedRows = await fetchSortedTableRowsForDrop(); + const useCascade = canBatchDropCascade.value && batchDropCascade.value; for (const row of sortedRows) { - const sql = await buildDropTableSql(tableAdminSqlOptions(row)).catch(() => ""); + const sql = await buildDropTableSql(tableAdminSqlOptions(row, { cascade: useCascade })).catch(() => ""); if (sql) statements.push(sql); } batchDropPreviewSql.value = statements.join("\n"); @@ -807,6 +816,7 @@ async function refreshBatchDropPreviewSql() { function requestBatchDropTables() { if (selectedTableCount.value === 0) return; + batchDropCascade.value = false; batchDropPreviewSql.value = ""; void refreshBatchDropPreviewSql(); showBatchDropConfirm.value = true; @@ -816,8 +826,9 @@ async function confirmBatchDropTables() { const targets = await fetchSortedTableRowsForDrop(); if (targets.length === 0) return; try { + const useCascade = canBatchDropCascade.value && batchDropCascade.value; for (const row of targets) { - const sql = await buildDropTableSql(tableAdminSqlOptions(row)); + const sql = await buildDropTableSql(tableAdminSqlOptions(row, { cascade: useCascade })); await api.executeQuery(props.connection.id, props.database, sql); closeDroppedTableObjectTabsForRow(row); } @@ -830,6 +841,43 @@ async function confirmBatchDropTables() { } } +async function refreshBatchTruncatePreviewSql() { + const statements: string[] = []; + const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value; + for (const row of selectedTableRows.value) { + const sql = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: useCascade })).catch(() => ""); + if (sql) statements.push(sql); + } + batchTruncatePreviewSql.value = statements.join("\n"); +} + +function requestBatchTruncateTables() { + if (selectedTableCount.value === 0 || !supportsTruncateTable.value) return; + batchTruncateCascade.value = false; + batchTruncatePreviewSql.value = ""; + void refreshBatchTruncatePreviewSql(); + showBatchTruncateConfirm.value = true; +} + +async function confirmBatchTruncateTables() { + const targets = [...selectedTableRows.value]; + if (targets.length === 0) return; + try { + const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value; + for (const row of targets) { + const sql = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: useCascade })); + await api.executeQuery(props.connection.id, props.database, sql); + } + toast(t("objects.batchTruncateSuccess", { count: targets.length })); + clearTableSelection(); + showBatchTruncateConfirm.value = false; + await reload(); + await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, selectedSchema.value); + } catch (e: any) { + toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000); + } +} + async function exportStructure(row: ObjectBrowserRow) { try { const schema = row.schema || selectedSchema.value || props.database; @@ -1136,11 +1184,12 @@ function tableAdminSqlOptions(row: ObjectBrowserRow, options?: { cascade?: boole async function refreshTruncatePreviewSql(row: ObjectBrowserRow) { truncatePreviewSql.value = ""; - truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row)).catch(() => ""); + truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value })).catch(() => ""); } function requestTruncateTable(row: ObjectBrowserRow) { truncateTarget.value = row; + truncateTableCascade.value = false; void refreshTruncatePreviewSql(row); showTruncateConfirm.value = true; } @@ -1149,7 +1198,7 @@ async function confirmTruncateTable() { const row = truncateTarget.value; if (!row) return; try { - const sql = truncatePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions(row))); + const sql = truncatePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value }))); await api.executeQuery(props.connection.id, props.database, sql); toast(t("contextMenu.truncateTableSuccess", { name: row.name })); } catch (e: any) { @@ -1604,7 +1653,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { {{ t("objects.pasteTableSelected") }} -
+
{{ t("objects.selectedTables", { count: selectedTableCount }) }}
@@ -1616,6 +1665,10 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { {{ t("objects.copyTableSelected") }} +