Merge pull request #140 from Bacon2994/codex/default-database

feat: 支持设置默认数据库
This commit is contained in:
skyler 2026-05-07 17:41:51 +08:00 committed by GitHub
commit db16bf5edd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 178 additions and 9 deletions

View File

@ -29,6 +29,7 @@ import { useDataGridActions } from "@/composables/useDataGridActions";
import { useTauriEvents } from "@/composables/useTauriEvents";
import { setLocale, currentLocale } from "@/i18n";
import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts";
@ -204,24 +205,31 @@ async function openSqlFile() {
}
}
function newQuery() {
async function newQuery() {
const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id;
if (!connId) return;
const conn = connectionStore.getConfig(connId);
if (!conn) return;
connectionStore.activeConnectionId = connId;
queryStore.createTab(conn.id, conn.database || "");
const tabId = queryStore.createTab(conn.id, resolveDefaultDatabase(conn, []));
try {
await connectionStore.ensureConnected(connId);
const options = await getDatabaseOptions(connId);
queryStore.updateDatabase(tabId, resolveDefaultDatabase(conn, options));
} catch (e: any) {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}
}
async function openConnectionQuery(connectionId: string) {
const connection = connectionStore.getConfig(connectionId);
if (!connection) return;
connectionStore.activeConnectionId = connectionId;
const tabId = queryStore.createTab(connectionId, connection.database || "");
const tabId = queryStore.createTab(connectionId, resolveDefaultDatabase(connection, []));
try {
await connectionStore.ensureConnected(connectionId);
const options = await getDatabaseOptions(connectionId);
if (!connection.database && options[0]) queryStore.updateDatabase(tabId, options[0]);
queryStore.updateDatabase(tabId, resolveDefaultDatabase(connection, options));
} catch (e: any) {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}
@ -253,12 +261,12 @@ async function changeActiveConnection(connectionId: string) {
if (!tab) return;
const connection = connectionStore.getConfig(connectionId);
if (!connection) return;
queryStore.updateConnection(tab.id, connectionId, connection.database || "");
queryStore.updateConnection(tab.id, connectionId, resolveDefaultDatabase(connection, []));
connectionStore.activeConnectionId = connectionId;
try {
await connectionStore.ensureConnected(connectionId);
const options = await getDatabaseOptions(connectionId);
if (!connection.database && options[0]) queryStore.updateDatabase(tab.id, options[0]);
queryStore.updateDatabase(tab.id, resolveDefaultDatabase(connection, options));
} catch (e: any) {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}
@ -269,6 +277,18 @@ function changeActiveDatabase(database: string) {
if (tab) queryStore.updateDatabase(tab.id, database);
}
async function setActiveDatabaseAsDefault() {
const tab = activeTab.value;
if (!tab || !tab.connectionId || !tab.database) return;
await connectionStore.setDefaultDatabase(tab.connectionId, tab.database);
}
async function clearActiveDefaultDatabase() {
const tab = activeTab.value;
if (!tab || !tab.connectionId) return;
await connectionStore.clearDefaultDatabase(tab.connectionId);
}
function changeActiveSchema(schema: string | undefined) {
const tab = activeTab.value;
if (tab) queryStore.updateSchema(tab.id, schema);
@ -453,6 +473,8 @@ onUnmounted(() => {
@change-connection="changeActiveConnection"
@change-database="changeActiveDatabase"
@change-schema="changeActiveSchema"
@set-default-database="setActiveDatabaseAsDefault"
@clear-default-database="clearActiveDefaultDatabase"
/>
<ContentArea
:active-tab="activeTab"

View File

@ -57,6 +57,7 @@ import {
import type { AiMessage } from "@/lib/api";
import type { ConnectionConfig, QueryTab } from "@/types/database";
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
const { t } = useI18n();
const settings = useSettingsStore();
@ -143,12 +144,16 @@ async function changeConnection(connectionId: string) {
connectionStore.activeConnectionId = connectionId;
const tab = props.tab;
if (tab) {
queryStore.updateConnection(tab.id, connectionId, conn.database || "");
queryStore.updateConnection(tab.id, connectionId, resolveDefaultDatabase(conn, []));
} else {
queryStore.createTab(connectionId, conn.database || "");
queryStore.createTab(connectionId, resolveDefaultDatabase(conn, []));
}
try {
await loadDatabaseOptions(connectionId);
const database = resolveDefaultDatabase(conn, allDbOptions.value[connectionId] || []);
if (tab) {
queryStore.updateDatabase(tab.id, database);
}
} catch (e: any) {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}

View File

@ -6,6 +6,7 @@ import {
Loader2,
Square,
Database,
Check,
Table2,
AlignLeft,
GitBranch,
@ -21,6 +22,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
import { useSchemaOptions } from "@/composables/useSchemaOptions";
import { connectionIconType } from "@/lib/connectionPresentation";
import { isDefaultDatabase } from "@/lib/defaultDatabase";
import { connectionDisplayName } from "@/lib/tabPresentation";
import type { QueryTab, ConnectionConfig } from "@/types/database";
@ -40,6 +42,8 @@ const emit = defineEmits<{
changeConnection: [connectionId: string];
changeDatabase: [database: string];
changeSchema: [schema: string | undefined];
setDefaultDatabase: [];
clearDefaultDatabase: [];
}>();
const { t } = useI18n();
@ -67,6 +71,8 @@ const activeSchemaOptions = computed(() => {
return getSchemaOptionsForDb(connection.id, props.activeTab.database);
});
const isActiveDatabaseDefault = computed(() => isDefaultDatabase(props.activeConnection, activeDatabaseValue.value));
function databaseDisplayName(database: string): string {
const connection = props.activeConnection;
if (connection?.db_type === "redis" && database !== "") return `db${database}`;
@ -210,6 +216,16 @@ function databaseDisplayName(database: string): string {
</SelectItem>
</SelectContent>
</Select>
<Button
v-if="activeDatabaseValue"
variant="ghost"
size="sm"
class="h-6 px-2 text-[11px]"
@click="isActiveDatabaseDefault ? emit('clearDefaultDatabase') : emit('setDefaultDatabase')"
>
<Check v-if="isActiveDatabaseDefault" class="h-3 w-3" />
{{ isActiveDatabaseDefault ? t("editor.defaultDatabase") : t("editor.setDefaultDatabase") }}
</Button>
</div>
<div v-if="showSchemaSelector" class="flex items-center gap-1">
<Layers class="h-3.5 w-3.5 shrink-0" />

View File

@ -51,8 +51,10 @@ import {
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useToast } from "@/composables/useToast";
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
import type { DatabaseType, QueryResult, TreeNode, TreeNodeType } from "@/types/database";
import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import {
DATABASE_EXPORT_PAGE_SIZE,
DATABASE_EXPORT_ROW_LIMIT,
@ -80,12 +82,14 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
const { t } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const { toast } = useToast();
const { getDatabaseOptions } = useDatabaseOptions();
const props = defineProps<{
node: TreeNode;
@ -295,12 +299,39 @@ async function newQuery() {
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
queryStore.createTab(node.connectionId, node.database || "", undefined, "query");
if (node.database) {
queryStore.createTab(node.connectionId, node.database, undefined, "query");
return;
}
const connection = connectionStore.getConfig(node.connectionId);
if (!connection) return;
const options = await getDatabaseOptions(node.connectionId);
queryStore.createTab(node.connectionId, resolveDefaultDatabase(connection, options), undefined, "query");
} catch (e: any) {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}
}
async function setNodeAsDefaultDatabase() {
const node = props.node;
if (!node.connectionId || !node.database) return;
try {
await connectionStore.setDefaultDatabase(node.connectionId, node.database);
} catch (e: any) {
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
async function clearNodeDefaultDatabase() {
const node = props.node;
if (!node.connectionId) return;
try {
await connectionStore.clearDefaultDatabase(node.connectionId);
} catch (e: any) {
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
async function refresh() {
const node = props.node;
node.isExpanded = false;
@ -818,6 +849,13 @@ const canOpenFieldLineage = computed(() => {
);
});
const isPinned = computed(() => props.node.pinned || connectionStore.isTreeNodePinned(props.node.id));
const isNodeDefaultDatabase = computed(
() =>
(props.node.type === "database" || props.node.type === "redis-db" || props.node.type === "mongo-db") &&
!!props.node.connectionId &&
!!props.node.database &&
connectionStore.isDefaultDatabase(props.node.connectionId, props.node.database),
);
const hasTypeMenu = computed(() => {
const t = props.node.type;
return (
@ -1047,6 +1085,9 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
@vue:mounted="($event: any) => $event.el.focus()"
/>
<span v-else class="min-w-0 flex-1 truncate">{{ isGroupLabel(node) ? t(node.label) : node.label }}</span>
<Badge v-if="isNodeDefaultDatabase" variant="secondary" class="h-4 px-1.5 text-[10px]">
{{ t("editor.defaultDatabase") }}
</Badge>
<span v-if="columnComment" class="truncate text-muted-foreground/60 text-[10px] max-w-[40%]">{{
columnComment
}}</span>
@ -1165,6 +1206,12 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
<ContextMenuItem @click="newQuery">
<TerminalSquare class="w-4 h-4" /> {{ t("contextMenu.newQuery") }}
</ContextMenuItem>
<ContextMenuItem v-if="node.type === 'database' && !isNodeDefaultDatabase" @click="setNodeAsDefaultDatabase">
<Database class="w-4 h-4" /> {{ t("contextMenu.setDefaultDatabase") }}
</ContextMenuItem>
<ContextMenuItem v-if="node.type === 'database' && isNodeDefaultDatabase" @click="clearNodeDefaultDatabase">
<Database class="w-4 h-4" /> {{ t("contextMenu.clearDefaultDatabase") }}
</ContextMenuItem>
<ContextMenuItem v-if="canCreateTable" @click="createTable">
<Plus class="w-4 h-4" /> {{ t("contextMenu.createTable") }}
</ContextMenuItem>
@ -1194,6 +1241,18 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
</ContextMenuItem>
</template>
<template v-if="node.type === 'redis-db' || node.type === 'mongo-db'">
<ContextMenuItem @click="newQuery">
<TerminalSquare class="w-4 h-4" /> {{ t("contextMenu.newQuery") }}
</ContextMenuItem>
<ContextMenuItem v-if="!isNodeDefaultDatabase" @click="setNodeAsDefaultDatabase">
<Database class="w-4 h-4" /> {{ t("contextMenu.setDefaultDatabase") }}
</ContextMenuItem>
<ContextMenuItem v-if="isNodeDefaultDatabase" @click="clearNodeDefaultDatabase">
<Database class="w-4 h-4" /> {{ t("contextMenu.clearDefaultDatabase") }}
</ContextMenuItem>
</template>
<template v-if="node.type === 'table' || node.type === 'view'">
<ContextMenuItem @click="openData">
<TableProperties class="w-4 h-4" /> {{ t("contextMenu.viewData") }}

View File

@ -134,6 +134,8 @@ export default {
selectConnection: "Select connection",
selectDatabase: "Select database",
selectSchema: "Select schema",
setDefaultDatabase: "Set Default",
defaultDatabase: "Default",
},
tabs: {
sql: "SQL",
@ -415,6 +417,8 @@ export default {
closeOtherTabs: "Close Other Tabs",
closeAllTabs: "Close All Tabs",
copyName: "Copy Name",
setDefaultDatabase: "Set as Default Database",
clearDefaultDatabase: "Clear Default Database",
exportDatabase: "Export Database",
exportDatabaseSuccess: "Exported {count} tables, up to {limit} rows each",
exportDatabaseFailed: "Failed to export database: {message}",

View File

@ -133,6 +133,8 @@ export default {
selectConnection: "选择连接",
selectDatabase: "选择数据库",
selectSchema: "选择模式",
setDefaultDatabase: "设为默认",
defaultDatabase: "默认库",
},
tabs: {
sql: "SQL",
@ -407,6 +409,8 @@ export default {
closeOtherTabs: "关闭其他标签页",
closeAllTabs: "关闭全部标签页",
copyName: "复制名称",
setDefaultDatabase: "设为默认数据库",
clearDefaultDatabase: "取消默认数据库",
exportDatabase: "导出数据库",
exportDatabaseSuccess: "已导出 {count} 张表,每表最多 {limit} 行",
exportDatabaseFailed: "导出数据库失败:{message}",

View File

@ -0,0 +1,12 @@
import type { ConnectionConfig } from "@/types/database";
export function resolveDefaultDatabase(connection: Pick<ConnectionConfig, "database">, options: string[]): string {
return connection.database || options[0] || "";
}
export function isDefaultDatabase(
connection: Pick<ConnectionConfig, "database"> | undefined,
database: string,
): boolean {
return !!connection?.database && !!database && connection.database === database;
}

View File

@ -264,6 +264,28 @@ export const useConnectionStore = defineStore("connection", () => {
invalidateCompletionCache(config.id);
}
async function setDefaultDatabase(connectionId: string, database: string) {
const config = getConfig(connectionId);
if (!config || config.database === database) return;
await updateConnection({
...config,
database,
});
}
async function clearDefaultDatabase(connectionId: string) {
const config = getConfig(connectionId);
if (!config || !config.database) return;
await updateConnection({
...config,
database: undefined,
});
}
function isDefaultDatabase(connectionId: string, database: string): boolean {
return getConfig(connectionId)?.database === database && database !== "";
}
async function connect(config: ConnectionConfig) {
config = normalizeConnection(config);
const pendingNode = findNode(treeNodes.value, config.id);
@ -934,6 +956,9 @@ export const useConnectionStore = defineStore("connection", () => {
addConnection,
addEphemeralConnection,
updateConnection,
setDefaultDatabase,
clearDefaultDatabase,
isDefaultDatabase,
removeConnection,
editingConnectionId,
startEditing,

View File

@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isDefaultDatabase, resolveDefaultDatabase } from "../src/lib/defaultDatabase.ts";
test("优先使用连接上已保存的默认数据库", () => {
assert.equal(resolveDefaultDatabase({ database: "analytics" }, ["app", "analytics"]), "analytics");
});
test("默认数据库为空时回退到首个可选数据库", () => {
assert.equal(resolveDefaultDatabase({ database: undefined }, ["app", "analytics"]), "app");
});
test("没有默认数据库且无候选项时返回空字符串", () => {
assert.equal(resolveDefaultDatabase({ database: undefined }, []), "");
});
test("判断当前数据库是否为默认数据库", () => {
assert.equal(isDefaultDatabase({ database: "analytics" }, "analytics"), true);
assert.equal(isDefaultDatabase({ database: "analytics" }, "app"), false);
assert.equal(isDefaultDatabase(undefined, "analytics"), false);
assert.equal(isDefaultDatabase({ database: "analytics" }, ""), false);
});