Skip to content

Commit 42fc87d

Browse files
feat(advisor): surface schema in recommendations and grants
Advisor scans non-public schemas, emits schema-qualified CREATE INDEX SQL, and Performance/privileges/dashboard skill copy follow suit. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent e46fe52 commit 42fc87d

8 files changed

Lines changed: 32 additions & 23 deletions

File tree

agent/skills/dashboard-design/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Hard rules:
5151
## Procedure
5252

5353
1. **Ground.** `get_brain_context`, `get_schema`, `list_business_rules`, `get_relationships`. Obey business rules about which table/column/filter/currency a concept uses — quote them; don't guess a similar-looking table.
54-
2. **Design.** Decide the KPIs, charts, tables, and controls (date range, dropdowns) the request calls for. Sketch the SQL for each — table-qualified, read-only.
54+
2. **Design.** Decide the KPIs, charts, tables, and controls (date range, dropdowns) the request calls for. Sketch the SQL for each — **schema-qualified** (`crm.orders`, not bare `orders` when the DB has multiple schemas), table-qualified columns, read-only.
5555
3. **Handle dates correctly.** Check the column's type in the schema. If it's a real DATE/DATETIME, filter with `BETWEEN '2026-07-01' AND '2026-07-08'`. **If it's a Unix-epoch integer** (seconds), filter on the epoch: `col >= UNIX_TIMESTAMP('2026-07-01 00:00:00') AND col < UNIX_TIMESTAMP('2026-07-09 00:00:00')`. Build these strings in JS from the picker's values.
5656
4. **Verify.** Run every query with `execute_sql` and READ the rows: date windows bounded and inside range (never the future), KPI value types right (name = text, money = currency), totals plausible vs a `COUNT(*)`. Fix and re-run until correct.
5757
5. **Intent checklist.** Before emitting, list every explicit ask (each chart, each metric, each control like "a date range picker defaulting to today") and confirm the HTML satisfies ALL of them. An unmet ask is a failed dashboard even if the data is perfect.

backend/src/main/java/com/dbaagent/service/DatabaseAdvisorService.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
281281

