feat(editor): resolve table hover across schemas in current database

This commit is contained in:
Mo-Moore
2026-09-17 14:32:45 +08:00
committed by GitHub
parent 90ad4e62e6
commit 254b9baaff
21 changed files with 675 additions and 29 deletions
@@ -103,6 +103,7 @@ import {
type ClickTableNavigationTarget,
type EditorSettings,
type SqlCompletionTriggerMode,
type TableHoverLookupMode,
SIDEBAR_INDENT_MIN,
SIDEBAR_INDENT_MAX,
SIDEBAR_FONT_SIZE_MIN,
@@ -657,6 +658,15 @@ const editGenerateSqlIncludeDatabaseName = ref(settingsStore.editorSettings.gene
const editGenerateSqlQuoteIdentifiers = ref(settingsStore.editorSettings.generateSqlQuoteIdentifiers);
const editFormatSqlOnSqlFileSave = ref(settingsStore.editorSettings.formatSqlOnSqlFileSave);
const editShowTableDdlHoverPreview = ref(settingsStore.editorSettings.showTableDdlHoverPreview);
const editTableHoverLookupMode = ref<TableHoverLookupMode>(settingsStore.editorSettings.tableHoverLookupMode);
const tableHoverLookupModeDescription = computed(() => {
const key = {
current: "settings.tableHoverLookupModeCurrentDescription",
fallback: "settings.tableHoverLookupModeFallbackDescription",
always: "settings.tableHoverLookupModeAlwaysDescription",
}[editTableHoverLookupMode.value];
return t(key);
});
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
const editAutoDownloadUpdates = ref(settingsStore.editorSettings.autoDownloadUpdates);
@@ -816,6 +826,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
generateSqlQuoteIdentifiers: editGenerateSqlQuoteIdentifiers.value,
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave.value,
showTableDdlHoverPreview: editShowTableDdlHoverPreview.value,
tableHoverLookupMode: editTableHoverLookupMode.value,
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
autoDownloadUpdates: editAutoDownloadUpdates.value,
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
@@ -1341,6 +1352,7 @@ function syncEditorSettingsDraftFromStore() {
editGenerateSqlQuoteIdentifiers.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers;
editFormatSqlOnSqlFileSave.value = settingsStore.editorSettings.formatSqlOnSqlFileSave;
editShowTableDdlHoverPreview.value = settingsStore.editorSettings.showTableDdlHoverPreview;
editTableHoverLookupMode.value = settingsStore.editorSettings.tableHoverLookupMode;
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
editAutoDownloadUpdates.value = settingsStore.editorSettings.autoDownloadUpdates;
@@ -1458,6 +1470,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
generateSqlQuoteIdentifiers: editGenerateSqlQuoteIdentifiers,
formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave,
showTableDdlHoverPreview: editShowTableDdlHoverPreview,
tableHoverLookupMode: editTableHoverLookupMode,
updateNotificationsEnabled: editUpdateNotificationsEnabled,
autoDownloadUpdates: editAutoDownloadUpdates,
sidebarObjectInfoMode: editSidebarObjectInfoMode,
@@ -1842,6 +1855,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editAppCloseUnsavedTabsMode.value = DEFAULT_EDITOR_SETTINGS.appCloseUnsavedTabsMode;
editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode;
editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview;
editTableHoverLookupMode.value = DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode;
editExternalSqlEditorMaxMb.value = DEFAULT_EDITOR_SETTINGS.externalSqlEditorMaxMb;
editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
editSqlVariableSubstitutionEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled;
@@ -2034,6 +2048,7 @@ function resetAllDefaults() {
editGenerateSqlQuoteIdentifiers.value = DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers;
editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave;
editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview;
editTableHoverLookupMode.value = DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editAutoDownloadUpdates.value = DEFAULT_EDITOR_SETTINGS.autoDownloadUpdates;
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
@@ -2197,6 +2212,12 @@ function onCompletionTriggerModeChange(v: any) {
}
}
function onTableHoverLookupModeChange(v: any) {
if (v === "current" || v === "fallback" || v === "always") {
editTableHoverLookupMode.value = v;
}
}
function onSqlSemanticDiagnosticsEnabledChange(value: boolean) {
editSqlSemanticDiagnosticsEnabled.value = value;
editSqlSemanticDiagnosticsMode.value = value ? "enabled" : "disabled";
@@ -5605,6 +5626,25 @@ onUnmounted(() => {
</Select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2" data-editor-table-hover-lookup-mode>
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.tableHoverLookupMode") }}</Label>
<p class="text-xs text-muted-foreground">
{{ tableHoverLookupModeDescription }}
</p>
</div>
<Select :model-value="editTableHoverLookupMode" @update:model-value="onTableHoverLookupModeChange">
<SelectTrigger class="h-8 w-48 shrink-0">
<SelectValue :placeholder="t('settings.tableHoverLookupMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="current">{{ t("settings.tableHoverLookupModeCurrent") }}</SelectItem>
<SelectItem value="fallback">{{ t("settings.tableHoverLookupModeFallback") }}</SelectItem>
<SelectItem value="always">{{ t("settings.tableHoverLookupModeAlways") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2" data-editor-default-transaction-mode>
<div class="min-w-0 space-y-1">
<Label for="editor-default-transaction-mode">{{ t("settings.defaultTransactionMode") }}</Label>
@@ -128,6 +128,7 @@ import {
type SqlObjectNavigationTarget,
} from "@/lib/sql/sqlNavigation";
import { buildHoverTableSql, ddlForHoverPreview, hoverTableMatchesScope, normalizeAlignedSqlWhitespace, quoteIdentifier, quoteQualifiedName, reformatHoverDdl, scopeHoverTables, type HoverTableScope } from "@/lib/editor/hoverTableSql";
import { matchHoverTableCandidates, resolveHoverTableLookupTarget } from "@/lib/editor/hoverTableLookup";
import { constrainSqlHoverLayout } from "@/lib/editor/sqlHoverLayout";
import { createHoverSearch, type HoverSearchController } from "@/lib/editor/sqlHoverSearch";
import { lineColumnToOffset, sqlErrorDecorationRange as resolveSqlErrorDecorationRange, sqlErrorSqlMatchesEditor } from "@/lib/sql/sqlDiagnostics";
@@ -3176,11 +3177,24 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
const tableLookupName = semanticTarget && !semanticQualifierIsRowSource ? semanticTarget.name : name;
const qualifiedTableLookup = semanticTarget?.schema ? `${semanticTarget.schema}.${semanticTarget.name}` : identifier;
const hoverTarget = completionMetadataTarget({
name: tableLookupName,
const lookup = resolveHoverTableLookupTarget({
database: props.database,
schema: props.schema,
catalog: props.catalog,
database: semanticTarget?.database,
schema: semanticTarget?.schema,
databaseType: props.databaseType,
tableName: tableLookupName,
// Alias.column must not be treated as schema.table; keep the bare column/table token.
identifierParts: semanticQualifierIsRowSource ? [tableLookupName] : parts,
semanticDatabase: semanticQualifierIsRowSource ? undefined : semanticTarget?.database,
semanticSchema: semanticQualifierIsRowSource ? undefined : semanticTarget?.schema,
mode: settingsStore.editorSettings.tableHoverLookupMode,
});
const hoverTarget = completionMetadataTarget({
name: lookup.tableName,
catalog: props.catalog,
database: lookup.database !== props.database ? lookup.database : undefined,
schema: lookup.schema,
});
if (!hoverTarget) return null;
const hoverScope: HoverTableScope = {
@@ -3188,27 +3202,73 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
database: hoverTarget.database,
schema: hoverTarget.schema,
};
const preferredSchema = props.schema;
const matchHoverTable = (tables: typeof cachedTables) =>
matchHoverTableCandidates(tables, {
lookups: [qualifiedTableLookup, identifier],
tableName: tableLookupName,
preferredSchema,
});
try {
let hoverTables = cachedTables.filter((table) => hoverTableMatchesScope(table, hoverScope));
let table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
if (!table) {
const localTables = connectionStore.lookupLocalCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, hoverScope.catalog);
const localHoverTables = scopeHoverTables(localTables, hoverScope);
const searchLocal = (schema?: string) => {
const scopeForFilter: HoverTableScope = { ...hoverScope, schema };
const localTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, schema, hoverScope.catalog);
const localHoverTables = schema ? scopeHoverTables(localTables, scopeForFilter) : scopeHoverTables(localTables, { ...hoverScope, schema: undefined });
return localHoverTables;
};
const searchRemote = async (schema: string | undefined, globalSearch: boolean) => {
const loadedTables = await connectionStore.listCompletionTables(props.connectionId!, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, schema, globalSearch, preferredSchema, hoverScope.catalog);
return schema ? scopeHoverTables(loadedTables, { ...hoverScope, schema }) : loadedTables;
};
let hoverTables = cachedTables.filter((table) => (lookup.preferGlobalFirst || !hoverScope.schema ? hoverTableMatchesScope(table, { ...hoverScope, schema: undefined }) : hoverTableMatchesScope(table, hoverScope)));
let table = matchHoverTable(hoverTables);
if (!table && !lookup.preferGlobalFirst) {
const localHoverTables = searchLocal(hoverScope.schema);
hoverTables = mergeCompletionTables(localHoverTables, hoverTables);
cachedTables = mergeCompletionTables(localHoverTables, cachedTables);
table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
table = matchHoverTable(hoverTables);
}
if (!table && !usesLocalOnlyCompletionMetadata()) {
const loadedTables = await connectionStore.listCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, false, hoverScope.schema, hoverScope.catalog);
const remoteHoverTables = scopeHoverTables(loadedTables, hoverScope);
if (!table && lookup.preferGlobalFirst) {
const localHoverTables = searchLocal(undefined);
hoverTables = mergeCompletionTables(localHoverTables, hoverTables);
cachedTables = mergeCompletionTables(localHoverTables, cachedTables);
table = matchHoverTable(hoverTables);
}
if (!table && !usesLocalOnlyCompletionMetadata() && !lookup.preferGlobalFirst) {
const remoteHoverTables = await searchRemote(hoverScope.schema, false);
hoverTables = mergeCompletionTables(hoverTables, remoteHoverTables);
cachedTables = mergeCompletionTables(cachedTables, remoteHoverTables);
table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
table = matchHoverTable(hoverTables);
}
if (!table && !usesLocalOnlyCompletionMetadata() && lookup.preferGlobalFirst) {
const remoteHoverTables = await searchRemote(undefined, true);
hoverTables = mergeCompletionTables(hoverTables, remoteHoverTables);
cachedTables = mergeCompletionTables(cachedTables, remoteHoverTables);
table = matchHoverTable(hoverTables);
}
if (!table && lookup.allowGlobalFallback && !lookup.preferGlobalFirst) {
const localHoverTables = searchLocal(undefined);
hoverTables = mergeCompletionTables(localHoverTables, hoverTables);
cachedTables = mergeCompletionTables(localHoverTables, cachedTables);
table = matchHoverTable(hoverTables);
if (!table && !usesLocalOnlyCompletionMetadata()) {
const remoteHoverTables = await searchRemote(undefined, true);
hoverTables = mergeCompletionTables(hoverTables, remoteHoverTables);
cachedTables = mergeCompletionTables(cachedTables, remoteHoverTables);
table = matchHoverTable(hoverTables);
}
}
if (table && settingsStore.editorSettings.showTableDdlHoverPreview && !semanticQualifierIsRowSource && (!qualifier || table.schema?.toLowerCase() === qualifier.toLowerCase() || table.name === name)) {
const hoverDatabase = hoverScope.database;
const hoverSchema = hoverScope.schema ?? table.schema ?? "";
const hoverDatabase = table.database ?? hoverScope.database;
const hoverSchema = table.schema ?? hoverScope.schema ?? "";
const hoverQualifiedName = [hoverScope.catalog, hoverDatabase, hoverSchema, table.name].filter(Boolean).join(".");
const objectMetadataRequest = {
connectionId: props.connectionId,
@@ -6851,6 +6911,22 @@ onMounted(async () => {
const objectSchemaHint = identity.parts.length >= 3 ? identity.schema : identity.parts.length === 1 ? props.schema : undefined;
const isRoutineCall = identity.role === "routine_call";
const isRelationColumnList = identity.role === "relation_column_list";
const tableLookup = resolveHoverTableLookupTarget({
database: props.database!,
schema: props.schema,
catalog: props.catalog,
databaseType: props.databaseType,
tableName: tableLookupFilter,
identifierParts,
mode: settingsStore.editorSettings.tableHoverLookupMode,
});
const tableLookupSchema = tableLookup.preferGlobalFirst ? undefined : (tableLookup.schema ?? props.schema);
const matchNavigationTable = (tables: typeof cachedTables) =>
matchHoverTableCandidates(tables, {
lookups: [identifier],
tableName: tableLookupFilter,
preferredSchema: props.schema,
});
const relationNavigationTarget = (target: SqlObjectNavigationTarget) =>
queryTableNavigationTargetAtSqlPosition(
{
@@ -6864,17 +6940,26 @@ onMounted(async () => {
target,
);
// 1. Local table cache (sync). Relation column lists always prefer tables over routines.
if (cachedTables.length === 0) {
cachedTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, tableLookupFilter, MAX_COMPLETION_TABLES, props.schema, props.catalog);
}
let matchedTable = matchTable(identifier, cachedTables);
// 1. Local table lookup with the resolved scope (do not trust stale editor cache alone —
// completion/hover may have filled cachedTables with the current schema only).
const localScopedTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema, props.catalog);
cachedTables = mergeCompletionTables(localScopedTables, cachedTables);
let matchedTable = matchNavigationTable(localScopedTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
if (!matchedTable && tableLookup.allowGlobalFallback && !tableLookup.preferGlobalFirst) {
const localGlobalTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, MAX_COMPLETION_TABLES, undefined, props.catalog);
cachedTables = mergeCompletionTables(cachedTables, localGlobalTables);
matchedTable = matchNavigationTable(localGlobalTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
}
const preserveOracleStoreCase = (value?: string) => !!value && value !== value.toUpperCase();
const openMatchedObject = (matchedObject: { name: string; schema?: string; type: string; signature?: string; parentName?: string; parentSchema?: string }) => {
const navigationType = sqlObjectNavigationTypeFromCompletionObjectType(matchedObject.type);
@@ -6929,21 +7014,39 @@ onMounted(async () => {
// Routine-call sites may still hit this when a table and procedure share a name and
// local caches were empty; table wins only if listed as a relation.
if (!usesLocalOnlyCompletionMetadata() && (!isRoutineCall || isRelationColumnList || identity.role === "unknown")) {
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, props.database!, tableLookupFilter, MAX_COMPLETION_TABLES, props.schema, false, props.schema, props.catalog);
matchedTable = matchTable(identifier, cachedTables);
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema, tableLookup.preferGlobalFirst, props.schema, props.catalog);
matchedTable = matchNavigationTable(cachedTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
if (tableLookup.allowGlobalFallback && !tableLookup.preferGlobalFirst) {
const globalTables = await connectionStore.listCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, MAX_COMPLETION_TABLES, undefined, true, props.schema, props.catalog);
cachedTables = mergeCompletionTables(cachedTables, globalTables);
matchedTable = matchNavigationTable(cachedTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
}
} else if (!usesLocalOnlyCompletionMetadata() && isRoutineCall) {
// Lightweight table check so INSERT INTO ORDERS(…) is not the only guarded path —
// still avoid global scans: only session-scoped prefix lookup.
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, props.database!, tableLookupFilter, 20, props.schema, false, props.schema, props.catalog);
matchedTable = matchTable(identifier, cachedTables);
// still avoid unbounded scans: session-scoped first, optional name-filtered global fallback.
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, 20, tableLookupSchema, tableLookup.preferGlobalFirst, props.schema, props.catalog);
matchedTable = matchNavigationTable(cachedTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
if (tableLookup.allowGlobalFallback && !tableLookup.preferGlobalFirst) {
const globalTables = await connectionStore.listCompletionTables(props.connectionId!, tableLookup.database, tableLookupFilter, 20, undefined, true, props.schema, props.catalog);
cachedTables = mergeCompletionTables(cachedTables, globalTables);
matchedTable = matchNavigationTable(cachedTables);
if (matchedTable) {
emit("clickTable", relationNavigationTarget(matchedTable));
return;
}
}
}
// 1d. Optimistic routine open only for confirmed call sites after metadata + table checks.
+8
View File
@@ -7303,6 +7303,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "Aktiv olduqda cədvəl adına Ctrl/Cmd+klik cədvəl strukturu redaktorunu (DDL) açır. Deaktiv olduqda cədvəl verilənləri görünüşünü açır.",
showTableDdlHoverPreview: "Kursor üzərinə gətirildikdə cədvəlin DDL-ini göstər",
showTableDdlHoverPreviewDescription: "SQL redaktorunda kursor cədvəl adının üzərinə gətirildikdə DDL önbaxışını göstər.",
tableHoverLookupMode: "Cədvəl üzərinə gəlmə axtarış əhatəsi",
tableHoverLookupModeDescription: "SQL redaktorunda ixtisassız cədvəl adlarına gəldikdə və ya Ctrl/Cmd+klik edəndə sxemin necə həll olunması.",
tableHoverLookupModeCurrent: "Yalnız cari sxem",
tableHoverLookupModeFallback: "Əvvəl cari sxem, sonra hamısı",
tableHoverLookupModeAlways: "Əvvəl bütün sxemlər",
tableHoverLookupModeCurrentDescription: "Cədvəlləri yalnız alət panelində seçilmiş sxemdə axtarır.",
tableHoverLookupModeFallbackDescription: "Əvvəl seçilmiş sxemi yoxlayır; tapılmasa cədvəl adına görə digər sxemləri axtarır.",
tableHoverLookupModeAlwaysDescription: "Cədvəl adına görə bütün sxemləri əvvəl axtarır. Çarpaz sxem proseduru yazarkən faydalıdır.",
sqlVariableSyntax: "SQL dəyişənlərinin və yer tutucularının əvəzlənməsi",
sqlVariableSyntaxDescription: "DBX-in SQL-i işlətməzdən əvvəl hansı dəyişən və yer tutucu sintaksislərini əvəzləyəcəyini verilənlər bazası növünə görə seçin. Hamısı standart olaraq aktivdir.",
sqlVariableSubstitutionEnabled: "Aktiv",
+8
View File
@@ -7501,6 +7501,14 @@ export default {
clickTableNavigationTargetDescription: "When enabled, Ctrl/Cmd+clicking a table name opens the table structure editor (DDL). When disabled, it opens the table data view.",
showTableDdlHoverPreview: "Show table DDL on hover",
showTableDdlHoverPreviewDescription: "Show a DDL preview when hovering over a table name in the SQL editor.",
tableHoverLookupMode: "Table hover lookup scope",
tableHoverLookupModeDescription: "How unqualified table names are resolved when hovering or Ctrl/Cmd+clicking in the SQL editor.",
tableHoverLookupModeCurrent: "Current schema only",
tableHoverLookupModeFallback: "Current schema, then all schemas",
tableHoverLookupModeAlways: "All schemas first",
tableHoverLookupModeCurrentDescription: "Only look up tables in the schema selected in the toolbar.",
tableHoverLookupModeFallbackDescription: "Try the selected schema first, then search other schemas by table name if not found.",
tableHoverLookupModeAlwaysDescription: "Search all schemas by table name first. Prefer when writing cross-schema procedures.",
sqlVariableSyntax: "SQL variable & placeholder substitution",
sqlVariableSyntaxDescription: "Choose which variable and placeholder syntaxes DBX substitutes before running SQL, per database type. All are enabled by default.",
sqlVariableSubstitutionEnabled: "Enabled",
+8
View File
@@ -7025,6 +7025,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "Cuando está activado, Ctrl/Cmd+clic en el nombre de la tabla abre el editor de estructura (DDL). Cuando está desactivado, abre la vista de datos.",
showTableDdlHoverPreview: "Mostrar DDL de tabla al pasar el cursor",
showTableDdlHoverPreviewDescription: "Muestra una vista previa del DDL al pasar el cursor sobre el nombre de una tabla en el editor SQL.",
tableHoverLookupMode: "Alcance de búsqueda al pasar el cursor",
tableHoverLookupModeDescription: "Cómo se resuelven los nombres de tabla sin calificar al pasar el cursor o al hacer Ctrl/Cmd+clic en el editor SQL.",
tableHoverLookupModeCurrent: "Solo esquema actual",
tableHoverLookupModeFallback: "Esquema actual, luego todos",
tableHoverLookupModeAlways: "Todos los esquemas primero",
tableHoverLookupModeCurrentDescription: "Busca tablas solo en el esquema seleccionado en la barra de herramientas.",
tableHoverLookupModeFallbackDescription: "Prueba primero el esquema seleccionado; si no aparece, busca por nombre en otros esquemas.",
tableHoverLookupModeAlwaysDescription: "Busca primero por nombre en todos los esquemas. Útil al escribir procedimientos entre esquemas.",
sqlVariableSyntax: "Sustitución de variables y marcadores SQL",
sqlVariableSyntaxDescription: "Elige qué sintaxis de variables y marcadores sustituye DBX antes de ejecutar SQL, por tipo de base de datos. Todas están habilitadas de forma predeterminada.",
sqlVariableSubstitutionEnabled: "Activada",
+8
View File
@@ -7025,6 +7025,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "Quando attivato, Ctrl/Cmd+clic sul nome della tabella apre l'editor di struttura (DDL). Quando disattivato, apre la vista dati.",
showTableDdlHoverPreview: "Mostra DDL tabella al passaggio del mouse",
showTableDdlHoverPreviewDescription: "Mostra un'anteprima DDL quando si passa sul nome di una tabella nell'editor SQL.",
tableHoverLookupMode: "Ambito di ricerca al passaggio del mouse",
tableHoverLookupModeDescription: "Come risolvere i nomi di tabella non qualificati al passaggio del mouse o con Ctrl/Cmd+clic nell'editor SQL.",
tableHoverLookupModeCurrent: "Solo schema corrente",
tableHoverLookupModeFallback: "Schema corrente, poi tutti",
tableHoverLookupModeAlways: "Tutti gli schemi prima",
tableHoverLookupModeCurrentDescription: "Cerca le tabelle solo nello schema selezionato nella barra degli strumenti.",
tableHoverLookupModeFallbackDescription: "Prova prima lo schema selezionato; se non trovato, cerca per nome in altri schemi.",
tableHoverLookupModeAlwaysDescription: "Cerca prima per nome in tutti gli schemi. Utile per procedure cross-schema.",
sqlVariableSyntax: "Sostituzione di variabili e segnaposto SQL",
sqlVariableSyntaxDescription: "Scegli quali sintassi di variabili e segnaposto DBX sostituisce prima di eseguire SQL, per tipo di database. Tutte abilitate per impostazione predefinita.",
sqlVariableSubstitutionEnabled: "Attiva",
+8
View File
@@ -7038,6 +7038,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "有効にすると、Ctrl/Cmd+クリックでテーブル構造エディタ(DDL)を開きます。無効の場合はテーブルデータビューを開きます。",
showTableDdlHoverPreview: "ホバー時にテーブル DDL を表示",
showTableDdlHoverPreviewDescription: "SQL エディタでテーブル名にホバーしたときに DDL プレビューを表示します。",
tableHoverLookupMode: "テーブルホバーの検索範囲",
tableHoverLookupModeDescription: "SQL エディタで未修飾のテーブル名にホバーまたは Ctrl/Cmd+クリックしたときのスキーマ解決方法。",
tableHoverLookupModeCurrent: "現在のスキーマのみ",
tableHoverLookupModeFallback: "現在のスキーマ優先、次に全体",
tableHoverLookupModeAlways: "全スキーマを先に検索",
tableHoverLookupModeCurrentDescription: "ツールバーで選択中のスキーマ内だけを検索します。",
tableHoverLookupModeFallbackDescription: "選択中のスキーマを先に試し、見つからなければテーブル名で他スキーマも検索します。",
tableHoverLookupModeAlwaysDescription: "テーブル名で全スキーマを先に検索します。クロススキーマのプロシージャ作成向け。",
sqlVariableSyntax: "SQL 変数・プレースホルダー置換",
sqlVariableSyntaxDescription: "SQL 実行前に置換する変数・プレースホルダー構文をデータベース種別ごとに選択します。既定ではすべて有効です。",
sqlVariableSubstitutionEnabled: "置換を有効化",
+8
View File
@@ -6219,6 +6219,14 @@ export default withEnglishFallback({
editorTab: "편집기",
showTableDdlHoverPreview: "호버 시 테이블 DDL 표시",
showTableDdlHoverPreviewDescription: "SQL 편집기에서 테이블 이름에 마우스를 올리면 DDL 미리 보기를 표시합니다.",
tableHoverLookupMode: "테이블 호버 조회 범위",
tableHoverLookupModeDescription: "SQL 편집기에서 한정되지 않은 테이블 이름에 호버하거나 Ctrl/Cmd+클릭할 때 스키마를 해석하는 방식입니다.",
tableHoverLookupModeCurrent: "현재 스키마만",
tableHoverLookupModeFallback: "현재 스키마 우선, 이후 전체",
tableHoverLookupModeAlways: "전체 스키마 우선 검색",
tableHoverLookupModeCurrentDescription: "도구 모음에서 선택한 스키마에서만 테이블을 찾습니다.",
tableHoverLookupModeFallbackDescription: "선택한 스키마를 먼저 찾고, 없으면 테이블 이름으로 다른 스키마도 검색합니다.",
tableHoverLookupModeAlwaysDescription: "테이블 이름으로 모든 스키마를 먼저 검색합니다. 크로스 스키마 프로시저 작성에 적합합니다.",
globalConnectTimeout: "전역 연결 제한 시간 (초)",
globalConnectTimeoutDescription: "전역 설정을 상속하는 연결에 사용됩니다.",
globalQueryTimeout: "전역 쿼리 제한 시간 (초)",
+8
View File
@@ -7027,6 +7027,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "Quando ativado, Ctrl/Cmd+clicar no nome da tabela abre o editor de estrutura (DDL). Quando desativado, abre a visualização de dados.",
showTableDdlHoverPreview: "Mostrar DDL da tabela ao passar o mouse",
showTableDdlHoverPreviewDescription: "Mostra uma prévia do DDL ao passar o mouse sobre o nome de uma tabela no editor SQL.",
tableHoverLookupMode: "Escopo de busca ao passar o mouse",
tableHoverLookupModeDescription: "Como nomes de tabela sem qualificação são resolvidos ao passar o mouse ou com Ctrl/Cmd+clique no editor SQL.",
tableHoverLookupModeCurrent: "Apenas o schema atual",
tableHoverLookupModeFallback: "Schema atual, depois todos",
tableHoverLookupModeAlways: "Todos os schemas primeiro",
tableHoverLookupModeCurrentDescription: "Procura tabelas apenas no schema selecionado na barra de ferramentas.",
tableHoverLookupModeFallbackDescription: "Tenta o schema selecionado primeiro; se não achar, busca pelo nome em outros schemas.",
tableHoverLookupModeAlwaysDescription: "Busca primeiro pelo nome em todos os schemas. Útil ao escrever procedures entre schemas.",
sqlVariableSyntax: "Substituição de variáveis e espaços reservados SQL",
sqlVariableSyntaxDescription: "Escolha quais sintaxes de variáveis e espaços reservados o DBX substitui antes de executar SQL, por tipo de banco de dados. Todas ativadas por padrão.",
sqlVariableSubstitutionEnabled: "Ativada",
+8
View File
@@ -7184,6 +7184,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "Etkinleştirildiğinde bir tablo adına Ctrl/Cmd+tıklamak tablo yapısı düzenleyicisini (DDL) açar. Kapatıldığında tablo veri görünümünü açar.",
showTableDdlHoverPreview: "Üzerine gelindiğinde tablo DDL'ini göster",
showTableDdlHoverPreviewDescription: "SQL düzenleyicisinde bir tablo adının üzerine gelindiğinde DDL önizlemesi göster.",
tableHoverLookupMode: "Tablo üzerine gelince arama kapsamı",
tableHoverLookupModeDescription: "SQL düzenleyicisinde nitelendirilmemiş tablo adlarına gelindiğinde veya Ctrl/Cmd+tıklandığında şemanın nasıl çözüleceği.",
tableHoverLookupModeCurrent: "Yalnızca geçerli şema",
tableHoverLookupModeFallback: "Önce geçerli şema, sonra tümü",
tableHoverLookupModeAlways: "Önce tüm şemalar",
tableHoverLookupModeCurrentDescription: "Tabloları yalnızca araç çubuğunda seçili şemada arar.",
tableHoverLookupModeFallbackDescription: "Önce seçili şemayı dener; bulunamazsa tablo adına göre diğer şemaları arar.",
tableHoverLookupModeAlwaysDescription: "Tablo adına göre tüm şemaları önce arar. Çapraz şema yordamları yazarken kullanışlıdır.",
sqlVariableSyntax: "SQL değişken ve yer tutucu değiştirme",
sqlVariableSyntaxDescription: "DBX'in SQL çalıştırmadan önce hangi değişken ve yer tutucu sözdizimlerini değiştireceğini veritabanı türüne göre seçin. Varsayılan olarak tümü etkindir.",
sqlVariableSubstitutionEnabled: "Etkin",
+8
View File
@@ -7473,6 +7473,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "开启后,Ctrl/Cmd+点击表名将打开表结构编辑器(DDL);关闭则打开表数据视图。",
showTableDdlHoverPreview: "悬浮表名时显示 DDL",
showTableDdlHoverPreviewDescription: "在 SQL 编辑器中悬浮表名时显示 DDL 预览。",
tableHoverLookupMode: "表悬浮查找范围",
tableHoverLookupModeDescription: "在 SQL 编辑器中悬浮或 Ctrl/Cmd+点击未限定表名时,如何解析所属模式。",
tableHoverLookupModeCurrent: "仅当前模式",
tableHoverLookupModeFallback: "当前模式优先,再查全部",
tableHoverLookupModeAlways: "优先全库按名查找",
tableHoverLookupModeCurrentDescription: "只在工具栏选中的模式中查找表。",
tableHoverLookupModeFallbackDescription: "先查选中模式,未找到再按表名跨模式查找。",
tableHoverLookupModeAlwaysDescription: "直接按表名跨模式查找。适合编写跨模式存储过程。",
sqlVariableSyntax: "SQL 变量与占位符替换",
sqlVariableSyntaxDescription: "按数据库类型选择执行 SQL 前替换哪些变量与占位符语法,默认全部开启。",
sqlVariableSubstitutionEnabled: "启用替换",
+8
View File
@@ -6338,6 +6338,14 @@ export default withEnglishFallback({
clickTableNavigationTargetDescription: "開啟後,Ctrl/Cmd+點擊表名將開啟表結構編輯器(DDL);關閉則開啟表資料檢視。",
showTableDdlHoverPreview: "滑過表名時顯示 DDL",
showTableDdlHoverPreviewDescription: "在 SQL 編輯器中滑過表名時顯示 DDL 預覽。",
tableHoverLookupMode: "資料表懸停查詢範圍",
tableHoverLookupModeDescription: "在 SQL 編輯器中懸停或 Ctrl/Cmd+點擊未限定資料表名稱時,如何解析所屬結構描述。",
tableHoverLookupModeCurrent: "僅目前結構描述",
tableHoverLookupModeFallback: "目前結構描述優先,再查全部",
tableHoverLookupModeAlways: "優先全庫依名稱查詢",
tableHoverLookupModeCurrentDescription: "只在工具列選取的結構描述中查詢資料表。",
tableHoverLookupModeFallbackDescription: "先查選取的結構描述,找不到再依資料表名稱跨結構描述查詢。",
tableHoverLookupModeAlwaysDescription: "直接依資料表名稱跨結構描述查詢。適合撰寫跨結構描述預存程序。",
sqlVariableSyntax: "SQL 變數與佔位符替換",
sqlVariableSyntaxDescription: "依資料庫類型選擇執行 SQL 前替換哪些變數與佔位符語法,預設全部開啟。",
sqlVariableSubstitutionEnabled: "啟用替換",
@@ -0,0 +1,261 @@
import { describe, expect, it } from "vitest";
import { matchHoverTableCandidates, normalizeTableHoverLookupMode, pickHoverTableMatch, resolveHoverTableLookupTarget } from "@/lib/editor/hoverTableLookup";
describe("normalizeTableHoverLookupMode", () => {
it("defaults to fallback", () => {
expect(normalizeTableHoverLookupMode(undefined)).toBe("fallback");
expect(normalizeTableHoverLookupMode("invalid")).toBe("fallback");
});
it("preserves valid modes", () => {
expect(normalizeTableHoverLookupMode("current")).toBe("current");
expect(normalizeTableHoverLookupMode("fallback")).toBe("fallback");
expect(normalizeTableHoverLookupMode("always")).toBe("always");
});
});
describe("resolveHoverTableLookupTarget", () => {
it("uses schema.table qualifier regardless of mode", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["other", "users"],
mode: "current",
});
expect(target).toMatchObject({
database: "app",
schema: "other",
tableName: "users",
schemaFromQualifier: true,
allowGlobalFallback: false,
preferGlobalFirst: false,
});
});
it("uses db.schema.table qualifier", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["analytics", "reporting", "users"],
mode: "fallback",
});
expect(target).toMatchObject({
database: "analytics",
schema: "reporting",
tableName: "users",
schemaFromQualifier: true,
allowGlobalFallback: false,
});
});
it("treats MySQL-style db.table as database qualifier", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
databaseType: "mysql",
tableName: "users",
identifierParts: ["other_db", "users"],
mode: "fallback",
});
expect(target).toMatchObject({
database: "other_db",
schema: undefined,
schemaFromQualifier: true,
allowGlobalFallback: false,
});
});
it("prefers semantic schema over identifier parts", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["t"],
semanticSchema: "reporting",
mode: "current",
});
expect(target).toMatchObject({
schema: "reporting",
schemaFromQualifier: true,
allowGlobalFallback: false,
});
});
it("scopes unqualified names to the selected schema in current mode", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["users"],
mode: "current",
});
expect(target).toMatchObject({
schema: "public",
schemaFromQualifier: false,
allowGlobalFallback: false,
preferGlobalFirst: false,
});
});
it("allows global fallback for unqualified names in fallback mode", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["users"],
mode: "fallback",
});
expect(target).toMatchObject({
schema: "public",
allowGlobalFallback: true,
preferGlobalFirst: false,
});
});
it("prefers global search first in always mode", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["users"],
mode: "always",
});
expect(target).toMatchObject({
schema: undefined,
allowGlobalFallback: true,
preferGlobalFirst: true,
});
});
});
describe("pickHoverTableMatch", () => {
const tables = [
{ name: "users", schema: "reporting" },
{ name: "users", schema: "public" },
{ name: "orders", schema: "public" },
];
it("prefers the selected schema when multiple schemas match", () => {
expect(pickHoverTableMatch("users", tables, "public")).toEqual({ name: "users", schema: "public" });
});
it("falls back to the first name match when preferred schema is absent", () => {
expect(pickHoverTableMatch("users", tables, "missing")).toEqual({ name: "users", schema: "reporting" });
expect(pickHoverTableMatch("users", tables)).toEqual({ name: "users", schema: "reporting" });
});
it("returns null when the table name is missing", () => {
expect(pickHoverTableMatch("missing", tables, "public")).toBeNull();
});
});
describe("matchHoverTableCandidates", () => {
const tables = [
{ name: "users", schema: "reporting" },
{ name: "users", schema: "public" },
{ name: "orders", schema: "sales" },
];
it("prefers the toolbar schema for unqualified names even when another schema appears first", () => {
expect(
matchHoverTableCandidates(tables, {
lookups: ["users"],
tableName: "users",
preferredSchema: "public",
}),
).toEqual({ name: "users", schema: "public" });
});
it("resolves qualified schema.table without using preferredSchema", () => {
expect(
matchHoverTableCandidates(tables, {
lookups: ["reporting.users"],
tableName: "users",
preferredSchema: "public",
}),
).toEqual({ name: "users", schema: "reporting" });
});
it("uses semantic qualified lookup before unqualified preferred-schema match", () => {
expect(
matchHoverTableCandidates(tables, {
lookups: ["sales.orders", "orders"],
tableName: "orders",
preferredSchema: "public",
}),
).toEqual({ name: "orders", schema: "sales" });
});
it("ignores unqualified lookups in the lookups list and still prefers schema", () => {
// Unqualified entries must not short-circuit via matchTable's first-name-wins behavior.
expect(
matchHoverTableCandidates(tables, {
lookups: ["users", "public.users"],
tableName: "users",
preferredSchema: "public",
}),
).toEqual({ name: "users", schema: "public" });
});
it("returns null when nothing matches", () => {
expect(
matchHoverTableCandidates(tables, {
lookups: ["missing.table"],
tableName: "missing",
preferredSchema: "public",
}),
).toBeNull();
});
});
describe("resolveHoverTableLookupTarget cache-scope modes", () => {
it("fallback keeps selected schema for the first local pass", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["users"],
mode: "fallback",
});
expect(target.schema).toBe("public");
expect(target.preferGlobalFirst).toBe(false);
expect(target.allowGlobalFallback).toBe(true);
});
it("always clears schema so local lookup scans same-database schemas", () => {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["users"],
mode: "always",
});
expect(target.schema).toBeUndefined();
expect(target.preferGlobalFirst).toBe(true);
});
it("qualified names never enable same-database global fallback", () => {
for (const mode of ["current", "fallback", "always"] as const) {
const target = resolveHoverTableLookupTarget({
database: "app",
schema: "public",
databaseType: "postgres",
tableName: "users",
identifierParts: ["other", "users"],
mode,
});
expect(target.allowGlobalFallback).toBe(false);
expect(target.preferGlobalFirst).toBe(false);
expect(target.schema).toBe("other");
}
});
});
@@ -0,0 +1,130 @@
import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
import { matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
import type { DatabaseType } from "@/types/database";
export const TABLE_HOVER_LOOKUP_MODES = ["current", "fallback", "always"] as const;
export type TableHoverLookupMode = (typeof TABLE_HOVER_LOOKUP_MODES)[number];
export function normalizeTableHoverLookupMode(value: unknown, fallback: TableHoverLookupMode = "fallback"): TableHoverLookupMode {
return value === "current" || value === "fallback" || value === "always" ? value : fallback;
}
export interface ResolveHoverTableLookupTargetInput {
database: string;
/** Currently selected schema from the editor toolbar. */
schema?: string;
catalog?: string;
databaseType?: DatabaseType;
tableName: string;
/** Identifier segments from the hovered/clicked token (e.g. `["other", "users"]`). */
identifierParts: string[];
semanticDatabase?: string;
semanticSchema?: string;
mode: TableHoverLookupMode;
}
export interface HoverTableLookupTarget {
database: string;
schema?: string;
catalog?: string;
tableName: string;
/** Schema/database came from SQL qualifier or semantic model — do not global-search. */
schemaFromQualifier: boolean;
/** After a scoped miss, retry with globalSearch by exact table name. */
allowGlobalFallback: boolean;
/** Unqualified + always mode: search across schemas first. */
preferGlobalFirst: boolean;
}
/**
* Resolve the metadata scope for table hover / Ctrl+click navigation.
*
* Qualified names (`schema.table`, `db.schema.table`, MySQL-style `db.table`)
* always use the SQL qualifier. Unqualified names follow `mode`:
* - `current`: only the selected schema
* - `fallback`: selected schema first, then name-filtered global search
* - `always`: name-filtered global search first
*
* "Global" here means same-database cross-schema, not cross-database.
*/
export function resolveHoverTableLookupTarget(input: ResolveHoverTableLookupTargetInput): HoverTableLookupTarget {
const tableName = input.tableName;
let database = input.semanticDatabase ?? input.database;
let schema = input.semanticSchema;
let schemaFromQualifier = !!input.semanticSchema;
if (!schemaFromQualifier && input.identifierParts.length >= 2) {
const parts = input.identifierParts;
if (parts.length >= 3) {
database = parts[parts.length - 3]!;
schema = parts[parts.length - 2];
schemaFromQualifier = true;
} else {
if (input.databaseType && !isSchemaAware(input.databaseType) && !isSingleDatabase(input.databaseType)) {
database = parts[0]!;
schema = undefined;
} else {
schema = parts[0];
}
schemaFromQualifier = true;
}
}
if (!schemaFromQualifier) {
schema = input.schema;
}
const unqualified = !schemaFromQualifier;
const preferGlobalFirst = unqualified && input.mode === "always";
const allowGlobalFallback = unqualified && (input.mode === "fallback" || input.mode === "always");
return {
database,
schema: preferGlobalFirst ? undefined : schema,
catalog: input.catalog,
tableName,
schemaFromQualifier,
allowGlobalFallback,
preferGlobalFirst,
};
}
/** Prefer the selected schema when the same table name exists in multiple schemas. */
export function pickHoverTableMatch<T extends { name: string; schema?: string }>(tableName: string, tables: readonly T[], preferredSchema?: string): T | null {
const nameLower = tableName.toLowerCase();
const nameMatches = tables.filter((table) => table.name.toLowerCase() === nameLower);
if (nameMatches.length === 0) return null;
if (preferredSchema) {
const preferred = nameMatches.find((table) => table.schema?.toLowerCase() === preferredSchema.toLowerCase());
if (preferred) return preferred;
}
return nameMatches[0] ?? null;
}
export interface MatchHoverTableCandidatesOptions {
/** Qualified forms to try first (e.g. `schema.table` from the token or semantic model). */
lookups?: string[];
tableName: string;
preferredSchema?: string;
}
/**
* Match hover/navigation table candidates.
*
* Qualified lookups use {@link matchTable}. Unqualified names go through
* {@link pickHoverTableMatch} so the toolbar schema wins over arbitrary order.
*/
export function matchHoverTableCandidates<T extends { name: string; schema?: string; database?: string }>(tables: readonly T[], options: MatchHoverTableCandidatesOptions): T | null {
const seen = new Set<string>();
for (const lookup of options.lookups ?? []) {
const trimmed = lookup.trim();
if (!trimmed) continue;
const key = trimmed.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
if (splitQualifiedIdentifier(trimmed).length < 2) continue;
const matched = matchTable(trimmed, tables);
if (matched) return matched;
}
return pickHoverTableMatch(options.tableName, tables, options.preferredSchema);
}
@@ -126,6 +126,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode");
});
it("includes tableHoverLookupMode", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tableHoverLookupMode");
});
it("includes the SQL variable substitution master switch", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("sqlVariableSubstitutionEnabled");
});
@@ -4,6 +4,7 @@ import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { normalizeTableHoverLookupMode } from "@/lib/editor/hoverTableLookup";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
export const EDITOR_SETTINGS_DRAFT_KEYS = [
@@ -85,6 +86,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"generateSqlQuoteIdentifiers",
"formatSqlOnSqlFileSave",
"showTableDdlHoverPreview",
"tableHoverLookupMode",
"updateNotificationsEnabled",
"autoDownloadUpdates",
"sidebarObjectInfoMode",
@@ -138,6 +140,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
if (key === "externalSqlEditorMaxMb") return normalizeExternalSqlEditorMaxMb(value);
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
if (key === "tableHoverLookupMode") return normalizeTableHoverLookupMode(value);
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
return value;
@@ -138,6 +138,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-generate-sql-quote-identifiers", category: "editor", titleKey: "settings.generateSqlQuoteIdentifiers", descriptionKey: "settings.generateSqlQuoteIdentifiersDescription", targetId: "editor" },
{ id: "editor-format-sql-on-sql-file-save", category: "editor", titleKey: "settings.formatSqlOnSqlFileSave", descriptionKey: "settings.formatSqlOnSqlFileSaveDescription", targetId: "editor" },
{ id: "editor-table-ddl-hover-preview", category: "editor", titleKey: "settings.showTableDdlHoverPreview", descriptionKey: "settings.showTableDdlHoverPreviewDescription", targetId: "editor" },
{ id: "editor-table-hover-lookup-mode", category: "editor", titleKey: "settings.tableHoverLookupMode", descriptionKey: "settings.tableHoverLookupModeDescription", targetId: "editor" },
{ id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" },
{ id: "editor-sql-variables", category: "editor", titleKey: "settings.sqlVariableSyntax", descriptionKey: "settings.sqlVariableSyntaxDescription", targetId: "editor" },
{ id: "editor-saved-sql-target", category: "editor", titleKey: "settings.savedSqlOpenTarget", targetId: "editor" },
@@ -100,6 +100,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"generateSqlQuoteIdentifiers",
"formatSqlOnSqlFileSave",
"showTableDdlHoverPreview",
"tableHoverLookupMode",
"sqlVariableSubstitutionEnabled",
"sqlVariableSyntaxOverrides",
],
+1 -1
View File
@@ -643,7 +643,7 @@ export function splitQualifiedIdentifier(identifier: string): string[] {
}
/** Match identifier against known table names (case-insensitive). Supports qualified identifiers like schema.table. */
export function matchTable<T extends { name: string; database?: string; schema?: string }>(identifier: string, tables: T[]): T | null {
export function matchTable<T extends { name: string; database?: string; schema?: string }>(identifier: string, tables: readonly T[]): T | null {
const parts = splitQualifiedIdentifier(identifier);
const normalizedIdentifier = parts.length > 0 ? parts.join(".").toLowerCase() : identifier.toLowerCase();
+18
View File
@@ -676,6 +676,24 @@ describe("normalizeEditorSettings - showTableDdlHoverPreview", () => {
});
});
describe("normalizeEditorSettings - tableHoverLookupMode", () => {
it("defaults tableHoverLookupMode to fallback", () => {
expect(normalizeEditorSettings({}).tableHoverLookupMode).toBe("fallback");
});
it("preserves the three valid modes", () => {
expect(normalizeEditorSettings({ tableHoverLookupMode: "current" }).tableHoverLookupMode).toBe("current");
expect(normalizeEditorSettings({ tableHoverLookupMode: "fallback" }).tableHoverLookupMode).toBe("fallback");
expect(normalizeEditorSettings({ tableHoverLookupMode: "always" }).tableHoverLookupMode).toBe("always");
});
it("falls back to fallback for invalid values", () => {
expect(normalizeEditorSettings({ tableHoverLookupMode: "invalid" } as any).tableHoverLookupMode).toBe("fallback");
expect(normalizeEditorSettings({ tableHoverLookupMode: undefined } as any).tableHoverLookupMode).toBe("fallback");
expect(normalizeEditorSettings({ tableHoverLookupMode: null } as any).tableHoverLookupMode).toBe("fallback");
});
});
describe("normalizeEditorSettings - completionTriggerMode", () => {
it("defaults completionTriggerMode to positional", () => {
expect(normalizeEditorSettings({}).completionTriggerMode).toBe("positional");
+6 -1
View File
@@ -27,13 +27,14 @@ import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type Sql
import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax";
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
import { normalizeTableHoverLookupMode, type TableHoverLookupMode } from "@/lib/editor/hoverTableLookup";
import { normalizeCompletionTriggerMode, type SqlCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { DEFAULT_CSV_QUOTE_MODE, normalizeCsvQuoteMode, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
import { configureMetadataRuntimeCache, METADATA_CACHE_DEFAULT_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache";
import type { AiApiStyle, AiAssistantMode, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai";
import type { SqlShortcutAction, SqlSnippet, TableInfoTab } from "@/types/database";
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, CsvQuoteMode, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode };
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, CsvQuoteMode, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode, TableHoverLookupMode };
export interface DesktopSettings {
show_tray_icon: boolean;
@@ -895,6 +896,7 @@ export interface EditorSettings {
sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides;
continueOnErrorOnBatch: boolean;
showTableDdlHoverPreview: boolean;
tableHoverLookupMode: TableHoverLookupMode;
clickTableNavigationTarget: ClickTableNavigationTarget;
completionTriggerMode: SqlCompletionTriggerMode;
defaultTransactionMode: DefaultTransactionMode;
@@ -1136,6 +1138,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
sqlVariableSyntaxOverrides: {},
continueOnErrorOnBatch: false,
showTableDdlHoverPreview: true,
tableHoverLookupMode: "fallback",
clickTableNavigationTarget: "data",
completionTriggerMode: "positional",
defaultTransactionMode: "auto",
@@ -1650,6 +1653,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
sqlVariableSyntaxOverrides: normalizeSqlVariableSyntaxOverrides(settings.sqlVariableSyntaxOverrides),
continueOnErrorOnBatch: settings.continueOnErrorOnBatch === true,
showTableDdlHoverPreview: typeof settings.showTableDdlHoverPreview === "boolean" ? settings.showTableDdlHoverPreview : DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview,
tableHoverLookupMode: normalizeTableHoverLookupMode(settings.tableHoverLookupMode, DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode),
clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget),
completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode),
defaultTransactionMode: normalizeDefaultTransactionMode(settings.defaultTransactionMode),
@@ -2374,6 +2378,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.sqlVariableSyntaxOverrides !== undefined) editorSettings.value.sqlVariableSyntaxOverrides = normalizeSqlVariableSyntaxOverrides(partial.sqlVariableSyntaxOverrides);
if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true;
if (partial.showTableDdlHoverPreview !== undefined) editorSettings.value.showTableDdlHoverPreview = partial.showTableDdlHoverPreview === true;
if (partial.tableHoverLookupMode !== undefined) editorSettings.value.tableHoverLookupMode = normalizeTableHoverLookupMode(partial.tableHoverLookupMode, DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode);
if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);
if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode);
if (partial.defaultTransactionMode !== undefined) editorSettings.value.defaultTransactionMode = normalizeDefaultTransactionMode(partial.defaultTransactionMode);