mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(editor): resolve table hover across schemas in current database
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "置換を有効化",
|
||||
|
||||
@@ -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: "전역 쿼리 제한 시간 (초)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "启用替换",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user