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:
t8y2
2026-05-06 18:22:41 +08:00
parent 0d1b99e3ee
commit b6a39c1fa0
15 changed files with 279 additions and 32 deletions
+6
View File
@@ -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"
+56 -1
View File
@@ -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" />
+50
View File
@@ -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,
};
}
+1
View File
@@ -128,6 +128,7 @@ export default {
noDatabase: "No database selected",
selectConnection: "Select connection",
selectDatabase: "Select database",
selectSchema: "Select schema",
},
tabs: {
sql: "SQL",
+1
View File
@@ -127,6 +127,7 @@ export default {
noDatabase: "未选择数据库",
selectConnection: "选择连接",
selectDatabase: "选择数据库",
selectSchema: "选择模式",
},
tabs: {
sql: "SQL",
+18 -6
View File
@@ -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
View File
@@ -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(
+11 -2
View File
@@ -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,
+1
View File
@@ -147,6 +147,7 @@ export interface QueryTab {
title: string;
connectionId: string;
database: string;
schema?: string;
sql: string;
lastExecutedSql?: string;
pinned?: boolean;