@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/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 schema = { missingTables, extraTables, columnDiffs, indexDiffs };
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({