feat(postgres): add cascade table drop option
This commit is contained in:
1 parent
8539011f25
commit
745f7f60b0
12 files changed
+155
-28
No files matched your search
@@ -76,6 +76,7 @@ function onConfirm() {
|
||||
<div class="py-4 min-w-0">
|
||||
<p class="text-sm text-muted-foreground mb-3">{{ message || t("dangerDialog.message") }}</p>
|
||||
<p v-if="detailsText" class="text-xs text-muted-foreground mb-3 whitespace-pre-line">{{ detailsText }}</p>
|
||||
<slot name="options" />
|
||||
<div v-if="code" class="relative">
|
||||
<Button variant="ghost" size="icon-xs" class="absolute top-1 right-1 z-10 h-6 w-6" :class="wrap ? 'text-foreground bg-accent' : 'text-muted-foreground'" :title="t('dangerDialog.wrapLines')" @click="wrap = !wrap">
|
||||
<TextWrap class="h-3.5 w-3.5" />
|
||||
|
||||
@@ -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, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, objectSourceSaveExecutionMode, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
|
||||
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
|
||||
@@ -132,6 +132,8 @@ const sourceSaveError = ref("");
|
||||
const error = ref("");
|
||||
const showDropConfirm = ref(false);
|
||||
const dropTarget = ref<ObjectBrowserRow | null>(null);
|
||||
const dropPreviewSql = ref("");
|
||||
const dropTableCascade = ref(false);
|
||||
const showRenameDialog = ref(false);
|
||||
const renameTarget = ref<ObjectBrowserRow | null>(null);
|
||||
const renameInput = ref("");
|
||||
@@ -176,6 +178,7 @@ let stopColumnResize: (() => void) | null = null;
|
||||
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 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);
|
||||
@@ -506,7 +509,10 @@ async function executeProcedureSql(sql: string) {
|
||||
|
||||
function requestDrop(row: ObjectBrowserRow) {
|
||||
dropTarget.value = row;
|
||||
dropPreviewSql.value = "";
|
||||
dropTableCascade.value = false;
|
||||
showDropConfirm.value = true;
|
||||
void refreshDropPreviewSql();
|
||||
}
|
||||
|
||||
function requestRename(row: ObjectBrowserRow) {
|
||||
@@ -593,12 +599,7 @@ async function confirmDrop() {
|
||||
if (!dropTarget.value) return;
|
||||
const row = dropTarget.value;
|
||||
try {
|
||||
const sql = await buildDropObjectSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: row.type,
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
});
|
||||
const sql = dropPreviewSql.value || (await buildDropSqlForRow(row, { cascade: canDropTargetCascade.value && dropTableCascade.value }));
|
||||
await api.executeQuery(props.connection.id, props.database, sql);
|
||||
const successKey = row.type === "VIEW" ? "contextMenu.dropViewSuccess" : row.type === "PROCEDURE" ? "contextMenu.dropProcedureSuccess" : row.type === "FUNCTION" ? "contextMenu.dropFunctionSuccess" : "contextMenu.dropTableSuccess";
|
||||
toast(t(successKey, { name: row.name }));
|
||||
@@ -609,6 +610,34 @@ async function confirmDrop() {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
dropTarget.value = null;
|
||||
dropPreviewSql.value = "";
|
||||
dropTableCascade.value = false;
|
||||
}
|
||||
|
||||
async function buildDropSqlForRow(row: ObjectBrowserRow, options?: { cascade?: boolean }): Promise<string> {
|
||||
if (row.type === "TABLE") {
|
||||
return buildDropTableSql(tableAdminSqlOptions(row, { cascade: options?.cascade && supportsDropTableCascade(effectiveDatabaseType.value) }));
|
||||
}
|
||||
return buildDropObjectSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: row.type,
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
});
|
||||
}
|
||||
|
||||
let dropPreviewRequestId = 0;
|
||||
|
||||
async function refreshDropPreviewSql() {
|
||||
const requestId = ++dropPreviewRequestId;
|
||||
const row = dropTarget.value;
|
||||
if (!row) {
|
||||
dropPreviewSql.value = "";
|
||||
return;
|
||||
}
|
||||
dropPreviewSql.value = "";
|
||||
const sql = await buildDropSqlForRow(row, { cascade: canDropTargetCascade.value && dropTableCascade.value }).catch(() => "");
|
||||
if (requestId === dropPreviewRequestId) dropPreviewSql.value = sql;
|
||||
}
|
||||
|
||||
function dropConfirmTitle(): string {
|
||||
@@ -786,12 +815,7 @@ async function refreshBatchDropPreviewSql() {
|
||||
const statements: string[] = [];
|
||||
const sortedRows = await fetchSortedTableRowsForDrop();
|
||||
for (const row of sortedRows) {
|
||||
const sql = await buildDropObjectSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: "TABLE",
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
}).catch(() => "");
|
||||
const sql = await buildDropTableSql(tableAdminSqlOptions(row)).catch(() => "");
|
||||
if (sql) statements.push(sql);
|
||||
}
|
||||
batchDropPreviewSql.value = statements.join("\n");
|
||||
@@ -809,12 +833,7 @@ async function confirmBatchDropTables() {
|
||||
if (targets.length === 0) return;
|
||||
try {
|
||||
for (const row of targets) {
|
||||
const sql = await buildDropObjectSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: "TABLE",
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
});
|
||||
const sql = await buildDropTableSql(tableAdminSqlOptions(row));
|
||||
await api.executeQuery(props.connection.id, props.database, sql);
|
||||
closeDroppedTableObjectTabsForRow(row);
|
||||
}
|
||||
@@ -1121,12 +1140,14 @@ async function confirmPasteTable() {
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, selectedSchema.value);
|
||||
}
|
||||
|
||||
function tableAdminSqlOptions(row: ObjectBrowserRow): TableAdminSqlOptions {
|
||||
return {
|
||||
function tableAdminSqlOptions(row: ObjectBrowserRow, options?: { cascade?: boolean }): TableAdminSqlOptions {
|
||||
const result: TableAdminSqlOptions = {
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
schema: row.schema || selectedSchema.value,
|
||||
tableName: row.name,
|
||||
};
|
||||
if (options?.cascade) result.cascade = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
async function refreshTruncatePreviewSql(row: ObjectBrowserRow) {
|
||||
@@ -1822,7 +1843,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DangerConfirmDialog v-model:open="showDropConfirm" :title="dropConfirmTitle()" :details="dropConfirmMessage()" :confirm-label="t('dangerDialog.deleteConfirm')" @confirm="confirmDrop" />
|
||||
<DangerConfirmDialog v-model:open="showDropConfirm" :title="dropConfirmTitle()" :message="dropConfirmMessage()" :sql="dropPreviewSql" :confirm-label="t('dangerDialog.deleteConfirm')" @confirm="confirmDrop">
|
||||
<template v-if="canDropTargetCascade" #options>
|
||||
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
|
||||
<input v-model="dropTableCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshDropPreviewSql()" />
|
||||
<span class="grid gap-0.5">
|
||||
<span class="font-medium text-foreground">{{ t("contextMenu.dropTableCascade") }}</span>
|
||||
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.dropTableCascadeHint") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</DangerConfirmDialog>
|
||||
|
||||
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="t('objects.confirmBatchDropTitle')" :message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })" :sql="batchDropPreviewSql" :confirm-label="t('objects.dropSelected')" @confirm="confirmBatchDropTables" />
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DangerConfirmDialog v-model:open="showDropTableConfirm" :title="t('contextMenu.confirmDropTableTitle')" :message="t('contextMenu.confirmDropTableMessage', { name: node.label })" :sql="dropTablePreviewSql" :confirm-label="t('contextMenu.dropTable')" @confirm="confirmDropTable" />
|
||||
<DangerConfirmDialog v-model:open="showDropTableConfirm" :title="t('contextMenu.confirmDropTableTitle')" :message="t('contextMenu.confirmDropTableMessage', { name: node.label })" :sql="dropTablePreviewSql" :confirm-label="t('contextMenu.dropTable')" @confirm="confirmDropTable">
|
||||
<template v-if="canDropTableCascade" #options>
|
||||
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
|
||||
<input v-model="dropTableCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshDropTablePreviewSql()" />
|
||||
<span class="grid gap-0.5">
|
||||
<span class="font-medium text-foreground">{{ t("contextMenu.dropTableCascade") }}</span>
|
||||
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.dropTableCascadeHint") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</DangerConfirmDialog>
|
||||
|
||||
<DangerConfirmDialog v-model:open="showEmptyTableConfirm" :title="t('contextMenu.confirmEmptyTableTitle')" :message="t('contextMenu.confirmEmptyTableMessage', { name: node.label })" :sql="emptyTablePreviewSql" :confirm-label="t('contextMenu.emptyTable')" @confirm="confirmEmptyTable" />
|
||||
|
||||
|
||||
Reference in new issue
Block a user