@indiekitai/pg-dash 0.3.6 → 0.3.8
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 +51 -12
- package/README.zh-CN.md +52 -13
- package/dist/cli.js +985 -812
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +258 -4
- package/dist/mcp.js.map +1 -1
- package/package.json +3 -3
package/dist/mcp.js
CHANGED
|
@@ -63,6 +63,30 @@ async function getTables(pool2) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// src/server/queries/schema.ts
|
|
66
|
+
async function getSchemaTables(pool2) {
|
|
67
|
+
const client = await pool2.connect();
|
|
68
|
+
try {
|
|
69
|
+
const r = await client.query(`
|
|
70
|
+
SELECT
|
|
71
|
+
c.relname AS name,
|
|
72
|
+
n.nspname AS schema,
|
|
73
|
+
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
|
|
74
|
+
pg_total_relation_size(c.oid) AS total_size_bytes,
|
|
75
|
+
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
|
|
76
|
+
pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid)) AS index_size,
|
|
77
|
+
s.n_live_tup AS row_count,
|
|
78
|
+
obj_description(c.oid) AS description
|
|
79
|
+
FROM pg_class c
|
|
80
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
81
|
+
LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid
|
|
82
|
+
WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
83
|
+
ORDER BY pg_total_relation_size(c.oid) DESC
|
|
84
|
+
`);
|
|
85
|
+
return r.rows;
|
|
86
|
+
} finally {
|
|
87
|
+
client.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
66
90
|
async function getSchemaTableDetail(pool2, tableName) {
|
|
67
91
|
const client = await pool2.connect();
|
|
68
92
|
try {
|
|
@@ -160,6 +184,26 @@ async function getSchemaTableDetail(pool2, tableName) {
|
|
|
160
184
|
client.release();
|
|
161
185
|
}
|
|
162
186
|
}
|
|
187
|
+
async function getSchemaEnums(pool2) {
|
|
188
|
+
const client = await pool2.connect();
|
|
189
|
+
try {
|
|
190
|
+
const r = await client.query(`
|
|
191
|
+
SELECT
|
|
192
|
+
t.typname AS name,
|
|
193
|
+
n.nspname AS schema,
|
|
194
|
+
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
|
|
195
|
+
FROM pg_type t
|
|
196
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
197
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
198
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
199
|
+
GROUP BY t.typname, n.nspname
|
|
200
|
+
ORDER BY t.typname
|
|
201
|
+
`);
|
|
202
|
+
return r.rows;
|
|
203
|
+
} finally {
|
|
204
|
+
client.release();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
163
207
|
|
|
164
208
|
// src/server/queries/activity.ts
|
|
165
209
|
async function getActivity(pool2) {
|
|
@@ -989,6 +1033,140 @@ function diffSnapshots(prev, current) {
|
|
|
989
1033
|
|
|
990
1034
|
// src/server/env-differ.ts
|
|
991
1035
|
import { Pool } from "pg";
|
|
1036
|
+
|
|
1037
|
+
// src/server/schema-diff.ts
|
|
1038
|
+
function diffSnapshots2(oldSnap, newSnap) {
|
|
1039
|
+
const changes = [];
|
|
1040
|
+
const oldTableMap = new Map(oldSnap.tables.map((t) => [`${t.schema}.${t.name}`, t]));
|
|
1041
|
+
const newTableMap = new Map(newSnap.tables.map((t) => [`${t.schema}.${t.name}`, t]));
|
|
1042
|
+
for (const [key, t] of newTableMap) {
|
|
1043
|
+
if (!oldTableMap.has(key)) {
|
|
1044
|
+
changes.push({ change_type: "added", object_type: "table", table_name: key, detail: `Table ${key} added` });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const [key] of oldTableMap) {
|
|
1048
|
+
if (!newTableMap.has(key)) {
|
|
1049
|
+
changes.push({ change_type: "removed", object_type: "table", table_name: key, detail: `Table ${key} removed` });
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
for (const [key, newTable] of newTableMap) {
|
|
1053
|
+
const oldTable = oldTableMap.get(key);
|
|
1054
|
+
if (!oldTable) continue;
|
|
1055
|
+
const oldCols = new Map(oldTable.columns.map((c) => [c.name, c]));
|
|
1056
|
+
const newCols = new Map(newTable.columns.map((c) => [c.name, c]));
|
|
1057
|
+
for (const [name, col] of newCols) {
|
|
1058
|
+
const oldCol = oldCols.get(name);
|
|
1059
|
+
if (!oldCol) {
|
|
1060
|
+
changes.push({ change_type: "added", object_type: "column", table_name: key, detail: `Column ${name} added (${col.type})` });
|
|
1061
|
+
} else {
|
|
1062
|
+
if (oldCol.type !== col.type) {
|
|
1063
|
+
changes.push({ change_type: "modified", object_type: "column", table_name: key, detail: `Column ${name} type changed: ${oldCol.type} \u2192 ${col.type}` });
|
|
1064
|
+
}
|
|
1065
|
+
if (oldCol.nullable !== col.nullable) {
|
|
1066
|
+
changes.push({ change_type: "modified", object_type: "column", table_name: key, detail: `Column ${name} nullable changed: ${oldCol.nullable} \u2192 ${col.nullable}` });
|
|
1067
|
+
}
|
|
1068
|
+
if (oldCol.default_value !== col.default_value) {
|
|
1069
|
+
changes.push({ change_type: "modified", object_type: "column", table_name: key, detail: `Column ${name} default changed: ${oldCol.default_value ?? "NULL"} \u2192 ${col.default_value ?? "NULL"}` });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
for (const name of oldCols.keys()) {
|
|
1074
|
+
if (!newCols.has(name)) {
|
|
1075
|
+
changes.push({ change_type: "removed", object_type: "column", table_name: key, detail: `Column ${name} removed` });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const oldIdx = new Map(oldTable.indexes.map((i) => [i.name, i]));
|
|
1079
|
+
const newIdx = new Map(newTable.indexes.map((i) => [i.name, i]));
|
|
1080
|
+
for (const [name, idx] of newIdx) {
|
|
1081
|
+
if (!oldIdx.has(name)) {
|
|
1082
|
+
changes.push({ change_type: "added", object_type: "index", table_name: key, detail: `Index ${name} added` });
|
|
1083
|
+
} else if (oldIdx.get(name).definition !== idx.definition) {
|
|
1084
|
+
changes.push({ change_type: "modified", object_type: "index", table_name: key, detail: `Index ${name} definition changed` });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
for (const name of oldIdx.keys()) {
|
|
1088
|
+
if (!newIdx.has(name)) {
|
|
1089
|
+
changes.push({ change_type: "removed", object_type: "index", table_name: key, detail: `Index ${name} removed` });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const oldCon = new Map(oldTable.constraints.map((c) => [c.name, c]));
|
|
1093
|
+
const newCon = new Map(newTable.constraints.map((c) => [c.name, c]));
|
|
1094
|
+
for (const [name, con] of newCon) {
|
|
1095
|
+
if (!oldCon.has(name)) {
|
|
1096
|
+
changes.push({ change_type: "added", object_type: "constraint", table_name: key, detail: `Constraint ${name} added (${con.type})` });
|
|
1097
|
+
} else if (oldCon.get(name).definition !== con.definition) {
|
|
1098
|
+
changes.push({ change_type: "modified", object_type: "constraint", table_name: key, detail: `Constraint ${name} definition changed` });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
for (const name of oldCon.keys()) {
|
|
1102
|
+
if (!newCon.has(name)) {
|
|
1103
|
+
changes.push({ change_type: "removed", object_type: "constraint", table_name: key, detail: `Constraint ${name} removed` });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
const oldEnums = new Map((oldSnap.enums || []).map((e) => [`${e.schema}.${e.name}`, e]));
|
|
1108
|
+
const newEnums = new Map((newSnap.enums || []).map((e) => [`${e.schema}.${e.name}`, e]));
|
|
1109
|
+
for (const [key, en] of newEnums) {
|
|
1110
|
+
const oldEn = oldEnums.get(key);
|
|
1111
|
+
if (!oldEn) {
|
|
1112
|
+
changes.push({ change_type: "added", object_type: "enum", table_name: null, detail: `Enum ${key} added (${en.values.join(", ")})` });
|
|
1113
|
+
} else {
|
|
1114
|
+
const added = en.values.filter((v) => !oldEn.values.includes(v));
|
|
1115
|
+
const removed = oldEn.values.filter((v) => !en.values.includes(v));
|
|
1116
|
+
for (const v of added) {
|
|
1117
|
+
changes.push({ change_type: "modified", object_type: "enum", table_name: null, detail: `Enum ${key}: value '${v}' added` });
|
|
1118
|
+
}
|
|
1119
|
+
for (const v of removed) {
|
|
1120
|
+
changes.push({ change_type: "modified", object_type: "enum", table_name: null, detail: `Enum ${key}: value '${v}' removed` });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
for (const key of oldEnums.keys()) {
|
|
1125
|
+
if (!newEnums.has(key)) {
|
|
1126
|
+
changes.push({ change_type: "removed", object_type: "enum", table_name: null, detail: `Enum ${key} removed` });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return changes;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/server/schema-tracker.ts
|
|
1133
|
+
async function buildLiveSnapshot(pool2) {
|
|
1134
|
+
const tables = await getSchemaTables(pool2);
|
|
1135
|
+
const enums = await getSchemaEnums(pool2);
|
|
1136
|
+
const detailedTables = await Promise.all(
|
|
1137
|
+
tables.map(async (t) => {
|
|
1138
|
+
const detail = await getSchemaTableDetail(pool2, `${t.schema}.${t.name}`);
|
|
1139
|
+
if (!detail) return null;
|
|
1140
|
+
return {
|
|
1141
|
+
name: detail.name,
|
|
1142
|
+
schema: detail.schema,
|
|
1143
|
+
columns: detail.columns.map((c) => ({
|
|
1144
|
+
name: c.name,
|
|
1145
|
+
type: c.type,
|
|
1146
|
+
nullable: c.nullable,
|
|
1147
|
+
default_value: c.default_value
|
|
1148
|
+
})),
|
|
1149
|
+
indexes: detail.indexes.map((i) => ({
|
|
1150
|
+
name: i.name,
|
|
1151
|
+
definition: i.definition,
|
|
1152
|
+
is_unique: i.is_unique,
|
|
1153
|
+
is_primary: i.is_primary
|
|
1154
|
+
})),
|
|
1155
|
+
constraints: detail.constraints.map((c) => ({
|
|
1156
|
+
name: c.name,
|
|
1157
|
+
type: c.type,
|
|
1158
|
+
definition: c.definition
|
|
1159
|
+
}))
|
|
1160
|
+
};
|
|
1161
|
+
})
|
|
1162
|
+
);
|
|
1163
|
+
return {
|
|
1164
|
+
tables: detailedTables.filter(Boolean),
|
|
1165
|
+
enums: enums.map((e) => ({ name: e.name, schema: e.schema, values: e.values }))
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/server/env-differ.ts
|
|
992
1170
|
async function fetchTables(pool2) {
|
|
993
1171
|
const res = await pool2.query(`
|
|
994
1172
|
SELECT table_name
|
|
@@ -1107,6 +1285,8 @@ function countSchemaDrifts(schema) {
|
|
|
1107
1285
|
for (const id of schema.indexDiffs) {
|
|
1108
1286
|
n += id.missingIndexes.length + id.extraIndexes.length;
|
|
1109
1287
|
}
|
|
1288
|
+
n += (schema.constraintDiffs ?? []).length;
|
|
1289
|
+
n += (schema.enumDiffs ?? []).length;
|
|
1110
1290
|
return n;
|
|
1111
1291
|
}
|
|
1112
1292
|
async function diffEnvironments(sourceConn, targetConn, options) {
|
|
@@ -1119,22 +1299,46 @@ async function diffEnvironments(sourceConn, targetConn, options) {
|
|
|
1119
1299
|
sourceCols,
|
|
1120
1300
|
targetCols,
|
|
1121
1301
|
sourceIdxs,
|
|
1122
|
-
targetIdxs
|
|
1302
|
+
targetIdxs,
|
|
1303
|
+
sourceSnap,
|
|
1304
|
+
targetSnap
|
|
1123
1305
|
] = await Promise.all([
|
|
1124
1306
|
fetchTables(sourcePool),
|
|
1125
1307
|
fetchTables(targetPool),
|
|
1126
1308
|
fetchColumns(sourcePool),
|
|
1127
1309
|
fetchColumns(targetPool),
|
|
1128
1310
|
fetchIndexes(sourcePool),
|
|
1129
|
-
fetchIndexes(targetPool)
|
|
1311
|
+
fetchIndexes(targetPool),
|
|
1312
|
+
buildLiveSnapshot(sourcePool).catch(() => null),
|
|
1313
|
+
buildLiveSnapshot(targetPool).catch(() => null)
|
|
1130
1314
|
]);
|
|
1131
1315
|
const { missingTables, extraTables } = diffTables(sourceTables, targetTables);
|
|
1132
|
-
const sourceSet = new Set(sourceTables);
|
|
1133
1316
|
const targetSet = new Set(targetTables);
|
|
1134
1317
|
const commonTables = sourceTables.filter((t) => targetSet.has(t));
|
|
1135
1318
|
const columnDiffs = diffColumns(sourceCols, targetCols, commonTables);
|
|
1136
1319
|
const indexDiffs = diffIndexes(sourceIdxs, targetIdxs, commonTables);
|
|
1137
|
-
const
|
|
1320
|
+
const constraintDiffs = [];
|
|
1321
|
+
const enumDiffs = [];
|
|
1322
|
+
if (sourceSnap && targetSnap) {
|
|
1323
|
+
const snapChanges = diffSnapshots2(sourceSnap, targetSnap);
|
|
1324
|
+
for (const c of snapChanges) {
|
|
1325
|
+
if (c.object_type === "constraint") {
|
|
1326
|
+
constraintDiffs.push({
|
|
1327
|
+
table: c.table_name ?? null,
|
|
1328
|
+
type: c.change_type === "added" ? "extra" : c.change_type === "removed" ? "missing" : "modified",
|
|
1329
|
+
name: c.detail.split(" ")[1] ?? c.detail,
|
|
1330
|
+
detail: c.detail
|
|
1331
|
+
});
|
|
1332
|
+
} else if (c.object_type === "enum") {
|
|
1333
|
+
enumDiffs.push({
|
|
1334
|
+
type: c.change_type === "added" ? "extra" : c.change_type === "removed" ? "missing" : "modified",
|
|
1335
|
+
name: c.detail.split(" ")[1] ?? c.detail,
|
|
1336
|
+
detail: c.detail
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const schema = { missingTables, extraTables, columnDiffs, indexDiffs, constraintDiffs, enumDiffs };
|
|
1138
1342
|
const schemaDrifts = countSchemaDrifts(schema);
|
|
1139
1343
|
let health;
|
|
1140
1344
|
if (options?.includeHealth) {
|
|
@@ -1471,6 +1675,56 @@ function staticCheck(sql) {
|
|
|
1471
1675
|
lineNumber: findLineNumber(sql, m.index)
|
|
1472
1676
|
});
|
|
1473
1677
|
}
|
|
1678
|
+
const alterTypeRe = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w."]+)\s+ALTER\s+(?:COLUMN\s+)?[\w"]+\s+TYPE\b/gi;
|
|
1679
|
+
while ((m = alterTypeRe.exec(sql)) !== null) {
|
|
1680
|
+
const table = bareTable(m[1]);
|
|
1681
|
+
issues.push({
|
|
1682
|
+
severity: "warning",
|
|
1683
|
+
code: "ALTER_COLUMN_TYPE",
|
|
1684
|
+
message: "ALTER COLUMN TYPE rewrites the entire table and acquires an exclusive lock.",
|
|
1685
|
+
suggestion: "Consider using a new column + backfill + rename strategy to avoid downtime.",
|
|
1686
|
+
lineNumber: findLineNumber(sql, m.index),
|
|
1687
|
+
tableName: table
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
const dropColRe = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w."]+)\s+DROP\s+(?:COLUMN\s+)(?:IF\s+EXISTS\s+)?[\w"]+\b/gi;
|
|
1691
|
+
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1692
|
+
const table = bareTable(m[1]);
|
|
1693
|
+
issues.push({
|
|
1694
|
+
severity: "info",
|
|
1695
|
+
code: "DROP_COLUMN",
|
|
1696
|
+
message: "DROP COLUMN is safe in PostgreSQL (no table rewrite), but may break application code referencing that column.",
|
|
1697
|
+
suggestion: "Ensure no application code references this column before dropping it.",
|
|
1698
|
+
lineNumber: findLineNumber(sql, m.index),
|
|
1699
|
+
tableName: table
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
const addConRe = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w."]+)\s+ADD\s+CONSTRAINT\b[^;]*(;|$)/gi;
|
|
1703
|
+
while ((m = addConRe.exec(sql)) !== null) {
|
|
1704
|
+
const fragment = m[0];
|
|
1705
|
+
const table = bareTable(m[1]);
|
|
1706
|
+
const fragUpper = fragment.toUpperCase();
|
|
1707
|
+
if (!/\bNOT\s+VALID\b/.test(fragUpper)) {
|
|
1708
|
+
issues.push({
|
|
1709
|
+
severity: "warning",
|
|
1710
|
+
code: "ADD_CONSTRAINT_SCANS_TABLE",
|
|
1711
|
+
message: "ADD CONSTRAINT validates all existing rows and holds an exclusive lock during the scan.",
|
|
1712
|
+
suggestion: "Use ADD CONSTRAINT ... NOT VALID to skip validation, then VALIDATE CONSTRAINT in a separate transaction.",
|
|
1713
|
+
lineNumber: findLineNumber(sql, m.index),
|
|
1714
|
+
tableName: table
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const hasTransaction = /\bBEGIN\b/i.test(sql) || /\bSTART\s+TRANSACTION\b/i.test(sql);
|
|
1719
|
+
const hasConcurrently = /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+CONCURRENTLY\b/i.test(sql);
|
|
1720
|
+
if (hasTransaction && hasConcurrently) {
|
|
1721
|
+
issues.push({
|
|
1722
|
+
severity: "error",
|
|
1723
|
+
code: "CONCURRENTLY_IN_TRANSACTION",
|
|
1724
|
+
message: "CREATE INDEX CONCURRENTLY cannot run inside a transaction block. It will fail at runtime.",
|
|
1725
|
+
suggestion: "Remove the BEGIN/COMMIT wrapper, or use a migration tool that runs CONCURRENTLY outside transactions."
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1474
1728
|
const truncRe = /\bTRUNCATE\b/gi;
|
|
1475
1729
|
while ((m = truncRe.exec(sql)) !== null) {
|
|
1476
1730
|
issues.push({
|