282282
try (Connection connection = connectionService.getConnection(connectionId, connRequest)) {
283283

284-
// Query 1: Tables with high sequential scans
284+
// Query 1: Tables with high sequential scans (all non-system schemas)
285285
String query1 = """
286286
SELECT
287287
schemaname,
@@ -296,7 +296,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
296296
ELSE 0
297297
END as avg_seq_tup_read
298298
FROM pg_stat_user_tables
299-
WHERE schemaname = 'public'
299+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
300300
AND seq_scan > 1000
301301
AND n_live_tup > 10000
302302
AND (idx_scan IS NULL OR seq_scan > idx_scan * 2)
@@ -308,11 +308,13 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
308308
ResultSet rs = stmt.executeQuery(query1)) {
309309

310310
while (rs.next()) {
311+
String schemaName = rs.getString("schemaname");
311312
String tableName = rs.getString("tablename");
312313
long seqScans = rs.getLong("seq_scan");
313314
long seqTupRead = rs.getLong("seq_tup_read");
314315
long liveRows = rs.getLong("n_live_tup");
315316
double avgSeqRead = rs.getDouble("avg_seq_tup_read");
317+
String qualifiedTable = "public".equals(schemaName) ? tableName : schemaName + "." + tableName;
316318

317319
// Get candidate columns
318320
List<String> candidateColumns = getPostgresCandidateColumns(
@@ -325,7 +327,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
325327
.id(UUID.randomUUID().toString())
326328
.connectionId(connectionId)
327329
.tableName(tableName)
328-
.schemaName("public")
330+
.schemaName(schemaName)
329331
.columns(candidateColumns)
330332
.indexType("BTREE")
331333
.priority(seqScans > 10000 ?
@@ -334,13 +336,13 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
334336
.reasoning(String.format(
335337
"Table '%s' has %,d sequential scans reading %,d rows (avg %.0f rows/scan). " +
336338
"Current row count: %,d. An index would significantly improve query performance.",
337-
tableName, seqScans, seqTupRead, avgSeqRead, liveRows
339+
qualifiedTable, seqScans, seqTupRead, avgSeqRead, liveRows
338340
))
339341
.suggestedSQL(String.format(
340342
"CREATE INDEX CONCURRENTLY idx_%s_%s ON %s(%s)",
341343
tableName,
342344
String.join("_", candidateColumns),
343-
tableName,
345+
qualifiedTable,
344346
String.join(", ", candidateColumns)
345347
))
346348
.metrics(IndexRecommendation.IndexRecommendationMetrics.builder()
@@ -358,9 +360,10 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
358360
}
359361
}
360362

361-
// Query 2: Foreign keys without indexes
363+
// Query 2: Foreign keys without indexes (all non-system schemas)
362364
String query2 = """
363365
SELECT
366+
tc.table_schema,
364367
tc.table_name,
365368
kcu.column_name,
366369
ccu.table_name AS foreign_table_name
@@ -371,11 +374,11 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
371374
JOIN information_schema.constraint_column_usage AS ccu
372375
ON ccu.constraint_name = tc.constraint_name
373376
WHERE tc.constraint_type = 'FOREIGN KEY'
374-
AND tc.table_schema = 'public'
377+
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
375378
AND NOT EXISTS (
376379
SELECT 1
377380
FROM pg_indexes
378-
WHERE schemaname = 'public'
381+
WHERE schemaname = tc.table_schema
379382
AND tablename = tc.table_name
380383
AND indexdef LIKE '%' || kcu.column_name || '%'
381384
)
@@ -385,15 +388,17 @@ AND NOT EXISTS (
385388
ResultSet rs = stmt.executeQuery(query2)) {
386389

387390
while (rs.next()) {
391+
String schemaName = rs.getString("table_schema");
388392
String tableName = rs.getString("table_name");
389393
String columnName = rs.getString("column_name");
390394
String foreignTable = rs.getString("foreign_table_name");
395+
String qualifiedTable = "public".equals(schemaName) ? tableName : schemaName + "." + tableName;
391396

392397
IndexRecommendation rec = IndexRecommendation.builder()
393398
.id(UUID.randomUUID().toString())
394399
.connectionId(connectionId)
395400
.tableName(tableName)
396-
.schemaName("public")
401+
.schemaName(schemaName)
397402
.columns(Collections.singletonList(columnName))
398403
.indexType("BTREE")
399404
.priority(IndexRecommendation.RecommendationPriority.HIGH)
@@ -404,7 +409,7 @@ AND NOT EXISTS (
404409
))
405410
.suggestedSQL(String.format(
406411
"CREATE INDEX CONCURRENTLY idx_%s_%s ON %s(%s)",
407-
tableName, columnName, tableName, columnName
412+
tableName, columnName, qualifiedTable, columnName
408413
))
409414
.metrics(IndexRecommendation.IndexRecommendationMetrics.builder()
410415
.estimatedImprovementPercent(70)

src/components/ConnectionWizard/components/PrivilegesAccordion.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@ export function PrivilegesAccordion({ dbType }) {
1919
-- Replace 'your_user' with your database username
2020
-- Replace 'your_database' with your database name
2121
22-
-- Basic read access to all tables
22+
-- Basic read access (repeat GRANT block per schema you want DeepSQL to see)
2323
GRANT SELECT ON ALL TABLES IN SCHEMA public TO your_user;
2424
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO your_user;
25+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
26+
GRANT SELECT ON TABLES TO your_user;
27+
28+
-- Multi-schema example (crm / sales / …)
29+
-- GRANT USAGE ON SCHEMA crm TO your_user;
30+
-- GRANT SELECT ON ALL TABLES IN SCHEMA crm TO your_user;
31+
-- GRANT SELECT ON ALL SEQUENCES IN SCHEMA crm TO your_user;
32+
-- ALTER DEFAULT PRIVILEGES IN SCHEMA crm GRANT SELECT ON TABLES TO your_user;
2533
2634
-- Access to system views for monitoring
2735
GRANT pg_read_all_stats TO your_user;
2836
2937
-- Enable pg_stat_statements extension (if not already enabled)
30-
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
31-
32-
-- For future tables
33-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
34-
GRANT SELECT ON TABLES TO your_user;`
38+
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;`
3539
}
3640

3741
if (dbType === 'mysql') {

src/components/tabs/Core/DatabaseAdvisorTab.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export default function DatabaseAdvisorTab({ connectionId }) {
196196
<span>{rec.priority}</span>
197197
</div>
198198
<div className={styles.issueTitle}>
199-
{rec.tableName ? `Table: ${rec.tableName}` : rec.title}
199+
{rec.tableName ? `Table: ${rec.schemaName ? `${rec.schemaName}.${rec.tableName}` : rec.tableName}` : rec.title}
200200
</div>
201201
</div>
202202
<div className={styles.issueReasoning}>

src/components/tabs/Performance/ExplainPlanTab.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,9 +1741,9 @@ export default function ExplainPlanTab({ connectionId }) {
17411741
<div className={styles.indexSection}>
17421742
<h3>Index Recommendations ({indexRecommendations.length})</h3>
17431743
{indexRecommendations.map((rec, idx) => (
1744-
<div key={`${rec.tableName}-${idx}`} className={styles.indexCard}>
1744+
<div key={`${rec.schemaName || ''}.${rec.tableName}-${idx}`} className={styles.indexCard}>
17451745
<div className={styles.indexHeader}>
1746-
<span className={styles.indexTable}>{rec.tableName}</span>
1746+
<span className={styles.indexTable}>{rec.schemaName ? `${rec.schemaName}.${rec.tableName}` : rec.tableName}</span>
17471747
{rec.priority && (
17481748
<span className={styles.indexPriority}>{rec.priority}</span>
17491749
)}

src/components/tabs/Performance/SlowQueryAnalysisTab.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1772,7 +1772,7 @@ export default function SlowQueryAnalysisTab({ connectionId }) {
17721772
: "";
17731773
const statement =
17741774
rec.suggestedSQL ||
1775-
`CREATE INDEX ON ${rec.tableName} ${columns}`.trim();
1775+
`CREATE INDEX ON ${rec.schemaName ? `${rec.schemaName}.${rec.tableName}` : rec.tableName} ${columns}`.trim();
17761776
items.push({ label: "Index", text: statement });
17771777
});
17781778
}

src/components/tabs/Performance/WorkloadAnalysisPanel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export default function WorkloadAnalysisPanel({ connectionId }) {
251251
{rec.kind === "DROP_INDEX" ? "DROP" : "CREATE"}
252252
</span>
253253
<code className={styles.recTitle}>
254-
{rec.tableName} ({rec.columnNames})
254+
{rec.schemaName ? `${rec.schemaName}.${rec.tableName}` : rec.tableName} ({rec.columnNames})
255255
</code>
256256
{rec.priority && (
257257
<span className={`${styles.prio} ${styles[`prio_${(rec.priority || "").toLowerCase()}`] || ""}`}>

src/components/tabs/Performance/components/TableHeatmap.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default function TableHeatmap({ data = [], caption = 'Based on scan frequ
107107
{data.slice(0, 10).map((item, idx) => {
108108
const widthPercent = Math.max((item.usageScore / maxScore) * 100, 8)
109109
const opacity = 0.3 + (item.usageScore / 100) * 0.7
110-
const displayName = item.tableName?.split('.').pop() || item.tableName
110+
const displayName = item.tableName || ''
111111
const tooltipContent = getTableTooltipContent(displayName, item.usageScore)
112112

113113
return (

0 commit comments

Comments
 (0)