mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +00:00
feat(query): set schema context (search_path) before query execution (#113)
Add schema selector to the editor toolbar for schema-aware databases (PostgreSQL, SQL Server, Oracle). When a schema is selected, the backend sets the appropriate search_path before executing queries so users no longer need to write schema-qualified table names.
This commit is contained in:
@@ -248,6 +248,11 @@ function changeActiveDatabase(database: string) {
|
||||
if (tab) queryStore.updateDatabase(tab.id, database);
|
||||
}
|
||||
|
||||
function changeActiveSchema(schema: string | undefined) {
|
||||
const tab = activeTab.value;
|
||||
if (tab) queryStore.updateSchema(tab.id, schema);
|
||||
}
|
||||
|
||||
function toggleLocale() {
|
||||
setLocale(currentLocale() === "zh-CN" ? "en" : "zh-CN");
|
||||
}
|
||||
@@ -426,6 +431,7 @@ onUnmounted(() => {
|
||||
@open-sql="openSqlFile"
|
||||
@change-connection="changeActiveConnection"
|
||||
@change-database="changeActiveDatabase"
|
||||
@change-schema="changeActiveSchema"
|
||||
/>
|
||||
<ContentArea
|
||||
:active-tab="activeTab"
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, Loader2, Square, Database, Table2, AlignLeft, GitBranch, Save, FolderOpen } from "lucide-vue-next";
|
||||
import {
|
||||
Play,
|
||||
Loader2,
|
||||
Square,
|
||||
Database,
|
||||
Table2,
|
||||
AlignLeft,
|
||||
GitBranch,
|
||||
Save,
|
||||
FolderOpen,
|
||||
Layers,
|
||||
} from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import { useSchemaOptions } from "@/composables/useSchemaOptions";
|
||||
import { connectionIconType } from "@/lib/connectionPresentation";
|
||||
import { connectionDisplayName } from "@/lib/tabPresentation";
|
||||
import type { QueryTab, ConnectionConfig } from "@/types/database";
|
||||
@@ -27,11 +39,13 @@ const emit = defineEmits<{
|
||||
openSql: [];
|
||||
changeConnection: [connectionId: string];
|
||||
changeDatabase: [database: string];
|
||||
changeSchema: [schema: string | undefined];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions } = useDatabaseOptions();
|
||||
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
|
||||
|
||||
const activeDatabaseOptions = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
@@ -40,6 +54,18 @@ const activeDatabaseOptions = computed(() => {
|
||||
|
||||
const activeDatabaseValue = computed(() => props.activeTab.database || "");
|
||||
const activeConnectionValue = computed(() => props.activeConnection?.id || "");
|
||||
const activeSchemaValue = computed(() => props.activeTab.schema || "");
|
||||
|
||||
const showSchemaSelector = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
return connection && isSchemaAware(connection.id) && props.activeTab.database;
|
||||
});
|
||||
|
||||
const activeSchemaOptions = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
if (!connection) return [];
|
||||
return getSchemaOptionsForDb(connection.id, props.activeTab.database);
|
||||
});
|
||||
|
||||
function databaseDisplayName(database: string): string {
|
||||
const connection = props.activeConnection;
|
||||
@@ -185,6 +211,35 @@ function databaseDisplayName(database: string): string {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="showSchemaSelector" class="flex items-center gap-1">
|
||||
<Layers class="h-3.5 w-3.5 shrink-0" />
|
||||
<Select
|
||||
:model-value="activeSchemaValue"
|
||||
@update:model-value="(v: any) => emit('changeSchema', v || undefined)"
|
||||
@update:open="
|
||||
(open: boolean) => {
|
||||
if (open && activeConnection) loadSchemaOptions(activeConnection.id, activeTab.database).catch(() => {});
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-6 w-auto max-w-56 border-0 bg-transparent px-1 text-xs shadow-none focus:ring-0">
|
||||
<SelectValue
|
||||
:placeholder="
|
||||
activeConnection && isLoadingSchemas(activeConnection.id, activeTab.database)
|
||||
? t('common.loading')
|
||||
: t('editor.selectSchema')
|
||||
"
|
||||
>
|
||||
{{ activeSchemaValue || t("editor.selectSchema") }}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="schema in activeSchemaOptions" :key="schema" :value="schema">
|
||||
{{ schema }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeTab.tableMeta" class="flex min-w-0 items-center gap-1 ml-2">
|
||||
<Table2 class="h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ref } from "vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
export function useSchemaOptions() {
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const schemaOptions = ref<Record<string, string[]>>({});
|
||||
const loadingSchemaOptions = ref<Record<string, boolean>>({});
|
||||
|
||||
function cacheKey(connectionId: string, database: string) {
|
||||
return `${connectionId}:${database}`;
|
||||
}
|
||||
|
||||
function isSchemaAware(connectionId: string): boolean {
|
||||
const dbType = connectionStore.getConfig(connectionId)?.db_type;
|
||||
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle";
|
||||
}
|
||||
|
||||
async function loadSchemaOptions(connectionId: string, database: string) {
|
||||
if (!isSchemaAware(connectionId) || !database) return;
|
||||
const key = cacheKey(connectionId, database);
|
||||
if (loadingSchemaOptions.value[key]) return;
|
||||
|
||||
loadingSchemaOptions.value[key] = true;
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
schemaOptions.value[key] = await api.listSchemas(connectionId, database);
|
||||
} finally {
|
||||
loadingSchemaOptions.value[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSchemaOptionsForDb(connectionId: string, database: string): string[] {
|
||||
return schemaOptions.value[cacheKey(connectionId, database)] ?? [];
|
||||
}
|
||||
|
||||
function isLoadingSchemas(connectionId: string, database: string): boolean {
|
||||
return !!loadingSchemaOptions.value[cacheKey(connectionId, database)];
|
||||
}
|
||||
|
||||
return {
|
||||
schemaOptions,
|
||||
loadingSchemaOptions,
|
||||
loadSchemaOptions,
|
||||
getSchemaOptionsForDb,
|
||||
isLoadingSchemas,
|
||||
isSchemaAware,
|
||||
};
|
||||
}
|
||||
@@ -128,6 +128,7 @@ export default {
|
||||
noDatabase: "No database selected",
|
||||
selectConnection: "Select connection",
|
||||
selectDatabase: "Select database",
|
||||
selectSchema: "Select schema",
|
||||
},
|
||||
tabs: {
|
||||
sql: "SQL",
|
||||
|
||||
@@ -127,6 +127,7 @@ export default {
|
||||
noDatabase: "未选择数据库",
|
||||
selectConnection: "选择连接",
|
||||
selectDatabase: "选择数据库",
|
||||
selectSchema: "选择模式",
|
||||
},
|
||||
tabs: {
|
||||
sql: "SQL",
|
||||
|
||||
+18
-6
@@ -157,26 +157,38 @@ export async function executeQuery(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute", { connectionId, database, sql, executionId });
|
||||
return post("/api/query/execute", { connectionId, database, sql, schema, executionId });
|
||||
}
|
||||
|
||||
export async function executeMulti(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult[]> {
|
||||
return post("/api/query/execute-multi", { connectionId, database, sql, executionId });
|
||||
return post("/api/query/execute-multi", { connectionId, database, sql, schema, executionId });
|
||||
}
|
||||
|
||||
export async function executeBatch(connectionId: string, database: string, statements: string[]): Promise<QueryResult> {
|
||||
return post("/api/query/execute-batch", { connectionId, database, statements });
|
||||
export async function executeBatch(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
statements: string[],
|
||||
schema?: string,
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute-batch", { connectionId, database, statements, schema });
|
||||
}
|
||||
|
||||
export async function executeScript(connectionId: string, database: string, sql: string): Promise<QueryResult> {
|
||||
return post("/api/query/execute-script", { connectionId, database, sql });
|
||||
export async function executeScript(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute-script", { connectionId, database, sql, schema });
|
||||
}
|
||||
|
||||
export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
|
||||
+18
-6
@@ -138,30 +138,42 @@ export async function executeQuery(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult> {
|
||||
return invoke("execute_query", { connectionId, database, sql, executionId });
|
||||
return invoke("execute_query", { connectionId, database, sql, schema, executionId });
|
||||
}
|
||||
|
||||
export async function executeMulti(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
): Promise<QueryResult[]> {
|
||||
return invoke("execute_multi", { connectionId, database, sql, executionId });
|
||||
return invoke("execute_multi", { connectionId, database, sql, schema, executionId });
|
||||
}
|
||||
|
||||
export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return invoke("cancel_query", { executionId });
|
||||
}
|
||||
|
||||
export async function executeBatch(connectionId: string, database: string, statements: string[]): Promise<QueryResult> {
|
||||
return invoke("execute_batch", { connectionId, database, statements });
|
||||
export async function executeBatch(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
statements: string[],
|
||||
schema?: string,
|
||||
): Promise<QueryResult> {
|
||||
return invoke("execute_batch", { connectionId, database, statements, schema });
|
||||
}
|
||||
|
||||
export async function executeScript(connectionId: string, database: string, sql: string): Promise<QueryResult> {
|
||||
return invoke("execute_script", { connectionId, database, sql });
|
||||
export async function executeScript(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
): Promise<QueryResult> {
|
||||
return invoke("execute_script", { connectionId, database, sql, schema });
|
||||
}
|
||||
|
||||
export async function listIndexes(
|
||||
|
||||
@@ -148,17 +148,25 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab || tab.database === database) return;
|
||||
tab.database = database;
|
||||
tab.schema = undefined;
|
||||
tab.result = undefined;
|
||||
tab.lastExecutedSql = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
}
|
||||
|
||||
function updateSchema(id: string, schema: string | undefined) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab || tab.schema === schema) return;
|
||||
tab.schema = schema;
|
||||
}
|
||||
|
||||
function updateConnection(id: string, connectionId: string, database = "") {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab || tab.connectionId === connectionId) return;
|
||||
tab.connectionId = connectionId;
|
||||
tab.database = database;
|
||||
tab.schema = undefined;
|
||||
tab.result = undefined;
|
||||
tab.lastExecutedSql = undefined;
|
||||
clearExplain(tab);
|
||||
@@ -229,7 +237,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.executionId = executionId;
|
||||
tab.lastExecutedSql = sql;
|
||||
try {
|
||||
const results = await api.executeMulti(tab.connectionId, tab.database, sql, executionId);
|
||||
const results = await api.executeMulti(tab.connectionId, tab.database, sql, tab.schema, executionId);
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
if (current?.executionId === executionId) {
|
||||
if (results.length > 1) {
|
||||
@@ -278,7 +286,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.explainSql = built.sql;
|
||||
tab.lastExplainedSql = sql;
|
||||
try {
|
||||
const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, executionId);
|
||||
const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, tab.schema, executionId);
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
if (current?.explainExecutionId === executionId) {
|
||||
current.explainPlan = parseExplainResult(databaseType as "mysql" | "postgres", result);
|
||||
@@ -373,6 +381,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
updateSql,
|
||||
togglePinnedTab,
|
||||
updateDatabase,
|
||||
updateSchema,
|
||||
updateConnection,
|
||||
setTableMeta,
|
||||
setExecuting,
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface QueryTab {
|
||||
title: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
lastExecutedSql?: string;
|
||||
pinned?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user