feat: add cross-database entity lookup for searching column values across all databases

Enables searching for a specific column value (e.g. carrier_id=123) across all databases on a PostgreSQL server. The backend creates temporary connection pools per database (semaphore-limited to 5), queries information_schema for matching columns, and executes read-only SELECTs with real-time progress events. Results are grouped by database/table in a new "Entity Lookup" tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 17:28:33 +03:00
parent a2371f00df
commit d5cff8bd5e
13 changed files with 1030 additions and 5 deletions

View File

@@ -0,0 +1,116 @@
import { useState } from "react";
import {
ChevronDown,
ChevronRight,
AlertCircle,
Database,
} from "lucide-react";
import { ResultsTable } from "@/components/results/ResultsTable";
import type { LookupDatabaseResult } from "@/types";
interface Props {
dbResult: LookupDatabaseResult;
}
export function LookupResultGroup({ dbResult }: Props) {
const [expanded, setExpanded] = useState(dbResult.tables.length > 0);
const [expandedTables, setExpandedTables] = useState<Set<string>>(
() => new Set(dbResult.tables.map((t) => `${t.schema}.${t.table}`))
);
const totalRows = dbResult.tables.reduce((s, t) => s + t.row_count, 0);
const hasError = !!dbResult.error;
const hasMatches = dbResult.tables.length > 0;
const toggleTable = (key: string) => {
setExpandedTables((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
};
return (
<div className="border rounded-md">
<button
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-accent/50"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<ChevronDown className="h-4 w-4 shrink-0" />
) : (
<ChevronRight className="h-4 w-4 shrink-0" />
)}
<Database className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="font-medium">{dbResult.database}</span>
{hasMatches && (
<span className="text-xs text-muted-foreground">
{dbResult.tables.length} table{dbResult.tables.length !== 1 && "s"},{" "}
{totalRows} row{totalRows !== 1 && "s"}
</span>
)}
{hasError && (
<span className="flex items-center gap-1 text-xs text-destructive">
<AlertCircle className="h-3 w-3" />
{dbResult.error}
</span>
)}
{!hasError && !hasMatches && (
<span className="text-xs text-muted-foreground">no matches</span>
)}
<span className="ml-auto text-xs text-muted-foreground">
{dbResult.search_time_ms}ms
</span>
</button>
{expanded && hasMatches && (
<div className="border-t">
{dbResult.tables.map((table) => {
const key = `${table.schema}.${table.table}`;
const isOpen = expandedTables.has(key);
return (
<div key={key} className="border-b last:border-b-0">
<button
className="flex w-full items-center gap-2 px-5 py-1.5 text-left text-xs hover:bg-accent/50"
onClick={() => toggleTable(key)}
>
{isOpen ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span className="font-medium">
{table.schema}.{table.table}
</span>
<span className="text-muted-foreground">
({table.row_count} row{table.row_count !== 1 && "s"}
{table.total_count > table.row_count &&
`, ${table.total_count} total`}
)
</span>
<span className="text-muted-foreground">
[{table.column_type}]
</span>
</button>
{isOpen && table.columns.length > 0 && (
<div className="h-[200px] border-t">
<ResultsTable
columns={table.columns}
types={table.types}
rows={table.rows as unknown[][]}
/>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}