@indiekitai/pg-toolkit 0.1.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 +128 -0
- package/dist/analyze.js +168 -0
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/cli.js +270 -0
- package/dist/index.cjs +220 -0
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +178 -0
- package/dist/mcp.js +13874 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @indiekitai/pg-toolkit
|
|
2
|
+
|
|
3
|
+
Unified CLI for all IndieKit PostgreSQL tools. One command, all PG tools.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @indiekitai/pg-toolkit
|
|
9
|
+
# or
|
|
10
|
+
npx @indiekitai/pg-toolkit <command>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### `inspect` — Schema inspection
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Full schema dump
|
|
19
|
+
pg-toolkit inspect postgresql://localhost/mydb
|
|
20
|
+
|
|
21
|
+
# Filter to specific objects
|
|
22
|
+
pg-toolkit inspect --tables postgresql://localhost/mydb
|
|
23
|
+
pg-toolkit inspect --views --functions postgresql://localhost/mydb
|
|
24
|
+
pg-toolkit inspect --enums --types postgresql://localhost/mydb
|
|
25
|
+
|
|
26
|
+
# Summary counts
|
|
27
|
+
pg-toolkit inspect --summary postgresql://localhost/mydb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Filters: `--tables`, `--views`, `--functions`, `--indexes`, `--sequences`, `--enums`, `--extensions`, `--triggers`, `--constraints`, `--schemas`, `--privileges`, `--types`, `--domains`, `--collations`, `--rls`
|
|
31
|
+
|
|
32
|
+
### `diff` — Schema diff & migration SQL
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Generate migration SQL
|
|
36
|
+
pg-toolkit diff postgresql://localhost/old postgresql://localhost/new
|
|
37
|
+
|
|
38
|
+
# Safe mode (no DROP statements)
|
|
39
|
+
pg-toolkit diff --safe postgresql://localhost/old postgresql://localhost/new
|
|
40
|
+
|
|
41
|
+
# JSON output
|
|
42
|
+
pg-toolkit diff --json postgresql://localhost/old postgresql://localhost/new
|
|
43
|
+
|
|
44
|
+
# Pipe directly to psql
|
|
45
|
+
pg-toolkit diff postgres://localhost/old postgres://localhost/new | psql postgres://localhost/old
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `top` — Activity monitor
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Interactive TUI
|
|
52
|
+
pg-toolkit top postgresql://localhost/mydb
|
|
53
|
+
|
|
54
|
+
# Custom refresh rate, hide idle
|
|
55
|
+
pg-toolkit top --refresh 1 --no-idle postgresql://localhost/mydb
|
|
56
|
+
|
|
57
|
+
# Single snapshot as JSON
|
|
58
|
+
pg-toolkit top --snapshot --json postgresql://localhost/mydb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `health` — Health checks
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pg-toolkit health postgresql://localhost/mydb
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Delegates to `pg-health` (must be installed separately via `pip install pg-health`).
|
|
68
|
+
|
|
69
|
+
### `types` — TypeScript type generation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pg-toolkit types postgresql://localhost/mydb
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Delegates to `pg2ts` (must be installed separately).
|
|
76
|
+
|
|
77
|
+
### `mcp` — Unified MCP server
|
|
78
|
+
|
|
79
|
+
Start a single MCP server that combines all PG tools:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pg-toolkit mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
MCP config for AI agents:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"pg-toolkit": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["@indiekitai/pg-toolkit", "mcp"],
|
|
93
|
+
"env": { "DATABASE_URL": "postgresql://localhost/mydb" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Available MCP tools:
|
|
100
|
+
- `inspect_schema` — Inspect database schema
|
|
101
|
+
- `diff_schemas` — Compare two databases
|
|
102
|
+
- `pg_activity` — Get activity snapshot
|
|
103
|
+
|
|
104
|
+
## Programmatic API
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { inspect, diff, PgMonitor } from '@indiekitai/pg-toolkit';
|
|
108
|
+
|
|
109
|
+
// Inspect
|
|
110
|
+
const schema = await inspect('postgresql://localhost/mydb');
|
|
111
|
+
|
|
112
|
+
// Diff
|
|
113
|
+
const result = await diff(fromUrl, toUrl, { safe: true });
|
|
114
|
+
|
|
115
|
+
// Monitor
|
|
116
|
+
const monitor = new PgMonitor({ connectionString: '...', snapshot: true, json: true });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Packages
|
|
120
|
+
|
|
121
|
+
This toolkit wraps:
|
|
122
|
+
- [@indiekitai/pg-inspect](https://github.com/indiekitai/pg-inspect) — Schema inspection
|
|
123
|
+
- [@indiekitai/pg-diff](https://github.com/indiekitai/pg-diff) — Schema diff
|
|
124
|
+
- [@indiekitai/pg-top](https://github.com/indiekitai/pg-top) — Activity monitor
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-K3NQKI34.js";
|
|
3
|
+
|
|
4
|
+
// src/analyze.ts
|
|
5
|
+
var EAV_ENTITY_PATTERNS = /^(entity|item|record|submission|object|resource|parent)[_]?id$/i;
|
|
6
|
+
var EAV_KEY_PATTERNS = /^(key|name|attribute|field|property|type|kind|label)[_]?(name|key|id)?$/i;
|
|
7
|
+
var EAV_VALUE_PATTERNS = /^(value|data|content|payload|text)$/i;
|
|
8
|
+
function detectEav(tableName, columns) {
|
|
9
|
+
const colNames = columns.map((c) => c.columnName);
|
|
10
|
+
const entityCol = colNames.find((c) => EAV_ENTITY_PATTERNS.test(c));
|
|
11
|
+
const keyCol = colNames.find((c) => EAV_KEY_PATTERNS.test(c));
|
|
12
|
+
const valueCol = colNames.find((c) => EAV_VALUE_PATTERNS.test(c));
|
|
13
|
+
if (entityCol && keyCol && valueCol) {
|
|
14
|
+
return {
|
|
15
|
+
type: "eav_pattern",
|
|
16
|
+
severity: "high",
|
|
17
|
+
table: tableName,
|
|
18
|
+
description: `Table "${tableName}" looks like an EAV table (entity=${entityCol}, key=${keyCol}, value=${valueCol}). Querying parent + this table typically causes N+1.`,
|
|
19
|
+
suggestion: `Use CTE + JSON_AGG to aggregate rows by "${entityCol}" in a single query.`,
|
|
20
|
+
sql: `WITH aggregated AS (
|
|
21
|
+
SELECT
|
|
22
|
+
"${entityCol}",
|
|
23
|
+
JSON_AGG(JSON_BUILD_OBJECT('${keyCol}', "${keyCol}", '${valueCol}', "${valueCol}")) AS attrs
|
|
24
|
+
FROM "${tableName}"
|
|
25
|
+
GROUP BY "${entityCol}"
|
|
26
|
+
)
|
|
27
|
+
SELECT p.*, COALESCE(a.attrs, '[]'::json) AS attrs
|
|
28
|
+
FROM <parent_table> p
|
|
29
|
+
LEFT JOIN aggregated a ON a."${entityCol}" = p.id;`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function detectNPlus1FromFks(fks) {
|
|
35
|
+
const parentToChildren = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const fk of fks) {
|
|
37
|
+
const list = parentToChildren.get(fk.parentTable) || [];
|
|
38
|
+
list.push(fk);
|
|
39
|
+
parentToChildren.set(fk.parentTable, list);
|
|
40
|
+
}
|
|
41
|
+
const warnings = [];
|
|
42
|
+
for (const [parentTable, children] of parentToChildren) {
|
|
43
|
+
for (const fk of children) {
|
|
44
|
+
if (fk.childTable === parentTable) continue;
|
|
45
|
+
warnings.push({
|
|
46
|
+
type: "n_plus_1_risk",
|
|
47
|
+
severity: "medium",
|
|
48
|
+
table: fk.childTable,
|
|
49
|
+
parentTable,
|
|
50
|
+
description: `"${fk.childTable}".${fk.childColumn} \u2192 "${parentTable}".${fk.parentColumn}. Loading ${parentTable} list + ${fk.childTable} details per row = N+1.`,
|
|
51
|
+
suggestion: `Use CTE + JSON_AGG to pre-aggregate "${fk.childTable}" rows per "${fk.childColumn}".`,
|
|
52
|
+
sql: `WITH child_agg AS (
|
|
53
|
+
SELECT "${fk.childColumn}", JSON_AGG(t.*) AS children
|
|
54
|
+
FROM "${fk.childTable}" t
|
|
55
|
+
GROUP BY "${fk.childColumn}"
|
|
56
|
+
)
|
|
57
|
+
SELECT p.*, COALESCE(c.children, '[]'::json) AS ${fk.childTable}
|
|
58
|
+
FROM "${parentTable}" p
|
|
59
|
+
LEFT JOIN child_agg c ON c."${fk.childColumn}" = p."${fk.parentColumn}";`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const uniqueChildren = new Set(children.map((c) => c.childTable));
|
|
63
|
+
if (uniqueChildren.size > 2) {
|
|
64
|
+
warnings.push({
|
|
65
|
+
type: "multi_fk_parent",
|
|
66
|
+
severity: "low",
|
|
67
|
+
table: parentTable,
|
|
68
|
+
description: `"${parentTable}" has ${uniqueChildren.size} child tables referencing it. Loading all children per row compounds N+1.`,
|
|
69
|
+
suggestion: `Consider multiple CTEs in a single query to fetch all child data at once.`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return warnings;
|
|
74
|
+
}
|
|
75
|
+
async function analyze(connectionString) {
|
|
76
|
+
const pg = await import("pg");
|
|
77
|
+
const Pool = pg.default?.Pool || pg.Pool;
|
|
78
|
+
const pool = new Pool({ connectionString });
|
|
79
|
+
try {
|
|
80
|
+
const dbResult = await pool.query("SELECT current_database() AS db");
|
|
81
|
+
const database = dbResult.rows[0].db;
|
|
82
|
+
const colResult = await pool.query(`
|
|
83
|
+
SELECT table_name, column_name, data_type, is_nullable = 'YES' AS is_nullable
|
|
84
|
+
FROM information_schema.columns
|
|
85
|
+
WHERE table_schema = 'public'
|
|
86
|
+
ORDER BY table_name, ordinal_position
|
|
87
|
+
`);
|
|
88
|
+
const columnsByTable = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const row of colResult.rows) {
|
|
90
|
+
const list = columnsByTable.get(row.table_name) || [];
|
|
91
|
+
list.push({
|
|
92
|
+
tableName: row.table_name,
|
|
93
|
+
columnName: row.column_name,
|
|
94
|
+
dataType: row.data_type,
|
|
95
|
+
isNullable: row.is_nullable
|
|
96
|
+
});
|
|
97
|
+
columnsByTable.set(row.table_name, list);
|
|
98
|
+
}
|
|
99
|
+
const fkResult = await pool.query(`
|
|
100
|
+
SELECT
|
|
101
|
+
tc.constraint_name,
|
|
102
|
+
tc.table_name AS child_table,
|
|
103
|
+
kcu.column_name AS child_column,
|
|
104
|
+
ccu.table_name AS parent_table,
|
|
105
|
+
ccu.column_name AS parent_column
|
|
106
|
+
FROM information_schema.table_constraints tc
|
|
107
|
+
JOIN information_schema.key_column_usage kcu
|
|
108
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
109
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
110
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
111
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'
|
|
112
|
+
`);
|
|
113
|
+
const fks = fkResult.rows.map((r) => ({
|
|
114
|
+
constraintName: r.constraint_name,
|
|
115
|
+
childTable: r.child_table,
|
|
116
|
+
childColumn: r.child_column,
|
|
117
|
+
parentTable: r.parent_table,
|
|
118
|
+
parentColumn: r.parent_column
|
|
119
|
+
}));
|
|
120
|
+
const warnings = [];
|
|
121
|
+
for (const [tableName, columns] of columnsByTable) {
|
|
122
|
+
const eav = detectEav(tableName, columns);
|
|
123
|
+
if (eav) warnings.push(eav);
|
|
124
|
+
}
|
|
125
|
+
warnings.push(...detectNPlus1FromFks(fks));
|
|
126
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
127
|
+
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
128
|
+
return {
|
|
129
|
+
database,
|
|
130
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
tablesScanned: columnsByTable.size,
|
|
132
|
+
warnings
|
|
133
|
+
};
|
|
134
|
+
} finally {
|
|
135
|
+
await pool.end();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function formatAnalyzeResult(result, json) {
|
|
139
|
+
if (json) return JSON.stringify(result, null, 2);
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push(`\u{1F50D} N+1 Analysis for "${result.database}"`);
|
|
142
|
+
lines.push(` Scanned ${result.tablesScanned} tables at ${result.analyzedAt}`);
|
|
143
|
+
lines.push("");
|
|
144
|
+
if (result.warnings.length === 0) {
|
|
145
|
+
lines.push("\u2705 No N+1 patterns detected.");
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
lines.push(`\u26A0\uFE0F Found ${result.warnings.length} potential issue(s):
|
|
149
|
+
`);
|
|
150
|
+
const severityIcon = { high: "\u{1F534}", medium: "\u{1F7E1}", low: "\u{1F535}" };
|
|
151
|
+
for (const w of result.warnings) {
|
|
152
|
+
lines.push(`${severityIcon[w.severity]} [${w.severity.toUpperCase()}] ${w.type}`);
|
|
153
|
+
lines.push(` ${w.description}`);
|
|
154
|
+
lines.push(` \u{1F4A1} ${w.suggestion}`);
|
|
155
|
+
if (w.sql) {
|
|
156
|
+
lines.push(" \u{1F4DD} Suggested SQL:");
|
|
157
|
+
for (const sqlLine of w.sql.split("\n")) {
|
|
158
|
+
lines.push(` ${sqlLine}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
analyze,
|
|
167
|
+
formatAnalyzeResult
|
|
168
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { inspect } from "@indiekitai/pg-inspect";
|
|
5
|
+
import { diff } from "@indiekitai/pg-diff";
|
|
6
|
+
import { PgMonitor } from "@indiekitai/pg-top";
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
var VERSION = "0.1.0";
|
|
9
|
+
var HELP = `
|
|
10
|
+
pg-toolkit v${VERSION} \u2014 Unified PostgreSQL toolkit
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
pg-toolkit <command> [options] [connection-string...]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
inspect Inspect database schema (tables, views, functions, etc.)
|
|
17
|
+
diff Compare two database schemas and generate migration SQL
|
|
18
|
+
top Real-time activity monitor (like top for Postgres)
|
|
19
|
+
analyze Detect N+1 query patterns and suggest CTE + JSON_AGG optimizations
|
|
20
|
+
health Run health checks (requires pg-health)
|
|
21
|
+
types Generate TypeScript types from schema (requires pg2ts)
|
|
22
|
+
mcp Start unified MCP server
|
|
23
|
+
|
|
24
|
+
Run 'pg-toolkit <command> --help' for command-specific help.
|
|
25
|
+
`.trim();
|
|
26
|
+
function getConnStr(args) {
|
|
27
|
+
const url = args.find((a) => !a.startsWith("-"));
|
|
28
|
+
return url || process.env.DATABASE_URL || "";
|
|
29
|
+
}
|
|
30
|
+
function die(msg) {
|
|
31
|
+
console.error("Error: " + msg);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
function mapToObj(map) {
|
|
35
|
+
const obj = {};
|
|
36
|
+
for (const [k, v] of map) obj[k] = serialize(v);
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
function serialize(val) {
|
|
40
|
+
if (val instanceof Map) return mapToObj(val);
|
|
41
|
+
if (Array.isArray(val)) return val.map(serialize);
|
|
42
|
+
if (val && typeof val === "object") {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [k, v] of Object.entries(val)) {
|
|
45
|
+
if (typeof v === "function" || k.startsWith("_")) continue;
|
|
46
|
+
out[k] = serialize(v);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
var INSPECT_FILTERS = [
|
|
53
|
+
"tables",
|
|
54
|
+
"views",
|
|
55
|
+
"functions",
|
|
56
|
+
"indexes",
|
|
57
|
+
"sequences",
|
|
58
|
+
"enums",
|
|
59
|
+
"extensions",
|
|
60
|
+
"triggers",
|
|
61
|
+
"constraints",
|
|
62
|
+
"schemas",
|
|
63
|
+
"privileges",
|
|
64
|
+
"types",
|
|
65
|
+
"domains",
|
|
66
|
+
"collations",
|
|
67
|
+
"rls"
|
|
68
|
+
];
|
|
69
|
+
async function cmdInspect(args) {
|
|
70
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
71
|
+
console.log(`pg-toolkit inspect \u2014 Inspect PostgreSQL schema
|
|
72
|
+
|
|
73
|
+
Usage: pg-toolkit inspect [options] <connection-string>
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--tables, --views, --functions, --indexes, --sequences,
|
|
77
|
+
--enums, --extensions, --triggers, --constraints, --schemas,
|
|
78
|
+
--privileges, --types, --domains, --collations, --rls
|
|
79
|
+
Filter to specific object types
|
|
80
|
+
|
|
81
|
+
--summary Show summary counts only
|
|
82
|
+
--json JSON output (default)`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const connStr = getConnStr(args);
|
|
86
|
+
if (!connStr) die("Connection string required. Usage: pg-toolkit inspect <connection-string>");
|
|
87
|
+
const result = await inspect(connStr);
|
|
88
|
+
const filters = INSPECT_FILTERS.filter((f) => args.includes(`--${f}`));
|
|
89
|
+
const summary = args.includes("--summary");
|
|
90
|
+
if (summary) {
|
|
91
|
+
const counts = {};
|
|
92
|
+
for (const key of Object.keys(result)) {
|
|
93
|
+
const val = result[key];
|
|
94
|
+
if (val instanceof Map) counts[key] = val.size;
|
|
95
|
+
else if (Array.isArray(val)) counts[key] = val.length;
|
|
96
|
+
}
|
|
97
|
+
console.log(JSON.stringify(counts, null, 2));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (filters.length > 0) {
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const f of filters) {
|
|
103
|
+
const val = result[f];
|
|
104
|
+
if (val) out[f] = serialize(val);
|
|
105
|
+
}
|
|
106
|
+
console.log(JSON.stringify(out, null, 2));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(JSON.stringify(serialize(result), null, 2));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function cmdDiff(args) {
|
|
112
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
113
|
+
console.log(`pg-toolkit diff \u2014 Compare two PostgreSQL schemas
|
|
114
|
+
|
|
115
|
+
Usage: pg-toolkit diff [options] <from_url> <to_url>
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
--json JSON output (machine-readable)
|
|
119
|
+
--safe Omit DROP statements`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const urls = args.filter((a) => !a.startsWith("-"));
|
|
123
|
+
if (urls.length < 2) die("Two connection strings required. Usage: pg-toolkit diff <from_url> <to_url>");
|
|
124
|
+
const jsonMode = args.includes("--json");
|
|
125
|
+
const safe = args.includes("--safe");
|
|
126
|
+
const ignoreExtVersions = args.includes("--ignore-extension-versions");
|
|
127
|
+
const result = await diff(urls[0], urls[1], { safe, ignoreExtensionVersions: ignoreExtVersions });
|
|
128
|
+
if (jsonMode) {
|
|
129
|
+
console.log(JSON.stringify(result, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
if (result.sql) {
|
|
132
|
+
console.log(result.sql);
|
|
133
|
+
} else {
|
|
134
|
+
console.log("No differences found.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function cmdTop(args) {
|
|
139
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
140
|
+
console.log(`pg-toolkit top \u2014 Real-time PostgreSQL activity monitor
|
|
141
|
+
|
|
142
|
+
Usage: pg-toolkit top [options] <connection-string>
|
|
143
|
+
|
|
144
|
+
Options:
|
|
145
|
+
--refresh <seconds> Refresh interval (default: 2)
|
|
146
|
+
--no-idle Hide idle connections
|
|
147
|
+
--snapshot Single snapshot, then exit
|
|
148
|
+
--json Output as JSON (with --snapshot)`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const connStr = getConnStr(args);
|
|
152
|
+
if (!connStr) die("Connection string required. Usage: pg-toolkit top <connection-string>");
|
|
153
|
+
let refreshInterval = 2;
|
|
154
|
+
let noIdle = false;
|
|
155
|
+
let snapshot = false;
|
|
156
|
+
let json = false;
|
|
157
|
+
for (let i = 0; i < args.length; i++) {
|
|
158
|
+
if (args[i] === "--refresh" && args[i + 1]) refreshInterval = Number(args[++i]);
|
|
159
|
+
else if (args[i] === "--no-idle") noIdle = true;
|
|
160
|
+
else if (args[i] === "--snapshot") snapshot = true;
|
|
161
|
+
else if (args[i] === "--json") json = true;
|
|
162
|
+
}
|
|
163
|
+
const monitor = new PgMonitor({
|
|
164
|
+
connectionString: connStr,
|
|
165
|
+
refreshInterval,
|
|
166
|
+
noIdle,
|
|
167
|
+
snapshot,
|
|
168
|
+
json
|
|
169
|
+
});
|
|
170
|
+
await monitor.start();
|
|
171
|
+
}
|
|
172
|
+
function cmdHealth(args) {
|
|
173
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
174
|
+
console.log(`pg-toolkit health \u2014 PostgreSQL health checks (delegates to pg-health)
|
|
175
|
+
|
|
176
|
+
Usage: pg-toolkit health <connection-string>
|
|
177
|
+
|
|
178
|
+
Requires pg-health to be installed (pip install pg-health or available in PATH).`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const connStr = getConnStr(args);
|
|
182
|
+
if (!connStr) die("Connection string required.");
|
|
183
|
+
try {
|
|
184
|
+
execFileSync("pg-health", [connStr], { stdio: "inherit" });
|
|
185
|
+
} catch {
|
|
186
|
+
console.error("pg-health not found. Install it: pip install pg-health");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function cmdTypes(args) {
|
|
191
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
192
|
+
console.log(`pg-toolkit types \u2014 Generate TypeScript types from PostgreSQL schema
|
|
193
|
+
|
|
194
|
+
Usage: pg-toolkit types <connection-string>
|
|
195
|
+
|
|
196
|
+
Requires pg2ts to be installed and available in PATH.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const connStr = getConnStr(args);
|
|
200
|
+
if (!connStr) die("Connection string required.");
|
|
201
|
+
try {
|
|
202
|
+
execFileSync("pg2ts", [connStr], { stdio: "inherit" });
|
|
203
|
+
} catch {
|
|
204
|
+
console.error("pg2ts not found. Install it or check PATH.");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function cmdAnalyze(args) {
|
|
209
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
210
|
+
console.log(`pg-toolkit analyze \u2014 Detect N+1 query patterns and suggest optimizations
|
|
211
|
+
|
|
212
|
+
Usage: pg-toolkit analyze [options] <connection-string>
|
|
213
|
+
|
|
214
|
+
Options:
|
|
215
|
+
--json JSON output (default: human-readable)
|
|
216
|
+
|
|
217
|
+
Scans table structures and foreign keys to detect:
|
|
218
|
+
- EAV (Entity-Attribute-Value) pattern tables
|
|
219
|
+
- Many-to-one relationships prone to N+1
|
|
220
|
+
- Tables with multiple child tables
|
|
221
|
+
|
|
222
|
+
Suggests CTE + JSON_AGG rewrites for each finding.`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const connStr = getConnStr(args);
|
|
226
|
+
if (!connStr) die("Connection string required. Usage: pg-toolkit analyze <connection-string>");
|
|
227
|
+
const { analyze, formatAnalyzeResult } = await import("./analyze.js");
|
|
228
|
+
const result = await analyze(connStr);
|
|
229
|
+
const jsonMode = args.includes("--json");
|
|
230
|
+
console.log(formatAnalyzeResult(result, jsonMode));
|
|
231
|
+
}
|
|
232
|
+
async function main() {
|
|
233
|
+
const args = process.argv.slice(2);
|
|
234
|
+
const command = args[0];
|
|
235
|
+
const rest = args.slice(1);
|
|
236
|
+
if (!command || command === "--help" || command === "-h") {
|
|
237
|
+
console.log(HELP);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (command === "--version" || command === "-v") {
|
|
241
|
+
console.log(VERSION);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
switch (command) {
|
|
245
|
+
case "inspect":
|
|
246
|
+
return cmdInspect(rest);
|
|
247
|
+
case "diff":
|
|
248
|
+
return cmdDiff(rest);
|
|
249
|
+
case "top":
|
|
250
|
+
return cmdTop(rest);
|
|
251
|
+
case "analyze":
|
|
252
|
+
return cmdAnalyze(rest);
|
|
253
|
+
case "health":
|
|
254
|
+
return cmdHealth(rest);
|
|
255
|
+
case "types":
|
|
256
|
+
return cmdTypes(rest);
|
|
257
|
+
case "mcp": {
|
|
258
|
+
const { startMcpServer } = await import("./mcp.js");
|
|
259
|
+
return startMcpServer();
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
console.error(`Unknown command: ${command}`);
|
|
263
|
+
console.log(HELP);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
main().catch((err) => {
|
|
268
|
+
console.error(err.message || err);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|