feat(query): pin, rename and label result run tabs
This commit is contained in:
26 files changed
+319
-32
No files matched your search
@@ -661,6 +661,7 @@ const debugLogCopied = ref(false);
|
||||
const debugLogDownloaded = ref(false);
|
||||
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const editShowResultSourceDatabase = ref(settingsStore.editorSettings.showResultSourceDatabase);
|
||||
const editDataGridShowTransposeFieldMetadata = ref(settingsStore.editorSettings.dataGridShowTransposeFieldMetadata);
|
||||
const editColorizeDataGridCellTypes = ref(settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showIndexIndicatorsInHeader);
|
||||
@@ -1002,6 +1003,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
tabSortMode: editTabSortMode.value,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
|
||||
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
|
||||
showResultSourceDatabase: editShowResultSourceDatabase.value,
|
||||
dataGridShowTransposeFieldMetadata: editDataGridShowTransposeFieldMetadata.value,
|
||||
colorizeDataGridCellTypes: editColorizeDataGridCellTypes.value,
|
||||
dataGridTypeColorSchemes: editDataGridTypeColorSchemes.value,
|
||||
@@ -1647,6 +1649,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editTabSortMode.value = settingsStore.editorSettings.tabSortMode;
|
||||
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
|
||||
editShowResultSourceDatabase.value = settingsStore.editorSettings.showResultSourceDatabase;
|
||||
editDataGridShowTransposeFieldMetadata.value = settingsStore.editorSettings.dataGridShowTransposeFieldMetadata;
|
||||
editColorizeDataGridCellTypes.value = settingsStore.editorSettings.colorizeDataGridCellTypes;
|
||||
editDataGridTypeColorSchemes.value = cloneDataGridTypeColorSchemes(settingsStore.editorSettings.dataGridTypeColorSchemes);
|
||||
@@ -1780,6 +1783,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
|
||||
tabSortMode: editTabSortMode,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader,
|
||||
showColumnTypesInHeader: editShowColumnTypesInHeader,
|
||||
showResultSourceDatabase: editShowResultSourceDatabase,
|
||||
dataGridShowTransposeFieldMetadata: editDataGridShowTransposeFieldMetadata,
|
||||
colorizeDataGridCellTypes: editColorizeDataGridCellTypes,
|
||||
dataGridTypeColorSchemes: editDataGridTypeColorSchemes,
|
||||
@@ -2304,6 +2308,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
historyRetention.reset();
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
|
||||
editShowResultSourceDatabase.value = DEFAULT_EDITOR_SETTINGS.showResultSourceDatabase;
|
||||
editDataGridShowTransposeFieldMetadata.value = DEFAULT_EDITOR_SETTINGS.dataGridShowTransposeFieldMetadata;
|
||||
editColorizeDataGridCellTypes.value = DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes;
|
||||
// Back to the built-in palette, but keep the user's saved schemes available.
|
||||
@@ -7908,6 +7913,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Switch id="show-column-types-in-header" v-model="editShowColumnTypesInHeader" />
|
||||
</div>
|
||||
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="show-result-source-database">
|
||||
{{ t("settings.showResultSourceDatabase") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.showResultSourceDatabaseDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="show-result-source-database" v-model="editShowResultSourceDatabase" />
|
||||
</div>
|
||||
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="data-grid-show-transpose-field-metadata">
|
||||
|
||||
@@ -494,8 +494,10 @@ const activeQueryError = computed(() => {
|
||||
return String(result.rows[0]?.[0] ?? "");
|
||||
});
|
||||
const hasQueryOutput = computed(() => tabHasQueryOutput(props.activeTab));
|
||||
const visibleResultItems = computed(() => tabularResultItems(props.activeTab.results ?? (props.activeTab.result ? [props.activeTab.result] : undefined)));
|
||||
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
|
||||
// 结果集页签/列表的名称是否带库名,由编辑器设置控制(默认带库名)
|
||||
const includeResultSourceDatabase = computed(() => settingsStore.editorSettings.showResultSourceDatabase);
|
||||
const visibleResultItems = computed(() => tabularResultItems(props.activeTab.results ?? (props.activeTab.result ? [props.activeTab.result] : undefined), { includeSourceDatabase: includeResultSourceDatabase.value }));
|
||||
const tabularResults = computed(() => tabularResultItems(props.activeTab.results, { includeSourceDatabase: includeResultSourceDatabase.value }));
|
||||
const allResultExportSheets = computed(() =>
|
||||
tabularResults.value.map((item) => ({
|
||||
sheetName: item.label || t("tabs.resultN", { n: item.n }),
|
||||
@@ -503,7 +505,14 @@ const allResultExportSheets = computed(() =>
|
||||
sql: item.index === props.activeTab.activeResultIndex ? queryResultExecutionSql(props.activeTab) : item.result.sourceStatement,
|
||||
})),
|
||||
);
|
||||
const resultRuns = computed(() => resultRunItems(props.activeTab));
|
||||
// 结果标签优先显示来源(表名),历史批次缺少来源信息时按批次 SQL 重新解析
|
||||
const resultRuns = computed(() =>
|
||||
resultRunItems(props.activeTab, {
|
||||
includeSourceDatabase: includeResultSourceDatabase.value,
|
||||
database: props.activeTab.database,
|
||||
databaseType: activeEffectiveDatabaseType.value,
|
||||
}),
|
||||
);
|
||||
const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab));
|
||||
const activeResultGridColumnWidthCacheKey = computed(() => resultGridColumnWidthCacheKey(props.activeTab));
|
||||
const activeResultGridInstanceKey = computed(() => resultGridInstanceKey(props.activeTab));
|
||||
@@ -1243,7 +1252,7 @@ async function closeResultRunsToRight(runId: string) {
|
||||
|
||||
function openResultRunRename(run: (typeof resultRuns.value)[number]) {
|
||||
resultRunRenameId.value = run.id;
|
||||
resultRunRenameTitle.value = run.title || t("tabs.runN", { n: run.sequence });
|
||||
resultRunRenameTitle.value = run.title || run.sourceLabel || t("tabs.runN", { n: run.sequence });
|
||||
resultRunRenameOpen.value = true;
|
||||
nextTick(() => {
|
||||
const input = document.querySelector<HTMLInputElement>("[data-result-run-name-input]");
|
||||
@@ -1314,6 +1323,16 @@ async function selectResultRun(runId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击结果标签:切换显示的同时固定该结果(issue #9975)。
|
||||
* 未固定的结果会被下一次普通查询复用/覆盖,点击后固定即可保留下来。
|
||||
*/
|
||||
async function selectResultRunFromTab(runId: string) {
|
||||
if (!(await selectResultRun(runId))) return;
|
||||
const run = resultRuns.value.find((item) => item.id === runId);
|
||||
if (run && !run.pinned) toggleResultRunPinned(runId);
|
||||
}
|
||||
|
||||
async function focusResultRunByIndex(index: number) {
|
||||
const run = resultRuns.value[index];
|
||||
if (!run) return;
|
||||
@@ -1743,11 +1762,11 @@ defineExpose({
|
||||
:aria-selected="run.active"
|
||||
:data-active-result-run="run.active ? 'true' : undefined"
|
||||
class="flex h-full select-none items-center gap-1 whitespace-nowrap pl-2.5 pr-1 text-xs font-medium outline-none focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"
|
||||
@click="selectResultRun(run.id)"
|
||||
@click="selectResultRunFromTab(run.id)"
|
||||
@keydown="onResultRunTabKeydown($event, runIndex)"
|
||||
>
|
||||
<Pin v-if="run.pinned" class="h-3 w-3 shrink-0 fill-current text-primary" />
|
||||
{{ run.title || t("tabs.runN", { n: run.sequence }) }}
|
||||
{{ run.title || run.sourceLabel || t("tabs.runN", { n: run.sequence }) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1767,17 +1786,17 @@ defineExpose({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-6 max-w-48 gap-1 px-2 text-xs">
|
||||
<span class="min-w-0 truncate">{{ activeResultRunItem ? activeResultRunItem.title || t("tabs.runN", { n: activeResultRunItem.sequence }) : t("tabs.resultRuns") }}</span>
|
||||
<span class="min-w-0 truncate">{{ activeResultRunItem ? activeResultRunItem.title || activeResultRunItem.sourceLabel || t("tabs.runN", { n: activeResultRunItem.sequence }) : t("tabs.resultRuns") }}</span>
|
||||
<ChevronDown class="h-3.5 w-3.5 shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" class="w-48">
|
||||
<CustomContextMenu v-for="run in resultRuns" :key="run.id" :items="() => resultRunContextMenuItems(run)" v-slot="{ onContextMenu }">
|
||||
<DropdownMenuItem class="flex items-center gap-2 pr-1" @select="selectResultRun(run.id)" @contextmenu="onContextMenu">
|
||||
<DropdownMenuItem class="flex items-center gap-2 pr-1" @select="selectResultRunFromTab(run.id)" @contextmenu="onContextMenu">
|
||||
<Check v-if="run.active" class="h-3.5 w-3.5 shrink-0" />
|
||||
<span v-else class="h-3.5 w-3.5 shrink-0" />
|
||||
<Pin v-if="run.pinned" class="h-3 w-3 shrink-0 fill-current text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ run.title || t("tabs.runN", { n: run.sequence }) }}</span>
|
||||
<span class="min-w-0 flex-1 truncate">{{ run.title || run.sourceLabel || t("tabs.runN", { n: run.sequence }) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
|
||||
@@ -7280,6 +7280,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Cədvəl sütunlarının şərhlərini birbaşa sütun adlarının altında göstər.",
|
||||
showColumnTypesInHeader: "Sütun növlərini adların altında göstər",
|
||||
showColumnTypesInHeaderDescription: "Hər sütunun verilən növünü birbaşa cədvəldəki sütun adlarının altında göstər.",
|
||||
showResultSourceDatabase: "Nəticə adlarında verilənlər bazası adını göstər",
|
||||
showResultSourceDatabaseDescription: "Söndürüldükdə nəticə sekmələri yalnız cədvəl adını göstərir; tam ad (verilənlər bazası adı ilə) ipucunda görünməyə davam edir.",
|
||||
dataGridShowTransposeFieldMetadata: "Transpozisiya görünüşündə sahə metaverilənlərini göstər",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Transpozisiya görünüşündə aktiv edilmiş sütun növlərini və şərhləri ayrı sətirlərdə göstər. Bu, sətrin hündürlüyünü artırır.",
|
||||
colorizeDataGridCellTypes: "Verilənlər cədvəlinin dəyərlərini rənglə fərqləndir",
|
||||
|
||||
@@ -7821,6 +7821,8 @@ export default {
|
||||
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
|
||||
showColumnTypesInHeader: "Show column types under names",
|
||||
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
|
||||
showResultSourceDatabase: "Include database name in result set names",
|
||||
showResultSourceDatabaseDescription: "When off, result set tabs show only the table name; the full name stays visible in the tooltip.",
|
||||
dataGridShowTransposeFieldMetadata: "Show field metadata in transpose view",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Display enabled column types and comments on separate lines in transpose view. This increases row height.",
|
||||
colorizeDataGridCellTypes: "Color-code data grid values",
|
||||
|
||||
@@ -7211,6 +7211,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
|
||||
showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
|
||||
showResultSourceDatabase: "Incluir el nombre de la base de datos en los nombres de los resultados",
|
||||
showResultSourceDatabaseDescription: "Si se desactiva, las pestañas de resultados solo muestran el nombre de la tabla; el nombre completo sigue visible en la información emergente.",
|
||||
dataGridShowTransposeFieldMetadata: "Mostrar metadatos de campo en la vista transpuesta",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Muestra los tipos de columna y comentarios habilitados en líneas separadas en la vista transpuesta. Esto aumenta la altura de las filas.",
|
||||
colorizeDataGridCellTypes: "Colorear valores por tipo de dato",
|
||||
|
||||
@@ -7210,6 +7210,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
|
||||
showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showResultSourceDatabase: "Includi il nome del database nei nomi dei risultati",
|
||||
showResultSourceDatabaseDescription: "Se disattivato, le schede dei risultati mostrano solo il nome della tabella; il nome completo resta visibile nel tooltip.",
|
||||
dataGridShowTransposeFieldMetadata: "Mostra i metadati dei campi nella vista trasposta",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Visualizza i tipi di colonna e i commenti abilitati su righe separate nella vista trasposta. Questo aumenta l'altezza delle righe.",
|
||||
colorizeDataGridCellTypes: "Colora i valori in base al tipo di dato",
|
||||
|
||||
@@ -7243,6 +7243,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。",
|
||||
showColumnTypesInHeader: "列名の下にデータ型を表示",
|
||||
showColumnTypesInHeaderDescription: "グリッド列名の直下に各列のデータ型を表示します。",
|
||||
showResultSourceDatabase: "結果セット名にデータベース名を含める",
|
||||
showResultSourceDatabaseDescription: "オフにすると結果セットのタブにはテーブル名のみを表示し、完全な名前(データベース名を含む)はツールチップで確認できます。",
|
||||
dataGridShowTransposeFieldMetadata: "転置ビューにフィールドメタデータを表示",
|
||||
dataGridShowTransposeFieldMetadataDescription: "有効なデータ型とコメントを転置ビューの別行に表示します。有効にすると行の高さが増えます。",
|
||||
colorizeDataGridCellTypes: "データ型に応じて値を色分け",
|
||||
|
||||
@@ -6978,6 +6978,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.",
|
||||
showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시",
|
||||
showColumnTypesInHeaderDescription: "그리드 컬럼 이름 바로 아래에 각 컬럼의 데이터 타입을 표시합니다.",
|
||||
showResultSourceDatabase: "결과 세트 이름에 데이터베이스 이름 포함",
|
||||
showResultSourceDatabaseDescription: "끄면 결과 세트 탭에 테이블 이름만 표시되며, 전체 이름(데이터베이스 이름 포함)은 툴팁에서 확인할 수 있습니다.",
|
||||
dataGridShowTransposeFieldMetadata: "전치 보기에 필드 메타데이터 표시",
|
||||
dataGridShowTransposeFieldMetadataDescription: "활성화된 데이터 타입과 주석을 전치 보기의 별도 줄에 표시합니다. 활성화하면 행 높이가 증가합니다.",
|
||||
colorizeDataGridCellTypes: "데이터 타입별 값 색상 표시",
|
||||
|
||||
@@ -7212,6 +7212,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
|
||||
showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
|
||||
showResultSourceDatabase: "Incluir o nome do banco de dados nos nomes dos resultados",
|
||||
showResultSourceDatabaseDescription: "Quando desativado, as abas de resultados mostram apenas o nome da tabela; o nome completo (com o banco de dados) continua na dica de ferramenta.",
|
||||
dataGridShowTransposeFieldMetadata: "Mostrar metadados de campo na visualização transposta",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Exibir tipos de coluna e comentários habilitados em linhas separadas na visualização transposta. Isso aumenta a altura das linhas.",
|
||||
colorizeDataGridCellTypes: "Colorir valores por tipo de dado",
|
||||
|
||||
@@ -9363,6 +9363,8 @@ export default withEnglishFallback({
|
||||
shortcutLowercaseSelection: "Преобразовать выделенное в нижний регистр",
|
||||
shortcutConvertNamingStyle: "Переключить стиль именования",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: вставить как условие IN",
|
||||
showResultSourceDatabase: "Имя набора результатов содержит имя базы данных",
|
||||
showResultSourceDatabaseDescription: "После отключения вкладки наборов результатов будут отображать только имена таблиц; полное имя (включая имя базы данных) по-прежнему можно посмотреть во всплывающей подсказке.",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Не удалось удалить старый каталог JRE: {path} (исходная ошибка: {error})",
|
||||
|
||||
@@ -7162,6 +7162,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "Tablo sütun açıklamalarını doğrudan ızgara sütun adlarının altında göster.",
|
||||
showColumnTypesInHeader: "Sütun türlerini adların altında göster",
|
||||
showColumnTypesInHeaderDescription: "Her sütunun veri türünü doğrudan ızgara sütun adlarının altında göster.",
|
||||
showResultSourceDatabase: "Sonuç adlarında veritabanı adını göster",
|
||||
showResultSourceDatabaseDescription: "Kapalıyken sonuç sekmeleri yalnızca tablo adını gösterir; tam ad (veritabanı adı dahil) ipucunda görünmeye devam eder.",
|
||||
dataGridShowTransposeFieldMetadata: "Devrik görünümde alan meta verisini göster",
|
||||
dataGridShowTransposeFieldMetadataDescription: "Etkin sütun türlerini ve açıklamalarını devrik görünümde ayrı satırlarda göster. Bu, satır yüksekliğini artırır.",
|
||||
colorizeDataGridCellTypes: "Veri tablosu değerlerini renklendir",
|
||||
|
||||
@@ -7793,6 +7793,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
|
||||
showColumnTypesInHeader: "在字段名下方显示数据类型",
|
||||
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
|
||||
showResultSourceDatabase: "结果集名称包含数据库名",
|
||||
showResultSourceDatabaseDescription: "关闭后结果集页签只显示表名,完整名称(含数据库名)仍可在悬浮提示中查看。",
|
||||
dataGridShowTransposeFieldMetadata: "在转置视图中显示字段元数据",
|
||||
dataGridShowTransposeFieldMetadataDescription: "在转置视图中分行显示已启用的数据类型和注释。开启后会增加行高。",
|
||||
colorizeDataGridCellTypes: "按数据类型为表格值着色",
|
||||
|
||||
@@ -6529,6 +6529,8 @@ export default withEnglishFallback({
|
||||
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
|
||||
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
|
||||
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
|
||||
showResultSourceDatabase: "結果集名稱包含資料庫名稱",
|
||||
showResultSourceDatabaseDescription: "關閉後結果集頁籤只顯示資料表名稱,完整名稱(含資料庫名稱)仍可在懸浮提示中查看。",
|
||||
dataGridShowTransposeFieldMetadata: "在轉置檢視中顯示欄位中繼資料",
|
||||
dataGridShowTransposeFieldMetadataDescription: "在轉置檢視中分行顯示已啟用的資料類型和註解。開啟後會增加列高。",
|
||||
colorizeDataGridCellTypes: "依資料類型為表格值著色",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel, queryResultSourceNameParts } from "@/lib/sql/queryResultSource";
|
||||
|
||||
describe("queryResultNameFromPreamble", () => {
|
||||
it("uses only the immediately preceding line comment", () => {
|
||||
@@ -65,3 +65,15 @@ describe("queryResultSourceLabel", () => {
|
||||
expect(queryResultSourceLabel("SELECT 1", { database: "app", databaseType: "mysql" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryResultSourceNameParts", () => {
|
||||
it("keeps the qualifier separate even when the database name contains dots", () => {
|
||||
expect(queryResultSourceNameParts("SELECT * FROM data_monitor", { database: "cosimulation2.0", databaseType: "mysql" })).toEqual({ qualifier: "cosimulation2.0", name: "data_monitor" });
|
||||
});
|
||||
|
||||
it("prefers the explicit qualifier and drops an empty one", () => {
|
||||
expect(queryResultSourceNameParts("SELECT * FROM analytics.events", { database: "app", databaseType: "mysql" })).toEqual({ qualifier: "analytics", name: "events" });
|
||||
expect(queryResultSourceNameParts("SELECT * FROM events", { databaseType: "mysql" })).toEqual({ name: "events" });
|
||||
expect(queryResultSourceNameParts("SELECT 1", { database: "app", databaseType: "mysql" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+92
-1
@@ -10,6 +10,7 @@ import {
|
||||
resultGridCacheKey,
|
||||
resultGridColumnWidthCacheKey,
|
||||
resultGridInstanceKey,
|
||||
resultRunItems,
|
||||
resultSourceRange,
|
||||
statementExecutionMarkers,
|
||||
tabColorStyle,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
dirtyTabTitleStyle,
|
||||
} from "@/lib/tabs/tabPresentation";
|
||||
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "@/types/database";
|
||||
import type { ConnectionConfig, QueryResult, QueryResultRun, QueryTab } from "@/types/database";
|
||||
|
||||
const translations: Record<string, string> = {
|
||||
"tabs.tooltipConnection": "Connection:",
|
||||
@@ -176,6 +177,51 @@ describe("query result labels", () => {
|
||||
expect(item?.label).toBeUndefined();
|
||||
expect(item?.title).toBe("SELECT 1");
|
||||
});
|
||||
|
||||
it("shows only the object name when result set names exclude the database", () => {
|
||||
const results = [
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: [[1]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
sourceLabel: "cosimulation2.0.data_monitor",
|
||||
sourceQualifier: "cosimulation2.0",
|
||||
sourceName: "data_monitor",
|
||||
sourceStatement: "SELECT * FROM data_monitor",
|
||||
},
|
||||
];
|
||||
|
||||
const [withDatabase] = tabularResultItems(results);
|
||||
const [withoutDatabase] = tabularResultItems(results, { includeSourceDatabase: false });
|
||||
|
||||
expect(withDatabase?.label).toBe("cosimulation2.0.data_monitor");
|
||||
expect(withoutDatabase?.label).toBe("data_monitor");
|
||||
expect(withoutDatabase?.displayLabel).toBe("data_monitor");
|
||||
// 完整名称(含库名)仍保留在悬浮提示中
|
||||
expect(withoutDatabase?.title).toBe("cosimulation2.0.data_monitor");
|
||||
});
|
||||
|
||||
it("keeps a custom result name even when the database is hidden", () => {
|
||||
const [item] = tabularResultItems(
|
||||
[
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: [[1]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
sourceLabel: "My weekly report",
|
||||
sourceQualifier: "app",
|
||||
sourceName: "users",
|
||||
sourceStatement: "SELECT * FROM users",
|
||||
},
|
||||
],
|
||||
{ includeSourceDatabase: false },
|
||||
);
|
||||
|
||||
expect(item?.label).toBe("My weekly report");
|
||||
expect(item?.title).toBe("My weekly report");
|
||||
});
|
||||
});
|
||||
|
||||
describe("query result grid identity", () => {
|
||||
@@ -192,6 +238,51 @@ describe("query result grid identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("result run labels", () => {
|
||||
// 与 store 真实创建批次的方式保持一致:默认标题是 `Run N`,只有重命名/多库执行才标记 customTitle
|
||||
const run = (id: string, sequence: number, result?: QueryResult, overrides: Partial<QueryResultRun> = {}): QueryResultRun => ({ id, title: `Run ${sequence}`, sequence, sql: "SELECT * FROM users", createdAt: sequence, result, ...overrides });
|
||||
const sourceResult = (label: string, qualifier: string, name: string): QueryResult => ({ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, sourceLabel: label, sourceQualifier: qualifier, sourceName: name }) as QueryResult;
|
||||
|
||||
it("names result runs after their source table instead of the run ordinal", () => {
|
||||
const items = resultRunItems(
|
||||
queryTab({
|
||||
resultRuns: [run("run-1", 1, sourceResult("app.users", "app", "users")), run("run-2", 2, sourceResult("app.orders", "app", "orders"))],
|
||||
}) as QueryTab,
|
||||
);
|
||||
|
||||
expect(items.map((item) => item.sourceLabel)).toEqual(["app.users", "app.orders"]);
|
||||
});
|
||||
|
||||
it("suffixes repeated sources and follows the database-name setting", () => {
|
||||
const tab = queryTab({
|
||||
resultRuns: [run("run-1", 1, sourceResult("cosimulation2.0.users", "cosimulation2.0", "users")), run("run-2", 2, sourceResult("cosimulation2.0.users", "cosimulation2.0", "users")), run("run-3", 3)],
|
||||
}) as QueryTab;
|
||||
|
||||
expect(resultRunItems(tab).map((item) => item.sourceLabel)).toEqual(["cosimulation2.0.users", "cosimulation2.0.users (2)", undefined]);
|
||||
expect(resultRunItems(tab, { includeSourceDatabase: false }).map((item) => item.sourceLabel)).toEqual(["users", "users (2)", undefined]);
|
||||
});
|
||||
|
||||
it("keeps a renamed run title ahead of the source label", () => {
|
||||
const items = resultRunItems(queryTab({ resultRuns: [run("run-1", 1, sourceResult("app.users", "app", "users"), { title: "报表", customTitle: true })] }) as QueryTab);
|
||||
|
||||
expect(items[0]?.title).toBe("报表");
|
||||
expect(items[0]?.sourceLabel).toBe("app.users");
|
||||
});
|
||||
|
||||
it("replaces the default Run N title with the source label", () => {
|
||||
const items = resultRunItems(queryTab({ resultRuns: [run("run-1", 1, sourceResult("app.users", "app", "users"))] }) as QueryTab);
|
||||
|
||||
expect(items[0]?.title).toBe("");
|
||||
expect(items[0]?.sourceLabel).toBe("app.users");
|
||||
});
|
||||
|
||||
it("falls back to the run SQL for runs created before source labels were stored", () => {
|
||||
const items = resultRunItems(queryTab({ resultRuns: [run("run-1", 1)] }) as QueryTab, { database: "cosimulation2.0", databaseType: "mysql" });
|
||||
|
||||
expect(items[0]?.sourceLabel).toBe("cosimulation2.0.users");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tab group presentation", () => {
|
||||
// 编号由 store 在标签列表变化时分配,渲染函数只读结果;这里把两步串起来,
|
||||
// 与真实调用顺序保持一致(#9938)。
|
||||
|
||||
@@ -11,9 +11,14 @@ export interface SavedQueryResultRun {
|
||||
sql: string;
|
||||
createdAt: number;
|
||||
pinned?: boolean;
|
||||
/** 标题是否为用户/多库执行显式指定;为假时结果标签改用来源名显示 */
|
||||
customTitle?: boolean;
|
||||
activeResultIndex?: number;
|
||||
resultCacheKey?: string;
|
||||
resultEvicted?: boolean;
|
||||
/** 结果来源(库名.表名 / 表名),用于结果标签命名;与结果 payload 分离,回收 payload 后仍可显示 */
|
||||
sourceLabel?: string;
|
||||
sourceName?: string;
|
||||
}
|
||||
|
||||
export interface SavedOpenTab {
|
||||
@@ -219,6 +224,9 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
||||
sequence: run.sequence,
|
||||
sql: run.sql,
|
||||
createdAt: run.createdAt,
|
||||
...(run.sourceLabel ? { sourceLabel: run.sourceLabel } : {}),
|
||||
...(run.sourceName ? { sourceName: run.sourceName } : {}),
|
||||
...(run.customTitle ? { customTitle: true } : {}),
|
||||
...(run.pinned ? { pinned: true } : {}),
|
||||
activeResultIndex: run.activeResultIndex,
|
||||
...(run.resultCacheKey !== undefined ? { resultCacheKey: run.resultCacheKey } : {}),
|
||||
|
||||
@@ -46,6 +46,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"tabSortMode",
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"showResultSourceDatabase",
|
||||
"dataGridShowTransposeFieldMetadata",
|
||||
"colorizeDataGridCellTypes",
|
||||
"dataGridTypeColorSchemes",
|
||||
|
||||
@@ -221,6 +221,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "query-result-max-rows", category: "data", titleKey: "settings.queryResultMaxRows", descriptionKey: "settings.queryResultMaxRowsDescription", targetId: "data" },
|
||||
{ id: "data-grid-header-comments", category: "data", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "data" },
|
||||
{ id: "data-grid-header-types", category: "data", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "data" },
|
||||
{ id: "result-source-database-name", category: "data", titleKey: "settings.showResultSourceDatabase", descriptionKey: "settings.showResultSourceDatabaseDescription", targetId: "data" },
|
||||
{ id: "data-grid-transpose-field-metadata", category: "data", titleKey: "settings.dataGridShowTransposeFieldMetadata", descriptionKey: "settings.dataGridShowTransposeFieldMetadataDescription", targetId: "data" },
|
||||
{ id: "data-grid-cell-type-colors", category: "data", titleKey: "settings.colorizeDataGridCellTypes", descriptionKey: "settings.colorizeDataGridCellTypesDescription", targetId: "data" },
|
||||
{ id: "data-grid-type-colors", category: "data", titleKey: "settings.dataGridTypeColorScheme", descriptionKey: "settings.dataGridTypeColorSchemeDescription", targetId: "data-grid-type-colors" },
|
||||
|
||||
@@ -140,6 +140,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
||||
data: [
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"showResultSourceDatabase",
|
||||
"dataGridShowTransposeFieldMetadata",
|
||||
"colorizeDataGridCellTypes",
|
||||
"dataGridTypeColorSchemes",
|
||||
@@ -268,6 +269,7 @@ const PASS_THROUGH_BOOLEAN_KEYS = [
|
||||
"confirmUnsavedSqlClose",
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"showResultSourceDatabase",
|
||||
"colorizeDataGridCellTypes",
|
||||
"showIndexIndicatorsInHeader",
|
||||
"compactColumnHeaderActions",
|
||||
|
||||
@@ -29,7 +29,18 @@ function firstSourceOfKind(sources: SqlSemanticRowSource[], kind: SqlSemanticRow
|
||||
return sources.filter((source) => source.kind === kind).sort((left, right) => left.sourceSpan.start - right.sourceSpan.start)[0];
|
||||
}
|
||||
|
||||
export function queryResultSourceLabel(sql: string, options: QueryResultSourceLabelOptions = {}): string | undefined {
|
||||
export interface QueryResultSourceNameParts {
|
||||
/** 库名 / schema 等限定名;SQL 未显式限定时由当前库补齐 */
|
||||
qualifier?: string;
|
||||
/** 表名或写入目标对象名 */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果集来源的限定名与对象名。结果集页签需要按设置决定是否展示限定名,
|
||||
* 而库名本身可能包含点(例如 `cosimulation2.0`),所以不能对拼好的标签做字符串切割。
|
||||
*/
|
||||
export function queryResultSourceNameParts(sql: string, options: QueryResultSourceLabelOptions = {}): QueryResultSourceNameParts | undefined {
|
||||
const statement = sql.trim();
|
||||
if (!statement) return undefined;
|
||||
|
||||
@@ -38,5 +49,11 @@ export function queryResultSourceLabel(sql: string, options: QueryResultSourceLa
|
||||
if (!source?.name) return undefined;
|
||||
|
||||
const qualifier = source.qualifierParts[source.qualifierParts.length - 1]?.trim() || options.database?.trim();
|
||||
return qualifier ? `${qualifier}.${source.name}` : source.name;
|
||||
return { qualifier: qualifier || undefined, name: source.name };
|
||||
}
|
||||
|
||||
export function queryResultSourceLabel(sql: string, options: QueryResultSourceLabelOptions = {}): string | undefined {
|
||||
const parts = queryResultSourceNameParts(sql, options);
|
||||
if (!parts) return undefined;
|
||||
return parts.qualifier ? `${parts.qualifier}.${parts.name}` : parts.name;
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import type { SqlErrorPosition } from "@/lib/backend/errorUtils";
|
||||
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
|
||||
import { queryResultSourceNameParts } from "@/lib/sql/queryResultSource";
|
||||
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, QueryResultRun, QueryTab } from "@/types/database";
|
||||
|
||||
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
||||
export type OutputView = "result" | "summary" | "explain" | "chart";
|
||||
@@ -440,20 +441,26 @@ export function queryResultExecutionSql(tab: Pick<QueryTab, "result" | "resultBa
|
||||
return tab.resultSortedSql || resultSqlForGrid(tab);
|
||||
}
|
||||
|
||||
export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string; displayLabel?: string; labelTruncated: boolean; title?: string }[] {
|
||||
export function tabularResultItems(results: QueryResult[] | undefined, options: { includeSourceDatabase?: boolean } = {}): { result: QueryResult; index: number; n: number; label?: string; displayLabel?: string; labelTruncated: boolean; title?: string }[] {
|
||||
if (!results) return [];
|
||||
return results
|
||||
.map((result, index) => ({ result, index }))
|
||||
.filter((item) => item.result.columns.length > 0 && item.result.server_message !== true)
|
||||
.map((item, ordinal) => {
|
||||
const label = queryResultStatementLabel(item.result);
|
||||
const displayLabel = label ? middleEllipsis(label) : undefined;
|
||||
const { sourceName, sourceQualifier } = item.result;
|
||||
// 只有在标签确实由“库名.对象名”拼成时才启用短名称,避免覆盖 `-- name: xxx` 之类的自定义名称
|
||||
const qualifiedLabel = sourceName ? (sourceQualifier ? `${sourceQualifier}.${sourceName}` : sourceName) : undefined;
|
||||
// 关闭“结果集名称包含数据库名”时,页签与结果列表只展示对象名;
|
||||
// 完整名称(含库名)仍保留在 title,供悬浮提示与搜索使用。
|
||||
const displaySource = options.includeSourceDatabase === false && sourceName && qualifiedLabel === label ? sourceName : label;
|
||||
const displayLabel = displaySource ? middleEllipsis(displaySource) : undefined;
|
||||
return {
|
||||
...item,
|
||||
n: ordinal + 1,
|
||||
label,
|
||||
label: displaySource,
|
||||
displayLabel,
|
||||
labelTruncated: !!label && displayLabel !== label,
|
||||
labelTruncated: !!displaySource && displayLabel !== displaySource,
|
||||
title: item.result.sourceLabel || item.result.sourceStatement,
|
||||
};
|
||||
});
|
||||
@@ -463,14 +470,52 @@ export function activeResultRun(tab: Pick<QueryTab, "resultRuns" | "activeResult
|
||||
return tab.resultRuns?.find((run) => run.id === tab.activeResultRunId);
|
||||
}
|
||||
|
||||
export function resultRunItems(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">): { id: string; title: string; sequence: number; active: boolean; pinned: boolean }[] {
|
||||
return (tab.resultRuns ?? []).map((run) => ({
|
||||
id: run.id,
|
||||
title: run.title,
|
||||
sequence: run.sequence,
|
||||
active: run.id === tab.activeResultRunId,
|
||||
pinned: run.pinned === true,
|
||||
}));
|
||||
/**
|
||||
* 执行批次默认显示名:取该批次主结果的来源(库名.表名 / 表名)。
|
||||
* 连表查询同样取 FROM 之后的第一个物理表,与结果集页签的来源解析保持一致。
|
||||
*/
|
||||
function resultRunSourceLabel(run: QueryResultRun, includeSourceDatabase: boolean, fallback: { database?: string; databaseType?: DatabaseType }): string | undefined {
|
||||
// 优先用批次自带的来源:非活动批次的结果 payload 会被回收,payload 里的来源会丢失
|
||||
const own = includeSourceDatabase ? run.sourceLabel || run.sourceName : run.sourceName || run.sourceLabel;
|
||||
if (own) return own;
|
||||
const candidates = run.result ? [run.result, ...(run.results ?? [])] : (run.results ?? []);
|
||||
for (const result of candidates) {
|
||||
if (!result) continue;
|
||||
// 批次级来源缺失时回退到结果本身;关闭“结果集名称包含数据库名”时优先用对象名
|
||||
const label = includeSourceDatabase ? result.sourceLabel || result.sourceName : result.sourceName || result.sourceLabel;
|
||||
if (label) return label;
|
||||
}
|
||||
// 历史批次(功能上线前创建的)没有来源信息:用批次 SQL 重新解析,连表查询同样取第一个物理表
|
||||
if (run.sql && fallback.databaseType) {
|
||||
const parts = queryResultSourceNameParts(run.sql, { database: fallback.database, databaseType: fallback.databaseType });
|
||||
if (parts) return includeSourceDatabase ? (parts.qualifier ? `${parts.qualifier}.${parts.name}` : parts.name) : parts.name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resultRunItems(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">, options: { includeSourceDatabase?: boolean; database?: string; databaseType?: DatabaseType } = {}): { id: string; title: string; sequence: number; active: boolean; pinned: boolean; sourceLabel?: string }[] {
|
||||
const includeSourceDatabase = options.includeSourceDatabase !== false;
|
||||
const fallback = { database: options.database, databaseType: options.databaseType };
|
||||
const seenBySource = new Map<string, number>();
|
||||
return (tab.resultRuns ?? []).map((run) => {
|
||||
const source = resultRunSourceLabel(run, includeSourceDatabase, fallback);
|
||||
let sourceLabel = source;
|
||||
if (source) {
|
||||
// 同一张表被查询多次时用序号后缀区分页签(users、users (2)…)
|
||||
const seen = (seenBySource.get(source) ?? 0) + 1;
|
||||
seenBySource.set(source, seen);
|
||||
if (seen > 1) sourceLabel = `${source} (${seen})`;
|
||||
}
|
||||
return {
|
||||
id: run.id,
|
||||
// 系统默认标题(Run N)由来源名取代;用户重命名/多库按目标命名过的标题优先
|
||||
title: run.customTitle ? run.title : "",
|
||||
sequence: run.sequence,
|
||||
active: run.id === tab.activeResultRunId,
|
||||
pinned: run.pinned === true,
|
||||
sourceLabel,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function resultGridCacheKey(tab: Pick<QueryTab, "id" | "activeResultRunId" | "activeResultIndex">): string {
|
||||
|
||||
@@ -74,6 +74,8 @@ interface ColumnarQueryResult {
|
||||
truncated?: boolean;
|
||||
has_more?: boolean;
|
||||
sourceLabel?: string;
|
||||
sourceQualifier?: string;
|
||||
sourceName?: string;
|
||||
sourceStatement?: string;
|
||||
sourceFrom?: number;
|
||||
sourceTo?: number;
|
||||
@@ -363,6 +365,8 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
|
||||
session_id: undefined,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceQualifier: result.sourceQualifier,
|
||||
sourceName: result.sourceName,
|
||||
sourceStatement: result.sourceStatement,
|
||||
sourceFrom: result.sourceFrom,
|
||||
sourceTo: result.sourceTo,
|
||||
@@ -419,6 +423,8 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
||||
truncated: result.truncated,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceQualifier: result.sourceQualifier,
|
||||
sourceName: result.sourceName,
|
||||
sourceStatement: result.sourceStatement,
|
||||
sourceFrom: result.sourceFrom,
|
||||
sourceTo: result.sourceTo,
|
||||
@@ -449,6 +455,8 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
||||
session_id: undefined,
|
||||
has_more: result.has_more,
|
||||
sourceLabel: result.sourceLabel,
|
||||
sourceQualifier: result.sourceQualifier,
|
||||
sourceName: result.sourceName,
|
||||
sourceStatement: result.sourceStatement,
|
||||
sourceFrom: result.sourceFrom,
|
||||
sourceTo: result.sourceTo,
|
||||
|
||||
@@ -75,7 +75,7 @@ import { formatDdlForDisplay } from "@/lib/sql/ddlDisplay";
|
||||
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint, jdbcConnectionUsesDriverRowOffset, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel, queryResultSourceNameParts } from "@/lib/sql/queryResultSource";
|
||||
import { sqlServerCountUsesLocalTempTable } from "@/lib/query/queryResultCountSession";
|
||||
import { stripPaginationRowNumber } from "@/lib/query/queryPaginationResult";
|
||||
import { beginDataGridNativeSelectionBlock, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection";
|
||||
@@ -380,6 +380,8 @@ function releaseResultObjectPayload(result: QueryResult): void {
|
||||
result.messages = undefined;
|
||||
result.error = undefined;
|
||||
result.sourceLabel = undefined;
|
||||
result.sourceQualifier = undefined;
|
||||
result.sourceName = undefined;
|
||||
result.sourceStatement = undefined;
|
||||
}
|
||||
|
||||
@@ -435,7 +437,12 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas
|
||||
const documentStatement = sourceOffset === undefined ? undefined : findSourceDocumentStatement(documentStatements, sourceOffset + statement.from);
|
||||
const preamble = documentStatement ? sourceDocumentSql!.slice(documentStatement.hitFrom, documentStatement.from) : sql.slice(statement.hitFrom, statement.from);
|
||||
const customName = queryResultNameFromPreamble(preamble, { databaseType });
|
||||
if (customName) result.sourceLabel = customName;
|
||||
if (customName) {
|
||||
result.sourceLabel = customName;
|
||||
// 自定义名称(-- name: xxx)优先:清除结构化来源,避免“结果集名称包含数据库名”设置把它替换成表名
|
||||
result.sourceQualifier = undefined;
|
||||
result.sourceName = undefined;
|
||||
}
|
||||
const successfulUseDatabase = result.execution_error !== true ? useDatabaseFromStatement(statement.sql, databaseType) : undefined;
|
||||
if (successfulUseDatabase) {
|
||||
sourceDatabase = successfulUseDatabase;
|
||||
@@ -650,8 +657,14 @@ function annotateQueryResultSource(result: QueryResult, sourceStatement: string,
|
||||
result.sourceFrom = sourceRange.from;
|
||||
result.sourceTo = sourceRange.to;
|
||||
}
|
||||
const label = databaseType ? queryResultSourceLabel(sourceStatement, { database, databaseType }) : undefined;
|
||||
if (label) result.sourceLabel = label;
|
||||
const parts = databaseType ? queryResultSourceNameParts(sourceStatement, { database, databaseType }) : undefined;
|
||||
if (parts) {
|
||||
// 同时保留结构化来源信息:结果集页签可以按设置只展示对象名,避免退化成字符串切割
|
||||
result.sourceQualifier = parts.qualifier;
|
||||
result.sourceName = parts.name;
|
||||
const label = parts.qualifier ? `${parts.qualifier}.${parts.name}` : parts.name;
|
||||
if (label) result.sourceLabel = label;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1917,6 +1930,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const run = tab?.resultRuns?.find((item) => item.id === runId);
|
||||
if (!tab || !run) return false;
|
||||
run.title = trimmed;
|
||||
run.customTitle = true;
|
||||
void persistResultRun(tab, run);
|
||||
return true;
|
||||
}
|
||||
@@ -2128,12 +2142,17 @@ export const useQueryStore = defineStore("query", () => {
|
||||
function captureDisplayedResultRun(tab: QueryTab, sql: string, createdAt = Date.now(), options: ResultRunCaptureOptions = {}) {
|
||||
if (tab.mode !== "query" || !tab.result) return;
|
||||
const sequence = nextResultRunSequence(tab);
|
||||
// 批次级来源:非活动批次的 payload 会被回收,结果标签命名需要独立保存来源
|
||||
const primaryResult = tab.results?.[0] ?? tab.result;
|
||||
const run: NonNullable<QueryTab["resultRuns"]>[number] = {
|
||||
id: uuid(),
|
||||
title: options.title ?? `Run ${sequence}`,
|
||||
customTitle: !!options.title,
|
||||
sequence,
|
||||
sql,
|
||||
createdAt,
|
||||
sourceLabel: primaryResult?.sourceLabel,
|
||||
sourceName: primaryResult?.sourceName,
|
||||
result: tab.result,
|
||||
results: tab.results,
|
||||
activeResultIndex: tab.activeResultIndex,
|
||||
@@ -2206,6 +2225,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
...workerRun,
|
||||
id: runId,
|
||||
title,
|
||||
customTitle: true,
|
||||
sequence: nextResultRunSequence(source),
|
||||
multiDbExecution: execution,
|
||||
resultCacheKey: undefined,
|
||||
@@ -2562,6 +2582,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
sql: run.sql,
|
||||
createdAt: run.createdAt,
|
||||
pinned: run.pinned,
|
||||
customTitle: run.customTitle,
|
||||
sourceLabel: run.sourceLabel,
|
||||
sourceName: run.sourceName,
|
||||
activeResultIndex: run.activeResultIndex,
|
||||
resultCacheKey: run.resultCacheKey,
|
||||
resultEvicted: run.resultEvicted,
|
||||
@@ -6972,6 +6995,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const annotateMongoResult = (result: QueryResult): QueryResult => {
|
||||
const annotated = annotateQueryResultSource(result, sourceStatement, undefined, undefined, sourceRange);
|
||||
if ("collection" in mongoCommand) {
|
||||
annotated.sourceQualifier = currentDatabase || undefined;
|
||||
annotated.sourceName = mongoCommand.collection;
|
||||
annotated.sourceLabel = currentDatabase ? `${currentDatabase}.${mongoCommand.collection}` : mongoCommand.collection;
|
||||
}
|
||||
return annotated;
|
||||
|
||||
@@ -860,6 +860,8 @@ export interface EditorSettings {
|
||||
mongoViewMode: "document" | "table";
|
||||
showColumnCommentsInHeader: boolean;
|
||||
showColumnTypesInHeader: boolean;
|
||||
/** 结果集页签/结果列表的名称是否带上库名(关闭后只显示表名,完整名称仍在悬浮提示中)。 */
|
||||
showResultSourceDatabase: boolean;
|
||||
dataGridShowTransposeFieldMetadata: boolean;
|
||||
colorizeDataGridCellTypes: boolean;
|
||||
dataGridTypeColorSchemes: DataGridTypeColorScheme[];
|
||||
@@ -1137,6 +1139,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
mongoViewMode: "document",
|
||||
showColumnCommentsInHeader: true,
|
||||
showColumnTypesInHeader: true,
|
||||
showResultSourceDatabase: true,
|
||||
dataGridShowTransposeFieldMetadata: false,
|
||||
colorizeDataGridCellTypes: false,
|
||||
dataGridTypeColorSchemes: [],
|
||||
@@ -1691,6 +1694,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
|
||||
showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
|
||||
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
|
||||
showResultSourceDatabase: settings.showResultSourceDatabase ?? DEFAULT_EDITOR_SETTINGS.showResultSourceDatabase,
|
||||
dataGridShowTransposeFieldMetadata: settings.dataGridShowTransposeFieldMetadata === true,
|
||||
colorizeDataGridCellTypes: settings.colorizeDataGridCellTypes ?? DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes,
|
||||
dataGridTypeColorSchemes,
|
||||
@@ -2511,6 +2515,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
|
||||
if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;
|
||||
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
|
||||
if (partial.showResultSourceDatabase !== undefined) editorSettings.value.showResultSourceDatabase = partial.showResultSourceDatabase;
|
||||
if (partial.dataGridShowTransposeFieldMetadata !== undefined) editorSettings.value.dataGridShowTransposeFieldMetadata = partial.dataGridShowTransposeFieldMetadata === true;
|
||||
if (partial.colorizeDataGridCellTypes !== undefined) editorSettings.value.colorizeDataGridCellTypes = partial.colorizeDataGridCellTypes === true;
|
||||
if (partial.dataGridTypeColorSchemes !== undefined) {
|
||||
|
||||
@@ -1281,6 +1281,10 @@ export interface QueryResult {
|
||||
* the tabular view and the original JSON. */
|
||||
elasticsearch_raw_body?: string;
|
||||
sourceLabel?: string;
|
||||
/** 结果集来源的库名 / schema(与 sourceLabel 同时写入),供结果集页签按设置决定是否展示。 */
|
||||
sourceQualifier?: string;
|
||||
/** 结果集来源的对象名(通常为表名),关闭“结果集名称包含数据库名”时用于展示短名称。 */
|
||||
sourceName?: string;
|
||||
sourceStatement?: string;
|
||||
/** Absolute offsets in the editor document at execution time. */
|
||||
sourceFrom?: number;
|
||||
@@ -1346,8 +1350,19 @@ export interface QueryResultRun {
|
||||
createdAt: number;
|
||||
/** Keeps this result from being replaced by an ordinary query execution. */
|
||||
pinned?: boolean;
|
||||
/**
|
||||
* 标题是否由用户/多库执行显式指定(重命名或按目标库命名)。
|
||||
* 为假时 title 只是系统默认的 `Run N`,结果标签应改用来源名显示。
|
||||
*/
|
||||
customTitle?: boolean;
|
||||
/** Distinguishes successive result payloads that reuse the same run slot. */
|
||||
resultGridRevision?: string;
|
||||
/**
|
||||
* 结果来源(库名.表名 / 表名),随批次一起保存。
|
||||
* 非活动批次的结果 payload 会被回收,因此结果标签命名不能依赖 payload。
|
||||
*/
|
||||
sourceLabel?: string;
|
||||
sourceName?: string;
|
||||
/**
|
||||
* Logical-result identity for the tab-switch view snapshot cache. Distinct
|
||||
* from `resultGridRevision` (the grid remount key): this one changes on every
|
||||
|
||||
Reference in new issue
Block a user