Files
dbx/apps/desktop/src/lib/editor/hoverTableLookup.ts
T

131 lines
4.9 KiB
TypeScript

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);
}