feat(sidebar): add table management context menu operations (#114)
Add create, drop, empty, truncate, and duplicate structure operations to the sidebar table right-click menu. Also extends the table structure editor to support create mode from the database/schema context menu.
This commit is contained in:
parent
6a3e8067e1
commit
6523d1b3f6
|
|
@ -33,6 +33,10 @@ import {
|
|||
Search,
|
||||
FolderInput,
|
||||
FolderPlus,
|
||||
Eraser,
|
||||
Scissors,
|
||||
CopyPlus,
|
||||
Plus,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
ContextMenu,
|
||||
|
|
@ -58,6 +62,7 @@ import {
|
|||
} from "@/lib/databaseExport";
|
||||
import { qualifiedTableName as buildQualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { treeNodeRowAction } from "@/lib/treeNodeClick";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
|
||||
|
|
@ -343,6 +348,148 @@ function copyName() {
|
|||
navigator.clipboard.writeText(props.node.label);
|
||||
}
|
||||
|
||||
// --- Table Management Operations ---
|
||||
const showDropTableConfirm = ref(false);
|
||||
const showEmptyTableConfirm = ref(false);
|
||||
const showTruncateTableConfirm = ref(false);
|
||||
const showDuplicateDialog = ref(false);
|
||||
const duplicateTableName = ref("");
|
||||
|
||||
const isTableNotView = computed(() => props.node.type === "table");
|
||||
|
||||
const supportsTruncate = computed(() => {
|
||||
const dbType = currentDatabaseType();
|
||||
return dbType !== "sqlite" && dbType !== "duckdb";
|
||||
});
|
||||
|
||||
const canCreateTable = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (
|
||||
(props.node.type === "database" || props.node.type === "schema") &&
|
||||
!!props.node.database &&
|
||||
!!config &&
|
||||
tableStructureSupportedTypes.has(config.db_type)
|
||||
);
|
||||
});
|
||||
|
||||
function buildDropTableSql(): string {
|
||||
return `DROP TABLE ${qualifiedTableName(props.node.label, props.node.schema)};`;
|
||||
}
|
||||
|
||||
function buildEmptyTableSql(): string {
|
||||
return `DELETE FROM ${qualifiedTableName(props.node.label, props.node.schema)};`;
|
||||
}
|
||||
|
||||
function buildTruncateTableSql(): string {
|
||||
const dbType = currentDatabaseType();
|
||||
const name = qualifiedTableName(props.node.label, props.node.schema);
|
||||
if (dbType === "sqlite" || dbType === "duckdb") return `DELETE FROM ${name};`;
|
||||
return `TRUNCATE TABLE ${name};`;
|
||||
}
|
||||
|
||||
function dropTable() {
|
||||
showDropTableConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmDropTable() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
await api.executeQuery(node.connectionId, node.database, buildDropTableSql(), node.schema);
|
||||
toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000);
|
||||
if (node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
} else {
|
||||
await connectionStore.loadTables(node.connectionId, node.database);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function emptyTable() {
|
||||
showEmptyTableConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmEmptyTable() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
await api.executeQuery(node.connectionId, node.database, buildEmptyTableSql(), node.schema);
|
||||
toast(t("contextMenu.emptyTableSuccess", { name: node.label }), 3000);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function truncateTable() {
|
||||
showTruncateTableConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmTruncateTable() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
await api.executeQuery(node.connectionId, node.database, buildTruncateTableSql(), node.schema);
|
||||
toast(t("contextMenu.truncateTableSuccess", { name: node.label }), 3000);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateStructure() {
|
||||
duplicateTableName.value = `${props.node.label}_copy`;
|
||||
showDuplicateDialog.value = true;
|
||||
}
|
||||
|
||||
async function confirmDuplicateStructure() {
|
||||
const node = props.node;
|
||||
const newName = duplicateTableName.value.trim();
|
||||
if (!newName || !node.connectionId || !node.database) return;
|
||||
showDuplicateDialog.value = false;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const dbType = currentDatabaseType();
|
||||
const source = qualifiedTableName(node.label, node.schema);
|
||||
const target = qualifiedTableName(newName, node.schema);
|
||||
let sql: string;
|
||||
if (dbType === "mysql") {
|
||||
sql = `CREATE TABLE ${target} LIKE ${source};`;
|
||||
} else if (dbType === "postgres" || dbType === "redshift") {
|
||||
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
|
||||
} else if (dbType === "sqlserver") {
|
||||
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;
|
||||
} else if (dbType === "oracle") {
|
||||
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`;
|
||||
} else {
|
||||
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`;
|
||||
}
|
||||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
|
||||
if (node.schema) {
|
||||
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
|
||||
} else {
|
||||
await connectionStore.loadTables(node.connectionId, node.database);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function createTable() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
connectionStore.structureEditorSource = {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
tableName: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function collectDatabaseExportTables(): Promise<Array<{ schema?: string; name: string; displayName: string }>> {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return [];
|
||||
|
|
@ -1033,6 +1180,9 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
<ContextMenuItem @click="newQuery">
|
||||
<TerminalSquare class="w-4 h-4" /> {{ t("contextMenu.newQuery") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canCreateTable" @click="createTable">
|
||||
<Plus class="w-4 h-4" /> {{ t("contextMenu.createTable") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canOpenSqlFileExecution" @click="openSqlFileExecution">
|
||||
<FileCode class="w-4 h-4" /> {{ t("sqlFile.title") }}
|
||||
</ContextMenuItem>
|
||||
|
|
@ -1089,6 +1239,22 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
<ContextMenuItem @click="exportStructure">
|
||||
<FileCode class="w-4 h-4" /> {{ t("contextMenu.exportStructure") }}
|
||||
</ContextMenuItem>
|
||||
<template v-if="isTableNotView">
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem @click="duplicateStructure">
|
||||
<CopyPlus class="w-4 h-4" /> {{ t("contextMenu.duplicateStructure") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem v-if="supportsTruncate" class="text-destructive" @click="truncateTable">
|
||||
<Scissors class="w-4 h-4" /> {{ t("contextMenu.truncateTable") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem class="text-destructive" @click="emptyTable">
|
||||
<Eraser class="w-4 h-4" /> {{ t("contextMenu.emptyTable") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem class="text-destructive" @click="dropTable">
|
||||
<Trash2 class="w-4 h-4" /> {{ t("contextMenu.dropTable") }}
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem @click="refresh">
|
||||
<RefreshCw class="w-4 h-4" /> {{ t("contextMenu.refreshChildren") }}
|
||||
|
|
@ -1167,4 +1333,50 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDropTableConfirm"
|
||||
:title="t('contextMenu.confirmDropTableTitle')"
|
||||
:message="t('contextMenu.confirmDropTableMessage', { name: node.label })"
|
||||
:sql="buildDropTableSql()"
|
||||
:confirm-label="t('contextMenu.dropTable')"
|
||||
@confirm="confirmDropTable"
|
||||
/>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showEmptyTableConfirm"
|
||||
:title="t('contextMenu.confirmEmptyTableTitle')"
|
||||
:message="t('contextMenu.confirmEmptyTableMessage', { name: node.label })"
|
||||
:sql="buildEmptyTableSql()"
|
||||
:confirm-label="t('contextMenu.emptyTable')"
|
||||
@confirm="confirmEmptyTable"
|
||||
/>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showTruncateTableConfirm"
|
||||
:title="t('contextMenu.confirmTruncateTableTitle')"
|
||||
:message="t('contextMenu.confirmTruncateTableMessage', { name: node.label })"
|
||||
:sql="buildTruncateTableSql()"
|
||||
:confirm-label="t('contextMenu.truncateTable')"
|
||||
@confirm="confirmTruncateTable"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="showDuplicateDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.duplicateNameTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
v-model="duplicateTableName"
|
||||
:placeholder="t('contextMenu.duplicateNamePlaceholder')"
|
||||
@keydown.enter.prevent="confirmDuplicateStructure"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDuplicateDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!duplicateTableName.trim()" @click="confirmDuplicateStructure">{{
|
||||
t("dangerDialog.confirm")
|
||||
}}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
|||
import { useToast } from "@/composables/useToast";
|
||||
import {
|
||||
buildTableStructureChangeSql,
|
||||
buildCreateTableSql,
|
||||
type EditableStructureColumn,
|
||||
type EditableStructureIndex,
|
||||
} from "@/lib/tableStructureEditorSql";
|
||||
|
|
@ -107,19 +108,32 @@ const indexColLabels = computed(() => [
|
|||
t("structureEditor.actions"),
|
||||
]);
|
||||
const targetSchema = computed(() => props.prefillSchema || props.prefillDatabase || "");
|
||||
const targetLabel = computed(() =>
|
||||
[connection.value?.name, props.prefillDatabase, props.prefillSchema, props.prefillTable].filter(Boolean).join(" / "),
|
||||
);
|
||||
const isCreateMode = computed(() => !props.prefillTable);
|
||||
const newTableName = ref("");
|
||||
const targetLabel = computed(() => {
|
||||
const parts = [connection.value?.name, props.prefillDatabase, props.prefillSchema];
|
||||
if (!isCreateMode.value) parts.push(props.prefillTable);
|
||||
return parts.filter(Boolean).join(" / ");
|
||||
});
|
||||
|
||||
const changeSql = computed(() =>
|
||||
buildTableStructureChangeSql({
|
||||
const changeSql = computed(() => {
|
||||
if (isCreateMode.value) {
|
||||
return buildCreateTableSql({
|
||||
databaseType: databaseType.value,
|
||||
schema: props.prefillSchema,
|
||||
tableName: newTableName.value,
|
||||
columns: columns.value,
|
||||
indexes: indexes.value,
|
||||
});
|
||||
}
|
||||
return buildTableStructureChangeSql({
|
||||
databaseType: databaseType.value,
|
||||
schema: props.prefillSchema,
|
||||
tableName: props.prefillTable || "",
|
||||
columns: columns.value,
|
||||
indexes: indexes.value,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
const pendingStatements = computed(() => changeSql.value.statements);
|
||||
const warnings = computed(() => changeSql.value.warnings);
|
||||
const canApply = computed(
|
||||
|
|
@ -129,7 +143,7 @@ const canApply = computed(
|
|||
pendingStatements.value.length > 0 &&
|
||||
warnings.value.length === 0 &&
|
||||
!!props.prefillConnectionId &&
|
||||
!!props.prefillTable,
|
||||
(isCreateMode.value ? !!newTableName.value.trim() : !!props.prefillTable),
|
||||
);
|
||||
|
||||
function resetState() {
|
||||
|
|
@ -141,6 +155,7 @@ function resetState() {
|
|||
indexes.value = [];
|
||||
foreignKeys.value = [];
|
||||
triggers.value = [];
|
||||
newTableName.value = "";
|
||||
}
|
||||
|
||||
async function loadStructure() {
|
||||
|
|
@ -246,7 +261,11 @@ async function applyChanges() {
|
|||
await api.executeBatch(props.prefillConnectionId, props.prefillDatabase, pendingStatements.value);
|
||||
toast(t("structureEditor.saved"), 2500);
|
||||
emit("saved");
|
||||
await loadStructure();
|
||||
if (isCreateMode.value) {
|
||||
open.value = false;
|
||||
} else {
|
||||
await loadStructure();
|
||||
}
|
||||
} catch (e: any) {
|
||||
errorMessage.value = e?.message || String(e);
|
||||
} finally {
|
||||
|
|
@ -268,7 +287,7 @@ watch(open, (value) => {
|
|||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<TableProperties class="h-4 w-4" />
|
||||
{{ t("structureEditor.title") }}
|
||||
{{ isCreateMode ? t("structureEditor.createTitle") : t("structureEditor.title") }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -277,12 +296,28 @@ watch(open, (value) => {
|
|||
<Database class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ targetLabel || t("editor.noDatabase") }}</span>
|
||||
<Badge variant="outline">{{ connection?.driver_label || databaseType }}</Badge>
|
||||
<Button variant="ghost" size="sm" class="h-7 gap-1" :disabled="loading || saving" @click="loadStructure">
|
||||
<Button
|
||||
v-if="!isCreateMode"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="loading || saving"
|
||||
@click="loadStructure"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="isCreateMode" class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-muted-foreground shrink-0">{{ t("structureEditor.tableName") }}</label>
|
||||
<Input
|
||||
v-model="newTableName"
|
||||
:placeholder="t('contextMenu.duplicateNamePlaceholder')"
|
||||
class="h-7 text-xs max-w-[240px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex h-[420px] items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ export function useNavigationTargets(dialogs: {
|
|||
showFieldLineageDialog: { value: boolean };
|
||||
showDatabaseSearchDialog: { value: boolean };
|
||||
structurePrefillTable: { value: string };
|
||||
structurePrefillConnectionId?: { value: string };
|
||||
structurePrefillDatabase?: { value: string };
|
||||
structurePrefillSchema?: { value: string };
|
||||
}) {
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
async function openLineageTarget(target: NavigationTarget) {
|
||||
|
|
@ -69,6 +73,17 @@ export function useNavigationTargets(dialogs: {
|
|||
reloadData: () => Promise<void>,
|
||||
toast: (msg: string, duration?: number) => void,
|
||||
) {
|
||||
if (!dialogs.structurePrefillTable.value) {
|
||||
const connId = dialogs.structurePrefillConnectionId?.value;
|
||||
const db = dialogs.structurePrefillDatabase?.value;
|
||||
const schema = dialogs.structurePrefillSchema?.value;
|
||||
if (connId && db) {
|
||||
try {
|
||||
await connectionStore.loadTables(connId, db, schema || undefined);
|
||||
} catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const activeTab = queryStore.tabs.find((t) => t.id === queryStore.activeTabId);
|
||||
if (activeTab?.mode === "data" && activeTab.tableMeta?.tableName === dialogs.structurePrefillTable.value) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -416,6 +416,25 @@ export default {
|
|||
importData: "Import Data",
|
||||
exportData: "Export Data",
|
||||
exportStructure: "Export Structure",
|
||||
createTable: "Create Table",
|
||||
dropTable: "Drop Table",
|
||||
emptyTable: "Empty Table",
|
||||
truncateTable: "Truncate Table",
|
||||
duplicateStructure: "Duplicate Structure",
|
||||
confirmDropTableTitle: "Drop Table",
|
||||
confirmDropTableMessage:
|
||||
'Are you sure you want to drop "{name}"? This will permanently delete the table and all its data.',
|
||||
confirmEmptyTableTitle: "Empty Table",
|
||||
confirmEmptyTableMessage: 'Are you sure you want to delete all data from "{name}"?',
|
||||
confirmTruncateTableTitle: "Truncate Table",
|
||||
confirmTruncateTableMessage: 'Are you sure you want to truncate "{name}"? This will remove all rows.',
|
||||
dropTableSuccess: 'Table "{name}" dropped',
|
||||
emptyTableSuccess: 'All data deleted from "{name}"',
|
||||
truncateTableSuccess: 'Table "{name}" truncated',
|
||||
duplicateStructureSuccess: 'Table structure duplicated as "{name}"',
|
||||
tableOperationFailed: "Operation failed: {message}",
|
||||
duplicateNameTitle: "Duplicate Structure",
|
||||
duplicateNamePlaceholder: "New table name",
|
||||
},
|
||||
tree: {
|
||||
columns: "Columns",
|
||||
|
|
@ -425,6 +444,8 @@ export default {
|
|||
},
|
||||
structureEditor: {
|
||||
title: "Edit Table Structure",
|
||||
createTitle: "Create Table",
|
||||
tableName: "Table Name",
|
||||
refresh: "Refresh Structure",
|
||||
columns: "Columns",
|
||||
indexes: "Indexes",
|
||||
|
|
|
|||
|
|
@ -408,6 +408,24 @@ export default {
|
|||
importData: "导入数据",
|
||||
exportData: "导出数据",
|
||||
exportStructure: "导出表结构",
|
||||
createTable: "新建表",
|
||||
dropTable: "删除表",
|
||||
emptyTable: "清空数据",
|
||||
truncateTable: "截断表",
|
||||
duplicateStructure: "复制表结构",
|
||||
confirmDropTableTitle: "删除表",
|
||||
confirmDropTableMessage: "确定要删除「{name}」吗?这将永久删除表及其所有数据。",
|
||||
confirmEmptyTableTitle: "清空数据",
|
||||
confirmEmptyTableMessage: "确定要删除「{name}」中的所有数据吗?",
|
||||
confirmTruncateTableTitle: "截断表",
|
||||
confirmTruncateTableMessage: "确定要截断「{name}」吗?这将删除所有行。",
|
||||
dropTableSuccess: "表「{name}」已删除",
|
||||
emptyTableSuccess: "已清空「{name}」的所有数据",
|
||||
truncateTableSuccess: "表「{name}」已截断",
|
||||
duplicateStructureSuccess: "已复制表结构为「{name}」",
|
||||
tableOperationFailed: "操作失败:{message}",
|
||||
duplicateNameTitle: "复制表结构",
|
||||
duplicateNamePlaceholder: "新表名",
|
||||
},
|
||||
tree: {
|
||||
columns: "字段",
|
||||
|
|
@ -417,6 +435,8 @@ export default {
|
|||
},
|
||||
structureEditor: {
|
||||
title: "编辑表结构",
|
||||
createTitle: "新建表",
|
||||
tableName: "表名",
|
||||
refresh: "刷新结构",
|
||||
columns: "字段",
|
||||
indexes: "索引",
|
||||
|
|
|
|||
|
|
@ -297,3 +297,76 @@ export function buildTableStructureChangeSql(options: BuildTableStructureChangeS
|
|||
|
||||
return { statements, warnings };
|
||||
}
|
||||
|
||||
export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions): TableStructureChangeSql {
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!clean(options.tableName)) {
|
||||
warnings.push("Table name is required.");
|
||||
}
|
||||
|
||||
const activeColumns = options.columns.filter((c) => !c.markedForDrop);
|
||||
if (activeColumns.length === 0) {
|
||||
warnings.push("At least one column is required.");
|
||||
}
|
||||
|
||||
const names = new Set<string>();
|
||||
for (const col of activeColumns) {
|
||||
if (!clean(col.name)) warnings.push("Column name cannot be empty.");
|
||||
if (!clean(col.dataType)) warnings.push(`Column "${col.name || "(new)"}" type cannot be empty.`);
|
||||
const key = clean(col.name).toLowerCase();
|
||||
if (key && names.has(key)) warnings.push(`Column "${col.name}" is duplicated.`);
|
||||
if (key) names.add(key);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) return { statements: [], warnings };
|
||||
|
||||
const databaseType = options.databaseType;
|
||||
const table = qualifiedTable(databaseType, options.schema, options.tableName);
|
||||
const statements: string[] = [];
|
||||
|
||||
const pkColumns = activeColumns.filter((c) => c.isPrimaryKey);
|
||||
const colDefs = activeColumns.map((col) => {
|
||||
const parts = [quoteIdent(databaseType, col.name), col.dataType.trim()];
|
||||
if (!col.isNullable && !col.isPrimaryKey) parts.push("NOT NULL");
|
||||
const defaultValue = normalizeDefault(col.defaultValue);
|
||||
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
|
||||
if (databaseType === "mysql" && clean(col.comment)) {
|
||||
parts.push(`COMMENT ${quoteString(clean(col.comment))}`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
});
|
||||
|
||||
if (pkColumns.length > 0) {
|
||||
const pkList = pkColumns.map((c) => quoteIdent(databaseType, c.name)).join(", ");
|
||||
colDefs.push(`PRIMARY KEY (${pkList})`);
|
||||
}
|
||||
|
||||
statements.push(`CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n);`);
|
||||
|
||||
if (databaseType === "postgres") {
|
||||
for (const col of activeColumns) {
|
||||
if (clean(col.comment)) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, col.name)} IS ${quoteString(clean(col.comment))};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of options.indexes.filter((idx) => !idx.markedForDrop && !idx.isPrimary)) {
|
||||
const name = clean(index.name);
|
||||
const columns = index.columns.map(clean).filter(Boolean);
|
||||
if (!name || columns.length === 0) continue;
|
||||
const unique = index.isUnique ? "UNIQUE " : "";
|
||||
const cols = columns.map((c) => quoteIdent(databaseType, c)).join(", ");
|
||||
const idxType = clean(index.indexType);
|
||||
const usingClause = idxType && databaseType === "postgres" ? ` USING ${idxType}` : "";
|
||||
const typePrefix = idxType && databaseType === "sqlserver" ? `${idxType} ` : "";
|
||||
statements.push(
|
||||
`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols});`,
|
||||
);
|
||||
}
|
||||
|
||||
return { statements, warnings };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue