@indiekitai/pg-dash 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/README.zh-CN.md +11 -6
- package/dist/cli.js +124 -20
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +564 -19
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# pg-dash
|
|
4
4
|
|
|
5
|
-
**The AI-native PostgreSQL health checker.** One command to audit your database,
|
|
5
|
+
**The AI-native PostgreSQL health checker.** One command to audit your database, 23 MCP tools for AI-assisted optimization, CI integration for automated checks.
|
|
6
6
|
|
|
7
7
|
Not another monitoring dashboard — pg-dash is built to fit into your **AI coding workflow**:
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
|
|
|
47
47
|
| pganalyze | $149+/mo | SaaS signup | ❌ | ❌ |
|
|
48
48
|
| Grafana+Prometheus | Free | 3 services | ❌ | ❌ |
|
|
49
49
|
| pgAdmin | Free | Complex UI | ❌ | ❌ |
|
|
50
|
-
| **pg-dash** | **Free** | **One command** | **
|
|
50
|
+
| **pg-dash** | **Free** | **One command** | **23 MCP tools** | **`--ci --diff`** |
|
|
51
51
|
|
|
52
52
|
## Features
|
|
53
53
|
|
|
@@ -102,7 +102,7 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
|
|
|
102
102
|
|
|
103
103
|
### 🛡️ Migration Safety Check
|
|
104
104
|
- Analyze a migration SQL file for risks before running it
|
|
105
|
-
- Detects: `CREATE INDEX` without `CONCURRENTLY` (lock risk), `ADD COLUMN NOT NULL` without `DEFAULT`, `DROP TABLE`, `TRUNCATE`, `DELETE`/`UPDATE` without `WHERE`
|
|
105
|
+
- Detects: `CREATE INDEX` without `CONCURRENTLY` (lock risk), `ADD COLUMN NOT NULL` without `DEFAULT`, `ALTER COLUMN TYPE` (full table rewrite), `DROP COLUMN` (app breakage risk), `ADD CONSTRAINT` without `NOT VALID` (full table scan), `CREATE INDEX CONCURRENTLY` inside a transaction (runtime failure), `DROP TABLE`, `TRUNCATE`, `DELETE`/`UPDATE` without `WHERE`
|
|
106
106
|
- Dynamic checks: connects to DB to verify referenced tables exist, estimates lock time based on actual row counts
|
|
107
107
|
- CI-ready: `--ci` flag emits `::error::` / `::warning::` GitHub Actions annotations
|
|
108
108
|
|
|
@@ -113,12 +113,12 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
|
|
|
113
113
|
|
|
114
114
|
### 🔄 Multi-Env Diff
|
|
115
115
|
- Compare schema and health between two PostgreSQL environments (local vs staging, staging vs prod)
|
|
116
|
-
- Detects: missing/extra tables, missing/extra columns, column type mismatches, missing/extra indexes
|
|
116
|
+
- Detects: missing/extra tables, missing/extra columns, column type mismatches, missing/extra indexes, **foreign key and CHECK constraints**, **enum type differences**
|
|
117
117
|
- `--health` flag adds health score comparison and unique issues per environment
|
|
118
118
|
- `pg_dash_compare_env` MCP tool: ask your AI "what's different between local and staging?"
|
|
119
119
|
|
|
120
120
|
### 🤖 MCP Server
|
|
121
|
-
-
|
|
121
|
+
- 23 tools for AI agent integration
|
|
122
122
|
- `pg-dash-mcp postgres://...` — works with Claude, Cursor, etc.
|
|
123
123
|
|
|
124
124
|
### 🖥️ CLI
|
|
@@ -202,7 +202,7 @@ pg-dash-mcp postgres://user:pass@host/db
|
|
|
202
202
|
PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
### Available Tools (
|
|
205
|
+
### Available Tools (23)
|
|
206
206
|
|
|
207
207
|
| Tool | Description |
|
|
208
208
|
|------|-------------|
|
|
@@ -224,6 +224,11 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
|
|
|
224
224
|
| `pg_dash_analyze_query` | Deep EXPLAIN analysis with automatic index suggestions |
|
|
225
225
|
| `pg_dash_query_regressions` | Detect queries that degraded >50% vs historical baseline |
|
|
226
226
|
| `pg_dash_compare_env` | Compare schema and health between two database environments |
|
|
227
|
+
| `pg_dash_unused_indexes` | Find unused indexes that waste space and slow down writes |
|
|
228
|
+
| `pg_dash_bloat` | Detect table bloat (dead tuples) that slow down queries |
|
|
229
|
+
| `pg_dash_autovacuum` | Check autovacuum health — which tables are stale or never vacuumed |
|
|
230
|
+
| `pg_dash_locks` | Show active lock waits and long-running blocking queries |
|
|
231
|
+
| `pg_dash_config_check` | Audit PostgreSQL configuration and get tuning recommendations |
|
|
227
232
|
|
|
228
233
|
## MCP Setup
|
|
229
234
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# pg-dash
|
|
4
4
|
|
|
5
|
-
**AI 原生的 PostgreSQL 健康检查工具。** 一条命令审计数据库,
|
|
5
|
+
**AI 原生的 PostgreSQL 健康检查工具。** 一条命令审计数据库,23 个 MCP 工具让 AI 帮你优化,CI 集成自动检查。
|
|
6
6
|
|
|
7
7
|
不是又一个监控面板 —— pg-dash 是为 **AI 编程工作流** 设计的:
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ Dashboard 需要时可以用。但真正的核心能力在 CLI、MCP 和 CI。
|
|
|
47
47
|
| pganalyze | $149+/月 | SaaS 注册 | ❌ | ❌ |
|
|
48
48
|
| Grafana+Prometheus | 免费 | 配置 3 个服务 | ❌ | ❌ |
|
|
49
49
|
| pgAdmin | 免费 | 界面复杂 | ❌ | ❌ |
|
|
50
|
-
| **pg-dash** | **免费** | **一条命令** | **
|
|
50
|
+
| **pg-dash** | **免费** | **一条命令** | **23 个 MCP 工具** | **`--ci --diff`** |
|
|
51
51
|
|
|
52
52
|
## 功能
|
|
53
53
|
|
|
@@ -102,7 +102,7 @@ Dashboard 需要时可以用。但真正的核心能力在 CLI、MCP 和 CI。
|
|
|
102
102
|
|
|
103
103
|
### 🛡️ Migration 安全检查
|
|
104
104
|
- 执行迁移前分析 SQL 文件的风险
|
|
105
|
-
- 检测:`CREATE INDEX`(无 `CONCURRENTLY` 会锁表)、`ADD COLUMN NOT NULL`(无 DEFAULT 会失败)、`DROP TABLE`、`TRUNCATE`、无 WHERE 的 `DELETE`/`UPDATE`
|
|
105
|
+
- 检测:`CREATE INDEX`(无 `CONCURRENTLY` 会锁表)、`ADD COLUMN NOT NULL`(无 DEFAULT 会失败)、`ALTER COLUMN TYPE`(全表重写)、`DROP COLUMN`(可能 break 代码)、`ADD CONSTRAINT` 无 `NOT VALID`(全表扫描验证)、`CREATE INDEX CONCURRENTLY` 在事务内(运行时必然失败)、`DROP TABLE`、`TRUNCATE`、无 WHERE 的 `DELETE`/`UPDATE`
|
|
106
106
|
- 动态检查:连接数据库验证被引用表是否存在,根据实际行数估算锁表时间
|
|
107
107
|
- CI 友好:`--ci` 输出 `::error::` / `::warning::` GitHub Actions 注解
|
|
108
108
|
|
|
@@ -113,12 +113,12 @@ Dashboard 需要时可以用。但真正的核心能力在 CLI、MCP 和 CI。
|
|
|
113
113
|
|
|
114
114
|
### 🔄 多环境对比
|
|
115
115
|
- 对比两个 PostgreSQL 环境的 Schema 和健康状态(本地 vs 预发、预发 vs 生产)
|
|
116
|
-
-
|
|
116
|
+
- 检测:缺失/多余的表、缺失/多余的列、列类型不匹配、缺失/多余的索引、**外键和 CHECK 约束差异**、**枚举类型差异**
|
|
117
117
|
- `--health` 参数额外对比健康分和各环境独有的问题
|
|
118
118
|
- `pg_dash_compare_env` MCP 工具:直接问 AI "本地和预发有什么差异?"
|
|
119
119
|
|
|
120
120
|
### 🤖 MCP Server
|
|
121
|
-
-
|
|
121
|
+
- 23 个工具,支持 AI Agent 集成
|
|
122
122
|
- `pg-dash-mcp postgres://...` —— 可配合 Claude、Cursor 等使用
|
|
123
123
|
|
|
124
124
|
### 🖥️ CLI
|
|
@@ -202,7 +202,7 @@ pg-dash-mcp postgres://user:pass@host/db
|
|
|
202
202
|
PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
### 可用工具(
|
|
205
|
+
### 可用工具(23 个)
|
|
206
206
|
|
|
207
207
|
| 工具 | 描述 |
|
|
208
208
|
|------|------|
|
|
@@ -224,6 +224,11 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
|
|
|
224
224
|
| `pg_dash_analyze_query` | 深度 EXPLAIN 分析,自动生成索引建议 |
|
|
225
225
|
| `pg_dash_query_regressions` | 检测比历史基线慢超过 50% 的查询 |
|
|
226
226
|
| `pg_dash_compare_env` | 对比两个数据库环境的 Schema 和健康状态 |
|
|
227
|
+
| `pg_dash_unused_indexes` | 发现从未被使用的索引(浪费空间、拖慢写入) |
|
|
228
|
+
| `pg_dash_bloat` | 检测表膨胀(dead tuples 过多) |
|
|
229
|
+
| `pg_dash_autovacuum` | Autovacuum 健康状态——哪些表长期未 vacuum |
|
|
230
|
+
| `pg_dash_locks` | 显示活跃锁等待链和长时间阻塞的查询 |
|
|
231
|
+
| `pg_dash_config_check` | 审计 PostgreSQL 配置,给出调优建议 |
|
|
227
232
|
|
|
228
233
|
## MCP 配置
|
|
229
234
|
|
package/dist/cli.js
CHANGED
|
@@ -1381,6 +1381,33 @@ function staticCheck(sql) {
|
|
|
1381
1381
|
tableName: table
|
|
1382
1382
|
});
|
|
1383
1383
|
}
|
|
1384
|
+
const renameTableRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(\w+)\s+RENAME\s+TO\s+(\w+)/gi;
|
|
1385
|
+
while ((m = renameTableRe.exec(sql)) !== null) {
|
|
1386
|
+
const oldName = m[1];
|
|
1387
|
+
const newName = m[2];
|
|
1388
|
+
issues.push({
|
|
1389
|
+
severity: "warning",
|
|
1390
|
+
code: "RENAME_TABLE",
|
|
1391
|
+
message: `Renaming table "${oldName}" to "${newName}" breaks application code referencing the old name`,
|
|
1392
|
+
suggestion: "Deploy application code that handles both names before renaming, or use a view with the old name after renaming.",
|
|
1393
|
+
lineNumber: findLineNumber(sql, m.index),
|
|
1394
|
+
tableName: oldName
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
const renameColumnRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(\w+)\s+RENAME\s+COLUMN\s+(\w+)\s+TO\s+(\w+)/gi;
|
|
1398
|
+
while ((m = renameColumnRe.exec(sql)) !== null) {
|
|
1399
|
+
const table = m[1];
|
|
1400
|
+
const oldCol = m[2];
|
|
1401
|
+
const newCol = m[3];
|
|
1402
|
+
issues.push({
|
|
1403
|
+
severity: "warning",
|
|
1404
|
+
code: "RENAME_COLUMN",
|
|
1405
|
+
message: `Renaming column "${oldCol}" to "${newCol}" on table "${table}" breaks application code referencing the old column name`,
|
|
1406
|
+
suggestion: "Add new column, backfill data, update application to use new column, then drop old column (expand/contract pattern).",
|
|
1407
|
+
lineNumber: findLineNumber(sql, m.index),
|
|
1408
|
+
tableName: table
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1384
1411
|
const addConRe = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w."]+)\s+ADD\s+CONSTRAINT\b[^;]*(;|$)/gi;
|
|
1385
1412
|
while ((m = addConRe.exec(sql)) !== null) {
|
|
1386
1413
|
const fragment = m[0];
|
|
@@ -1562,7 +1589,7 @@ async function fetchColumns(pool) {
|
|
|
1562
1589
|
}
|
|
1563
1590
|
async function fetchIndexes(pool) {
|
|
1564
1591
|
const res = await pool.query(`
|
|
1565
|
-
SELECT tablename, indexname
|
|
1592
|
+
SELECT tablename, indexname, indexdef
|
|
1566
1593
|
FROM pg_indexes
|
|
1567
1594
|
WHERE schemaname = 'public' AND indexname NOT LIKE '%_pkey'
|
|
1568
1595
|
ORDER BY tablename, indexname
|
|
@@ -1603,6 +1630,8 @@ function diffColumns(sourceCols, targetCols, commonTables) {
|
|
|
1603
1630
|
const missingColumns = [];
|
|
1604
1631
|
const extraColumns = [];
|
|
1605
1632
|
const typeDiffs = [];
|
|
1633
|
+
const nullableDiffs = [];
|
|
1634
|
+
const defaultDiffs = [];
|
|
1606
1635
|
for (const [colName, srcInfo] of srcMap) {
|
|
1607
1636
|
if (!tgtMap.has(colName)) {
|
|
1608
1637
|
missingColumns.push(srcInfo);
|
|
@@ -1611,6 +1640,12 @@ function diffColumns(sourceCols, targetCols, commonTables) {
|
|
|
1611
1640
|
if (srcInfo.type !== tgtInfo.type) {
|
|
1612
1641
|
typeDiffs.push({ column: colName, sourceType: srcInfo.type, targetType: tgtInfo.type });
|
|
1613
1642
|
}
|
|
1643
|
+
if (srcInfo.nullable !== tgtInfo.nullable) {
|
|
1644
|
+
nullableDiffs.push({ column: colName, sourceNullable: srcInfo.nullable, targetNullable: tgtInfo.nullable });
|
|
1645
|
+
}
|
|
1646
|
+
if ((srcInfo.default ?? null) !== (tgtInfo.default ?? null)) {
|
|
1647
|
+
defaultDiffs.push({ column: colName, sourceDefault: srcInfo.default ?? null, targetDefault: tgtInfo.default ?? null });
|
|
1648
|
+
}
|
|
1614
1649
|
}
|
|
1615
1650
|
}
|
|
1616
1651
|
for (const [colName, tgtInfo] of tgtMap) {
|
|
@@ -1618,8 +1653,8 @@ function diffColumns(sourceCols, targetCols, commonTables) {
|
|
|
1618
1653
|
extraColumns.push(tgtInfo);
|
|
1619
1654
|
}
|
|
1620
1655
|
}
|
|
1621
|
-
if (missingColumns.length > 0 || extraColumns.length > 0 || typeDiffs.length > 0) {
|
|
1622
|
-
diffs.push({ table, missingColumns, extraColumns, typeDiffs });
|
|
1656
|
+
if (missingColumns.length > 0 || extraColumns.length > 0 || typeDiffs.length > 0 || nullableDiffs.length > 0 || defaultDiffs.length > 0) {
|
|
1657
|
+
diffs.push({ table, missingColumns, extraColumns, typeDiffs, nullableDiffs, defaultDiffs });
|
|
1623
1658
|
}
|
|
1624
1659
|
}
|
|
1625
1660
|
return diffs;
|
|
@@ -1627,8 +1662,8 @@ function diffColumns(sourceCols, targetCols, commonTables) {
|
|
|
1627
1662
|
function groupIndexesByTable(indexes) {
|
|
1628
1663
|
const map = /* @__PURE__ */ new Map();
|
|
1629
1664
|
for (const idx of indexes) {
|
|
1630
|
-
if (!map.has(idx.tablename)) map.set(idx.tablename, /* @__PURE__ */ new
|
|
1631
|
-
map.get(idx.tablename).
|
|
1665
|
+
if (!map.has(idx.tablename)) map.set(idx.tablename, /* @__PURE__ */ new Map());
|
|
1666
|
+
map.get(idx.tablename).set(idx.indexname, idx.indexdef);
|
|
1632
1667
|
}
|
|
1633
1668
|
return map;
|
|
1634
1669
|
}
|
|
@@ -1642,12 +1677,21 @@ function diffIndexes(sourceIdxs, targetIdxs, commonTables) {
|
|
|
1642
1677
|
]);
|
|
1643
1678
|
for (const table of allTables) {
|
|
1644
1679
|
if (!commonTables.includes(table)) continue;
|
|
1645
|
-
const
|
|
1646
|
-
const
|
|
1647
|
-
const missingIndexes = [...
|
|
1648
|
-
const extraIndexes = [...
|
|
1649
|
-
|
|
1650
|
-
|
|
1680
|
+
const srcMap = srcByTable.get(table) ?? /* @__PURE__ */ new Map();
|
|
1681
|
+
const tgtMap = tgtByTable.get(table) ?? /* @__PURE__ */ new Map();
|
|
1682
|
+
const missingIndexes = [...srcMap.keys()].filter((i) => !tgtMap.has(i));
|
|
1683
|
+
const extraIndexes = [...tgtMap.keys()].filter((i) => !srcMap.has(i));
|
|
1684
|
+
const modifiedIndexes = [];
|
|
1685
|
+
for (const [name, srcDef] of srcMap) {
|
|
1686
|
+
if (tgtMap.has(name)) {
|
|
1687
|
+
const tgtDef = tgtMap.get(name);
|
|
1688
|
+
if (srcDef !== tgtDef) {
|
|
1689
|
+
modifiedIndexes.push({ name, sourceDef: srcDef, targetDef: tgtDef });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (missingIndexes.length > 0 || extraIndexes.length > 0 || modifiedIndexes.length > 0) {
|
|
1694
|
+
diffs.push({ table, missingIndexes, extraIndexes, modifiedIndexes });
|
|
1651
1695
|
}
|
|
1652
1696
|
}
|
|
1653
1697
|
return diffs;
|
|
@@ -1655,10 +1699,10 @@ function diffIndexes(sourceIdxs, targetIdxs, commonTables) {
|
|
|
1655
1699
|
function countSchemaDrifts(schema) {
|
|
1656
1700
|
let n = schema.missingTables.length + schema.extraTables.length;
|
|
1657
1701
|
for (const cd of schema.columnDiffs) {
|
|
1658
|
-
n += cd.missingColumns.length + cd.extraColumns.length + cd.typeDiffs.length;
|
|
1702
|
+
n += cd.missingColumns.length + cd.extraColumns.length + cd.typeDiffs.length + cd.nullableDiffs.length + cd.defaultDiffs.length;
|
|
1659
1703
|
}
|
|
1660
1704
|
for (const id of schema.indexDiffs) {
|
|
1661
|
-
n += id.missingIndexes.length + id.extraIndexes.length;
|
|
1705
|
+
n += id.missingIndexes.length + id.extraIndexes.length + id.modifiedIndexes.length;
|
|
1662
1706
|
}
|
|
1663
1707
|
n += (schema.constraintDiffs ?? []).length;
|
|
1664
1708
|
n += (schema.enumDiffs ?? []).length;
|
|
@@ -1795,8 +1839,31 @@ function formatTextDiff(result) {
|
|
|
1795
1839
|
lines.push(` ~ column type differences:`);
|
|
1796
1840
|
lines.push(...typeChanges);
|
|
1797
1841
|
}
|
|
1842
|
+
const nullableChanges = [];
|
|
1843
|
+
const defaultChanges = [];
|
|
1844
|
+
for (const cd of schema.columnDiffs) {
|
|
1845
|
+
for (const nd of cd.nullableDiffs) {
|
|
1846
|
+
const src = nd.sourceNullable ? "nullable" : "NOT NULL";
|
|
1847
|
+
const tgt = nd.targetNullable ? "nullable" : "NOT NULL";
|
|
1848
|
+
nullableChanges.push(` ${cd.table}.${nd.column}: source=${src} \u2192 target=${tgt}`);
|
|
1849
|
+
}
|
|
1850
|
+
for (const dd of cd.defaultDiffs) {
|
|
1851
|
+
const src = dd.sourceDefault ?? "(none)";
|
|
1852
|
+
const tgt = dd.targetDefault ?? "(none)";
|
|
1853
|
+
defaultChanges.push(` ${cd.table}.${dd.column}: source=${src} \u2192 target=${tgt}`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (nullableChanges.length > 0) {
|
|
1857
|
+
lines.push(` ~ nullable differences:`);
|
|
1858
|
+
lines.push(...nullableChanges);
|
|
1859
|
+
}
|
|
1860
|
+
if (defaultChanges.length > 0) {
|
|
1861
|
+
lines.push(` ~ default differences:`);
|
|
1862
|
+
lines.push(...defaultChanges);
|
|
1863
|
+
}
|
|
1798
1864
|
const missingIdxs = [];
|
|
1799
1865
|
const extraIdxs = [];
|
|
1866
|
+
const modifiedIdxs = [];
|
|
1800
1867
|
for (const id of schema.indexDiffs) {
|
|
1801
1868
|
for (const idx of id.missingIndexes) {
|
|
1802
1869
|
missingIdxs.push(` ${id.table}: ${idx}`);
|
|
@@ -1804,6 +1871,9 @@ function formatTextDiff(result) {
|
|
|
1804
1871
|
for (const idx of id.extraIndexes) {
|
|
1805
1872
|
extraIdxs.push(` ${id.table}: ${idx}`);
|
|
1806
1873
|
}
|
|
1874
|
+
for (const mi of id.modifiedIndexes) {
|
|
1875
|
+
modifiedIdxs.push(` ${id.table}: ${mi.name} source="${mi.sourceDef}" \u2192 target="${mi.targetDef}"`);
|
|
1876
|
+
}
|
|
1807
1877
|
}
|
|
1808
1878
|
if (missingIdxs.length > 0) {
|
|
1809
1879
|
lines.push(` \u2717 target missing indexes:`);
|
|
@@ -1813,6 +1883,10 @@ function formatTextDiff(result) {
|
|
|
1813
1883
|
lines.push(` \u26A0 target has extra indexes:`);
|
|
1814
1884
|
lines.push(...extraIdxs);
|
|
1815
1885
|
}
|
|
1886
|
+
if (modifiedIdxs.length > 0) {
|
|
1887
|
+
lines.push(` ~ index definition differences:`);
|
|
1888
|
+
lines.push(...modifiedIdxs);
|
|
1889
|
+
}
|
|
1816
1890
|
const missingConstraints = (schema.constraintDiffs ?? []).filter((c) => c.type === "missing");
|
|
1817
1891
|
const extraConstraints = (schema.constraintDiffs ?? []).filter((c) => c.type === "extra");
|
|
1818
1892
|
const modifiedConstraints = (schema.constraintDiffs ?? []).filter((c) => c.type === "modified");
|
|
@@ -1849,7 +1923,7 @@ function formatTextDiff(result) {
|
|
|
1849
1923
|
lines.push(` ~ enum differences:`);
|
|
1850
1924
|
for (const e of modifiedEnums) lines.push(` ${e.detail}`);
|
|
1851
1925
|
}
|
|
1852
|
-
const noSchemaChanges = schema.missingTables.length === 0 && schema.extraTables.length === 0 && schema.columnDiffs.length === 0 && schema.indexDiffs.length === 0 && (schema.constraintDiffs ?? []).length === 0 && (schema.enumDiffs ?? []).length === 0;
|
|
1926
|
+
const noSchemaChanges = schema.missingTables.length === 0 && schema.extraTables.length === 0 && schema.columnDiffs.length === 0 && schema.indexDiffs.length === 0 && (schema.constraintDiffs ?? []).length === 0 && (schema.enumDiffs ?? []).length === 0 && nullableChanges.length === 0 && defaultChanges.length === 0 && modifiedIdxs.length === 0;
|
|
1853
1927
|
if (noSchemaChanges) {
|
|
1854
1928
|
lines.push(` \u2713 Schemas are identical`);
|
|
1855
1929
|
}
|
|
@@ -1900,14 +1974,33 @@ function formatMdDiff(result) {
|
|
|
1900
1974
|
if (missingColItems.length > 0) rows.push([`\u274C Missing columns`, missingColItems.join(", ")]);
|
|
1901
1975
|
if (extraColItems.length > 0) rows.push([`\u26A0\uFE0F Extra columns`, extraColItems.join(", ")]);
|
|
1902
1976
|
if (typeItems.length > 0) rows.push([`~ Type differences`, typeItems.join(", ")]);
|
|
1977
|
+
const nullableItems = [];
|
|
1978
|
+
const defaultItems = [];
|
|
1979
|
+
for (const cd of schema.columnDiffs) {
|
|
1980
|
+
for (const nd of cd.nullableDiffs) {
|
|
1981
|
+
const src = nd.sourceNullable ? "nullable" : "NOT NULL";
|
|
1982
|
+
const tgt = nd.targetNullable ? "nullable" : "NOT NULL";
|
|
1983
|
+
nullableItems.push(`\`${cd.table}.${nd.column}\` (${src}\u2192${tgt})`);
|
|
1984
|
+
}
|
|
1985
|
+
for (const dd of cd.defaultDiffs) {
|
|
1986
|
+
const src = dd.sourceDefault ?? "(none)";
|
|
1987
|
+
const tgt = dd.targetDefault ?? "(none)";
|
|
1988
|
+
defaultItems.push(`\`${cd.table}.${dd.column}\` (${src}\u2192${tgt})`);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (nullableItems.length > 0) rows.push([`~ Nullable differences`, nullableItems.join(", ")]);
|
|
1992
|
+
if (defaultItems.length > 0) rows.push([`~ Default differences`, defaultItems.join(", ")]);
|
|
1903
1993
|
const missingIdxItems = [];
|
|
1904
1994
|
const extraIdxItems = [];
|
|
1995
|
+
const modifiedIdxItems = [];
|
|
1905
1996
|
for (const id of schema.indexDiffs) {
|
|
1906
1997
|
for (const idx of id.missingIndexes) missingIdxItems.push(`\`${id.table}.${idx}\``);
|
|
1907
1998
|
for (const idx of id.extraIndexes) extraIdxItems.push(`\`${id.table}.${idx}\``);
|
|
1999
|
+
for (const mi of id.modifiedIndexes) modifiedIdxItems.push(`\`${id.table}.${mi.name}\``);
|
|
1908
2000
|
}
|
|
1909
2001
|
if (missingIdxItems.length > 0) rows.push([`\u274C Missing indexes`, missingIdxItems.join(", ")]);
|
|
1910
2002
|
if (extraIdxItems.length > 0) rows.push([`\u26A0\uFE0F Extra indexes`, extraIdxItems.join(", ")]);
|
|
2003
|
+
if (modifiedIdxItems.length > 0) rows.push([`~ Modified indexes`, modifiedIdxItems.join(", ")]);
|
|
1911
2004
|
const missingConItems = (schema.constraintDiffs ?? []).filter((c) => c.type === "missing").map((c) => c.detail);
|
|
1912
2005
|
const extraConItems = (schema.constraintDiffs ?? []).filter((c) => c.type === "extra").map((c) => c.detail);
|
|
1913
2006
|
const modConItems = (schema.constraintDiffs ?? []).filter((c) => c.type === "modified").map((c) => c.detail);
|
|
@@ -3057,12 +3150,23 @@ async function analyzeExplainPlan(explainJson, pool) {
|
|
|
3057
3150
|
if (pool) {
|
|
3058
3151
|
existingIndexCols = await getExistingIndexColumns(pool, scan.table);
|
|
3059
3152
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3153
|
+
const uncoveredCols = cols.filter(
|
|
3154
|
+
(col) => !existingIndexCols.some((idxCols) => idxCols.length > 0 && idxCols[0] === col)
|
|
3155
|
+
);
|
|
3156
|
+
if (uncoveredCols.length === 0) continue;
|
|
3157
|
+
const benefit = rateBenefit(scan.rowCount);
|
|
3158
|
+
if (uncoveredCols.length >= 2) {
|
|
3159
|
+
const idxName = `idx_${scan.table}_${uncoveredCols.join("_")}`;
|
|
3160
|
+
const sql = `CREATE INDEX CONCURRENTLY ${idxName} ON ${scan.table} (${uncoveredCols.join(", ")})`;
|
|
3161
|
+
result.missingIndexes.push({
|
|
3162
|
+
table: scan.table,
|
|
3163
|
+
columns: uncoveredCols,
|
|
3164
|
+
reason: `Seq Scan with multi-column filter (${uncoveredCols.join(", ")}) on ${fmtRows(scan.rowCount)} rows \u2014 composite index preferred`,
|
|
3165
|
+
sql,
|
|
3166
|
+
estimatedBenefit: benefit
|
|
3167
|
+
});
|
|
3168
|
+
} else {
|
|
3169
|
+
const col = uncoveredCols[0];
|
|
3066
3170
|
const idxName = `idx_${scan.table}_${col}`;
|
|
3067
3171
|
const sql = `CREATE INDEX CONCURRENTLY ${idxName} ON ${scan.table} (${col})`;
|
|
3068
3172
|
result.missingIndexes.push({
|