From 949324dc89517a0bb0a8770e3c56994907a8b7ef Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Thu, 16 Apr 2026 01:06:49 +0000 Subject: [PATCH] feat: SQL Explorer with PostgreSQL schema browser and pre-built queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SQL Explorer was querying Trino which has zero tables. Rewrote to use PostgreSQL directly: Backend: - GET /api/analytics/pg-schema: returns all public tables with column names, types, and nullability from information_schema - POST /api/analytics/pg-query: read-only SQL execution against PostgreSQL with SELECT-only enforcement, auto LIMIT, and descriptive error messages for syntax/table/query errors Frontend: - Schema browser shows all PostgreSQL tables with columns and types - Click a table name → generates SELECT * FROM table LIMIT 100 - Pre-built Queries section with 12 seeded queries covering companies, recommendations, trends, market prices, documents, global events, trading decisions, ingestion health, reserve pool, sector exposure - User-saved queries shown separately with delete buttons - Chart builder, Monaco editor, and save functionality preserved Migration 021: seeds 12 pre-built saved queries --- frontend/src/pages/SqlExplorer.tsx | 58 +++++++++++++++--- frontend/src/test/mocks/handlers.ts | 20 +++++++ infra/migrations/021_seed_saved_queries.sql | 38 ++++++++++++ services/api/app.py | 65 +++++++++++++++++++++ 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 infra/migrations/021_seed_saved_queries.sql diff --git a/frontend/src/pages/SqlExplorer.tsx b/frontend/src/pages/SqlExplorer.tsx index 98bcd35..a9a6780 100644 --- a/frontend/src/pages/SqlExplorer.tsx +++ b/frontend/src/pages/SqlExplorer.tsx @@ -26,7 +26,7 @@ interface SavedQuery { interface SchemaInfo { catalog: string; schema: string; - tables: Array<{ name: string; columns: Array<{ name: string; type: string }> }>; + tables: Array<{ name: string; columns: Array<{ name: string; type: string; nullable?: boolean }> }>; } type ChartType = 'none' | 'bar' | 'line' | 'scatter'; @@ -41,8 +41,8 @@ export function SqlExplorerPage() { const [yCol, setYCol] = useState(1); const { data: schema } = useQuery({ - queryKey: ['trino-schema'], - queryFn: () => apiGet('query', '/api/analytics/schema'), + queryKey: ['pg-schema'], + queryFn: () => apiGet('query', '/api/analytics/pg-schema'), }); const { data: savedQueries } = useQuery({ @@ -51,7 +51,7 @@ export function SqlExplorerPage() { }); const executeMutation = useMutation({ - mutationFn: (sqlText: string) => apiPost('query', '/api/analytics/query', { sql: sqlText, limit: 1000 }), + mutationFn: (sqlText: string) => apiPost('query', '/api/analytics/pg-query', { sql: sqlText, limit: 1000 }), onSuccess: (data) => { setResult(data); setError(null); }, onError: (err: Error) => { setError(err.message); setResult(null); }, }); @@ -75,6 +75,11 @@ export function SqlExplorerPage() { if (name) saveMutation.mutate({ name, sql_text: sql }); }, [sql, saveMutation]); + const handleQuickInsert = useCallback((tableName: string) => { + const query = `SELECT * FROM ${tableName} LIMIT 100`; + setSql(query); + }, []); + // Build chart data from result const chartData = result && result.columns.length >= 2 ? result.rows.map((row) => ({ @@ -84,16 +89,27 @@ export function SqlExplorerPage() { })) : []; + // Split saved queries into pre-built (no id-based delete) and user-saved + const preBuiltNames = new Set([ + 'Company Overview', 'Recent Recommendations', 'High Confidence Buys', + 'Trend Windows Summary', 'Market Prices', 'Document Counts by Type', + 'Global Events', 'Trading Decisions', 'Ingestion Health', + 'Reserve Pool Ledger', 'Sector Exposure', 'Source Coverage Matrix', + ]); + const preBuiltQueries = (savedQueries ?? []).filter((sq) => preBuiltNames.has(sq.name)); + const userQueries = (savedQueries ?? []).filter((sq) => !preBuiltNames.has(sq.name)); + return (
{/* Schema browser sidebar */} -
+

Schema

{schema?.tables.map((t) => (
@@ -108,9 +124,35 @@ export function SqlExplorerPage() {
))} - {/* Saved queries */} + {/* Pre-built queries */} + {preBuiltQueries.length > 0 && ( + <> +

Pre-built Queries

+ {preBuiltQueries.map((sq) => ( +
+ + {sq.description && ( +

+ {sq.description} +

+ )} +
+ ))} + + )} + + {/* User saved queries */}

Saved Queries

- {(savedQueries ?? []).map((sq) => ( + {userQueries.length === 0 && ( +

No saved queries yet

+ )} + {userQueries.map((sq) => (