@indiekitai/pg-dash 0.4.6 โ 0.5.1
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 +45 -0
- package/README.zh-CN.md +8 -0
- package/dist/cli.js +258 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,12 @@ npx @indiekitai/pg-dash check postgres://user:pass@host/db
|
|
|
21
21
|
# Check migration safety before running it
|
|
22
22
|
npx @indiekitai/pg-dash check-migration ./migrations/015_add_index.sql
|
|
23
23
|
|
|
24
|
+
# EXPLAIN ANALYZE a slow query in the terminal
|
|
25
|
+
npx @indiekitai/pg-dash explain "SELECT * FROM orders WHERE user_id = 1" postgres://...
|
|
26
|
+
|
|
27
|
+
# Real-time lock + long-query monitor (Ctrl+C to exit)
|
|
28
|
+
npx @indiekitai/pg-dash watch-locks postgres://...
|
|
29
|
+
|
|
24
30
|
# Compare two environments (local vs staging)
|
|
25
31
|
npx @indiekitai/pg-dash diff-env --source postgres://localhost/db --target postgres://staging/db
|
|
26
32
|
|
|
@@ -102,6 +108,45 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
|
|
|
102
108
|
- Auto-detects Slack vs Discord webhook URLs
|
|
103
109
|
- Configure via `--slack-webhook` or `--discord-webhook`
|
|
104
110
|
|
|
111
|
+
### ๐ฌ EXPLAIN ANALYZE CLI
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pg-dash explain "SELECT * FROM orders WHERE user_id = 1" postgres://...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Query: SELECT * FROM orders WHERE user_id = 1
|
|
119
|
+
|
|
120
|
+
Limit cost=3.01..3.04 actual=0.060ms rows=10/10
|
|
121
|
+
โโ Sort cost=3.01..3.09 actual=0.057ms rows=10/32
|
|
122
|
+
โโ Seq Scan on users cost=0.00..2.32 actual=0.023ms rows=32/32
|
|
123
|
+
|
|
124
|
+
โโโ Summary โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
125
|
+
Execution time: 0.087ms
|
|
126
|
+
Planning time: 0.756ms
|
|
127
|
+
Seq Scans: users
|
|
128
|
+
|
|
129
|
+
โโโ Recommendations โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
130
|
+
โน Sort on [created_at DESC]. An index might eliminate this.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- Color-coded node types: ๐ด Seq Scan, ๐ข Index Scan, ๐ก Hash Join, ๐ฃ Sort
|
|
134
|
+
- Shows actual vs estimated rows โ catches bad planner estimates
|
|
135
|
+
- Flags Seq Scans > 1000 rows, Sort nodes, Hash Join memory spills
|
|
136
|
+
- `--no-analyze` for dry EXPLAIN (no actual execution)
|
|
137
|
+
- `--json` for scripting
|
|
138
|
+
|
|
139
|
+
### ๐ watch-locks
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pg-dash watch-locks postgres://...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Real-time lock wait monitor โ refreshes every 3 seconds. Shows:
|
|
146
|
+
- Blocked queries with PID, wait time, and the blocking query
|
|
147
|
+
- Long-running queries (configurable threshold via `--long-query-threshold`)
|
|
148
|
+
- Table and lock type for each wait
|
|
149
|
+
|
|
105
150
|
### ๐ก๏ธ Migration Safety Check
|
|
106
151
|
- Analyze a migration SQL file for risks before running it
|
|
107
152
|
- 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`
|
package/README.zh-CN.md
CHANGED
|
@@ -21,6 +21,12 @@ npx @indiekitai/pg-dash check postgres://user:pass@host/db
|
|
|
21
21
|
# ๆง่ก migration ๅๆฃๆฅ้ฃ้ฉ
|
|
22
22
|
npx @indiekitai/pg-dash check-migration ./migrations/015_add_index.sql
|
|
23
23
|
|
|
24
|
+
# ็ป็ซฏ้ๅๆๆ
ขๆฅ่ฏข๏ผๅฝฉ่ฒๆ ๅฝข่พๅบ + ๅปบ่ฎฎ๏ผ
|
|
25
|
+
npx @indiekitai/pg-dash explain "SELECT * FROM orders WHERE user_id = 1" postgres://...
|
|
26
|
+
|
|
27
|
+
# ๅฎๆถ้็ๆง๏ผCtrl+C ้ๅบ๏ผ
|
|
28
|
+
npx @indiekitai/pg-dash watch-locks postgres://...
|
|
29
|
+
|
|
24
30
|
# ๅฏนๆฏไธคไธช็ฏๅข๏ผๆฌๅฐ vs ้ขๅ๏ผ
|
|
25
31
|
npx @indiekitai/pg-dash diff-env --source postgres://localhost/db --target postgres://staging/db
|
|
26
32
|
|
|
@@ -173,6 +179,8 @@ pg-dash --host localhost --user postgres --db mydb --port 3480
|
|
|
173
179
|
pg-dash <connection-string> ๅฏๅจ้ขๆฟ
|
|
174
180
|
pg-dash check <connection-string> ่ฟ่กๅฅๅบทๆฃๆฅๅนถ้ๅบ
|
|
175
181
|
pg-dash check-migration <file> [conn] ๆฃๆฅ migration SQL ็้ฃ้ฉ
|
|
182
|
+
pg-dash explain "<query>" <connection> ็ป็ซฏ้ EXPLAIN ANALYZE ๆฅ่ฏข
|
|
183
|
+
pg-dash watch-locks <connection> ๅฎๆถ้ๅ้ฟๆฅ่ฏข็ๆง
|
|
176
184
|
pg-dash diff-env --source <url> --target <url> ๅฏนๆฏไธคไธช็ฏๅข
|
|
177
185
|
pg-dash schema-diff <connection-string> ๆพ็คบ Schema ๅๆด
|
|
178
186
|
|
package/dist/cli.js
CHANGED
|
@@ -2079,6 +2079,97 @@ var init_env_differ = __esm({
|
|
|
2079
2079
|
}
|
|
2080
2080
|
});
|
|
2081
2081
|
|
|
2082
|
+
// src/server/locks.ts
|
|
2083
|
+
var locks_exports = {};
|
|
2084
|
+
__export(locks_exports, {
|
|
2085
|
+
formatDurationSecs: () => formatDurationSecs,
|
|
2086
|
+
getLockReport: () => getLockReport
|
|
2087
|
+
});
|
|
2088
|
+
function formatDurationSecs(secs) {
|
|
2089
|
+
const h = Math.floor(secs / 3600);
|
|
2090
|
+
const m = Math.floor(secs % 3600 / 60);
|
|
2091
|
+
const s = secs % 60;
|
|
2092
|
+
return [
|
|
2093
|
+
String(h).padStart(2, "0"),
|
|
2094
|
+
String(m).padStart(2, "0"),
|
|
2095
|
+
String(s).padStart(2, "0")
|
|
2096
|
+
].join(":");
|
|
2097
|
+
}
|
|
2098
|
+
async function getLockReport(pool) {
|
|
2099
|
+
const [locksResult, longResult] = await Promise.all([
|
|
2100
|
+
pool.query(`
|
|
2101
|
+
SELECT
|
|
2102
|
+
blocked.pid AS blocked_pid,
|
|
2103
|
+
blocked.query AS blocked_query,
|
|
2104
|
+
EXTRACT(EPOCH FROM (NOW() - blocked.query_start))::int AS blocked_secs,
|
|
2105
|
+
blocking.pid AS blocking_pid,
|
|
2106
|
+
blocking.query AS blocking_query,
|
|
2107
|
+
EXTRACT(EPOCH FROM (NOW() - blocking.query_start))::int AS blocking_secs,
|
|
2108
|
+
blocked_locks.relation::regclass::text AS table_name,
|
|
2109
|
+
blocked_locks.locktype
|
|
2110
|
+
FROM pg_catalog.pg_locks blocked_locks
|
|
2111
|
+
JOIN pg_catalog.pg_stat_activity blocked ON blocked.pid = blocked_locks.pid
|
|
2112
|
+
JOIN pg_catalog.pg_locks blocking_locks
|
|
2113
|
+
ON blocking_locks.locktype = blocked_locks.locktype
|
|
2114
|
+
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
|
2115
|
+
AND blocking_locks.pid != blocked_locks.pid
|
|
2116
|
+
AND blocking_locks.granted = true
|
|
2117
|
+
JOIN pg_catalog.pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
|
|
2118
|
+
WHERE NOT blocked_locks.granted
|
|
2119
|
+
`),
|
|
2120
|
+
pool.query(`
|
|
2121
|
+
SELECT
|
|
2122
|
+
pid,
|
|
2123
|
+
EXTRACT(EPOCH FROM (NOW() - query_start))::int AS duration_secs,
|
|
2124
|
+
query,
|
|
2125
|
+
state,
|
|
2126
|
+
wait_event_type
|
|
2127
|
+
FROM pg_stat_activity
|
|
2128
|
+
WHERE state != 'idle'
|
|
2129
|
+
AND query_start IS NOT NULL
|
|
2130
|
+
AND EXTRACT(EPOCH FROM (NOW() - query_start)) > 5
|
|
2131
|
+
AND query NOT LIKE '%pg_stat_activity%'
|
|
2132
|
+
ORDER BY duration_secs DESC
|
|
2133
|
+
LIMIT 20
|
|
2134
|
+
`)
|
|
2135
|
+
]);
|
|
2136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2137
|
+
const waitingLocks = [];
|
|
2138
|
+
for (const row of locksResult.rows) {
|
|
2139
|
+
const key = `${row.blocked_pid}:${row.blocking_pid}`;
|
|
2140
|
+
if (!seen.has(key)) {
|
|
2141
|
+
seen.add(key);
|
|
2142
|
+
waitingLocks.push({
|
|
2143
|
+
blockedPid: parseInt(row.blocked_pid, 10),
|
|
2144
|
+
blockedQuery: row.blocked_query,
|
|
2145
|
+
blockedDuration: formatDurationSecs(parseInt(row.blocked_secs, 10) || 0),
|
|
2146
|
+
blockingPid: parseInt(row.blocking_pid, 10),
|
|
2147
|
+
blockingQuery: row.blocking_query,
|
|
2148
|
+
blockingDuration: formatDurationSecs(parseInt(row.blocking_secs, 10) || 0),
|
|
2149
|
+
table: row.table_name ?? null,
|
|
2150
|
+
lockType: row.locktype
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
const longRunningQueries = longResult.rows.map((row) => ({
|
|
2155
|
+
pid: parseInt(row.pid, 10),
|
|
2156
|
+
duration: formatDurationSecs(parseInt(row.duration_secs, 10) || 0),
|
|
2157
|
+
query: row.query,
|
|
2158
|
+
state: row.state,
|
|
2159
|
+
waitEventType: row.wait_event_type ?? null
|
|
2160
|
+
}));
|
|
2161
|
+
return {
|
|
2162
|
+
waitingLocks,
|
|
2163
|
+
longRunningQueries,
|
|
2164
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
var init_locks = __esm({
|
|
2168
|
+
"src/server/locks.ts"() {
|
|
2169
|
+
"use strict";
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2082
2173
|
// src/cli.ts
|
|
2083
2174
|
import { parseArgs } from "util";
|
|
2084
2175
|
|
|
@@ -4056,6 +4147,7 @@ var { values, positionals } = parseArgs({
|
|
|
4056
4147
|
"slack-webhook": { type: "string" },
|
|
4057
4148
|
"discord-webhook": { type: "string" },
|
|
4058
4149
|
"no-open": { type: "boolean", default: false },
|
|
4150
|
+
"no-analyze": { type: "boolean", default: false },
|
|
4059
4151
|
json: { type: "boolean", default: false },
|
|
4060
4152
|
host: { type: "string" },
|
|
4061
4153
|
user: { type: "string", short: "u" },
|
|
@@ -4099,6 +4191,8 @@ Usage:
|
|
|
4099
4191
|
pg-dash check <connection-string> Run health check and exit
|
|
4100
4192
|
pg-dash health <connection-string> Alias for check
|
|
4101
4193
|
pg-dash check-migration <file> [connection] Analyze migration SQL for risks
|
|
4194
|
+
pg-dash explain <query> <connection> EXPLAIN ANALYZE a query in the terminal
|
|
4195
|
+
pg-dash watch-locks <connection> Real-time lock + long-query monitor
|
|
4102
4196
|
pg-dash diff-env --source <url> --target <url> Compare two environments
|
|
4103
4197
|
pg-dash schema-diff <connection-string> Show latest schema changes
|
|
4104
4198
|
pg-dash --host localhost --user postgres --db mydb
|
|
@@ -4140,7 +4234,7 @@ Environment variables:
|
|
|
4140
4234
|
`);
|
|
4141
4235
|
process.exit(0);
|
|
4142
4236
|
}
|
|
4143
|
-
var KNOWN_SUBCOMMANDS = ["check", "health", "check-migration", "schema-diff", "diff-env"];
|
|
4237
|
+
var KNOWN_SUBCOMMANDS = ["check", "health", "check-migration", "schema-diff", "diff-env", "explain", "watch-locks"];
|
|
4144
4238
|
var subcommand = positionals[0];
|
|
4145
4239
|
function isValidConnectionString(s) {
|
|
4146
4240
|
return s.startsWith("postgresql://") || s.startsWith("postgres://") || s.includes("@") || // user@host shorthand
|
|
@@ -4497,6 +4591,166 @@ Migration check: ${filePath}`);
|
|
|
4497
4591
|
console.error(`Error: ${err.message}`);
|
|
4498
4592
|
process.exit(1);
|
|
4499
4593
|
}
|
|
4594
|
+
} else if (subcommand === "explain") {
|
|
4595
|
+
const query = positionals[1];
|
|
4596
|
+
if (!query) {
|
|
4597
|
+
console.error('Error: provide a SQL query.\n\nUsage: pg-dash explain "<query>" <connection>');
|
|
4598
|
+
process.exit(1);
|
|
4599
|
+
}
|
|
4600
|
+
const connStr = positionals[2] || resolveConnectionString(2);
|
|
4601
|
+
if (!connStr) {
|
|
4602
|
+
console.error("Error: provide a connection string.");
|
|
4603
|
+
process.exit(1);
|
|
4604
|
+
}
|
|
4605
|
+
const doAnalyze = !values["no-analyze"];
|
|
4606
|
+
const fmt = values.json ? "json" : "text";
|
|
4607
|
+
const { Pool: Pool3 } = await import("pg");
|
|
4608
|
+
const pool = new Pool3({ connectionString: connStr, max: 1, connectionTimeoutMillis: 1e4 });
|
|
4609
|
+
try {
|
|
4610
|
+
let nodeColor = function(type, rows, analyzed) {
|
|
4611
|
+
if (type.includes("Seq Scan")) return analyzed && rows >= 1e3 ? "\x1B[31m\x1B[1m" : "\x1B[31m";
|
|
4612
|
+
if (type.includes("Index")) return "\x1B[32m";
|
|
4613
|
+
if (type === "Hash Join" || type === "Hash") return "\x1B[33m";
|
|
4614
|
+
if (type === "Sort") return "\x1B[35m";
|
|
4615
|
+
return "\x1B[37m";
|
|
4616
|
+
}, renderNode = function(node, indent = 0, isLast = true) {
|
|
4617
|
+
const lines = [];
|
|
4618
|
+
const prefix = indent === 0 ? "" : " ".repeat(indent - 1) + (isLast ? "\u2514\u2500 " : "\u251C\u2500 ");
|
|
4619
|
+
const analyzed = node["Actual Total Time"] !== void 0;
|
|
4620
|
+
const rows = node["Actual Rows"] ?? node["Plan Rows"];
|
|
4621
|
+
const color = nodeColor(node["Node Type"], rows, analyzed);
|
|
4622
|
+
const rel = node["Relation Name"] ? ` on ${dim}${node["Alias"] || node["Relation Name"]}${reset}` : "";
|
|
4623
|
+
const cost = `${dim}cost=${node["Startup Cost"].toFixed(2)}..${node["Total Cost"].toFixed(2)}${reset}`;
|
|
4624
|
+
const timing = analyzed ? ` ${cyan}actual=${node["Actual Total Time"].toFixed(3)}ms${reset}` : "";
|
|
4625
|
+
const rowStr = analyzed ? ` ${dim}rows=${node["Actual Rows"]}/${node["Plan Rows"]}${reset}` : ` ${dim}rows=${node["Plan Rows"]}${reset}`;
|
|
4626
|
+
const idx = node["Index Name"] ? ` ${dim}idx=${node["Index Name"]}${reset}` : "";
|
|
4627
|
+
const filter = node["Filter"] ? ` ${dim}filter=${String(node["Filter"]).slice(0, 40)}${reset}` : "";
|
|
4628
|
+
lines.push(`${prefix}${color}${node["Node Type"]}${reset}${rel} ${cost}${timing}${rowStr}${idx}${filter}`);
|
|
4629
|
+
if (node.Plans?.length) {
|
|
4630
|
+
for (let i = 0; i < node.Plans.length; i++) {
|
|
4631
|
+
lines.push(renderNode(node.Plans[i], indent + 1, i === node.Plans.length - 1));
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
return lines.join("\n");
|
|
4635
|
+
}, collectNodes2 = function(n) {
|
|
4636
|
+
return [n, ...n.Plans?.flatMap(collectNodes2) ?? []];
|
|
4637
|
+
};
|
|
4638
|
+
nodeColor2 = nodeColor, renderNode2 = renderNode, collectNodes3 = collectNodes2;
|
|
4639
|
+
const explainSql = doAnalyze ? `EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) ${query}` : `EXPLAIN (COSTS, VERBOSE, FORMAT JSON) ${query}`;
|
|
4640
|
+
const res = await pool.query(explainSql);
|
|
4641
|
+
await pool.end();
|
|
4642
|
+
const rawPlan = res.rows[0]["QUERY PLAN"];
|
|
4643
|
+
const planObj = Array.isArray(rawPlan) ? rawPlan[0] : rawPlan;
|
|
4644
|
+
const root = planObj.Plan;
|
|
4645
|
+
const planningTime = planObj["Planning Time"];
|
|
4646
|
+
const executionTime = planObj["Execution Time"];
|
|
4647
|
+
const reset = "\x1B[0m";
|
|
4648
|
+
const dim = "\x1B[2m";
|
|
4649
|
+
const cyan = "\x1B[36m";
|
|
4650
|
+
const allNodes = collectNodes2(root);
|
|
4651
|
+
const seqScans = allNodes.filter((n) => n["Node Type"] === "Seq Scan" && n["Relation Name"]).map((n) => n["Relation Name"]);
|
|
4652
|
+
const recs = [];
|
|
4653
|
+
for (const n of allNodes) {
|
|
4654
|
+
const rows = n["Actual Rows"] ?? n["Plan Rows"];
|
|
4655
|
+
if (n["Node Type"] === "Seq Scan" && n["Relation Name"] && rows >= 1e3) {
|
|
4656
|
+
recs.push(`\u26A0 Seq Scan on "${n["Relation Name"]}" (${rows} rows). Consider adding an index.`);
|
|
4657
|
+
}
|
|
4658
|
+
if (n["Node Type"] === "Sort") {
|
|
4659
|
+
recs.push(`\u2139 Sort on [${(n["Sort Key"] ?? []).join(", ")}]. An index might eliminate this.`);
|
|
4660
|
+
}
|
|
4661
|
+
if (n["Hash Batches"] && n["Hash Batches"] > 1) {
|
|
4662
|
+
recs.push(`\u26A0 Hash Join used ${n["Hash Batches"]} batches. Increase work_mem to avoid disk spilling.`);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
if (fmt === "json") {
|
|
4666
|
+
console.log(JSON.stringify({ query, planningTime, executionTime, seqScans, recommendations: recs }, null, 2));
|
|
4667
|
+
} else {
|
|
4668
|
+
if (query) console.log(`
|
|
4669
|
+
\x1B[1mQuery:\x1B[0m ${dim}${query.slice(0, 120)}${reset}`);
|
|
4670
|
+
console.log("\n" + renderNode(root));
|
|
4671
|
+
console.log(`
|
|
4672
|
+
${dim}\u2500\u2500\u2500 Summary ${"\u2500".repeat(36)}${reset}`);
|
|
4673
|
+
if (executionTime !== void 0) console.log(` Execution time: ${cyan}${executionTime.toFixed(3)}ms${reset}`);
|
|
4674
|
+
if (planningTime !== void 0) console.log(` Planning time: ${dim}${planningTime.toFixed(3)}ms${reset}`);
|
|
4675
|
+
if (seqScans.length > 0) console.log(` Seq Scans: \x1B[31m${seqScans.join(", ")}${reset}`);
|
|
4676
|
+
if (recs.length > 0) {
|
|
4677
|
+
console.log(`
|
|
4678
|
+
${dim}\u2500\u2500\u2500 Recommendations ${"\u2500".repeat(28)}${reset}`);
|
|
4679
|
+
for (const r of recs) console.log(` ${r}`);
|
|
4680
|
+
}
|
|
4681
|
+
console.log();
|
|
4682
|
+
}
|
|
4683
|
+
} catch (err) {
|
|
4684
|
+
await pool.end().catch(() => {
|
|
4685
|
+
});
|
|
4686
|
+
console.error(`Error: ${err.message}`);
|
|
4687
|
+
process.exit(1);
|
|
4688
|
+
}
|
|
4689
|
+
process.exit(0);
|
|
4690
|
+
} else if (subcommand === "watch-locks") {
|
|
4691
|
+
const connStr = positionals[1] || resolveConnectionString(1);
|
|
4692
|
+
if (!connStr) {
|
|
4693
|
+
console.error("Error: provide a connection string.\n\nUsage: pg-dash watch-locks <connection>");
|
|
4694
|
+
process.exit(1);
|
|
4695
|
+
}
|
|
4696
|
+
const intervalSec = values.interval ? parseInt(values.interval, 10) : 3;
|
|
4697
|
+
const { Pool: Pool3 } = await import("pg");
|
|
4698
|
+
const pool = new Pool3({ connectionString: connStr, max: 2, connectionTimeoutMillis: 1e4 });
|
|
4699
|
+
const { getLockReport: getLockReport2 } = await Promise.resolve().then(() => (init_locks(), locks_exports));
|
|
4700
|
+
const reset = "\x1B[0m";
|
|
4701
|
+
const dim = "\x1B[2m";
|
|
4702
|
+
const red = "\x1B[31m";
|
|
4703
|
+
const yellow = "\x1B[33m";
|
|
4704
|
+
const cyan = "\x1B[36m";
|
|
4705
|
+
const bold = "\x1B[1m";
|
|
4706
|
+
process.stdout.write("\x1B[?25l");
|
|
4707
|
+
const cleanup = () => {
|
|
4708
|
+
process.stdout.write("\x1B[?25h");
|
|
4709
|
+
pool.end();
|
|
4710
|
+
process.exit(0);
|
|
4711
|
+
};
|
|
4712
|
+
process.on("SIGINT", cleanup);
|
|
4713
|
+
process.on("SIGTERM", cleanup);
|
|
4714
|
+
async function tick() {
|
|
4715
|
+
try {
|
|
4716
|
+
const report = await getLockReport2(pool);
|
|
4717
|
+
console.clear();
|
|
4718
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
4719
|
+
console.log(`${bold}pg-dash watch-locks${reset} ${dim}(Ctrl+C to exit \u2014 refresh every ${intervalSec}s \u2014 ${ts})${reset}
|
|
4720
|
+
`);
|
|
4721
|
+
if (report.waitingLocks.length === 0) {
|
|
4722
|
+
console.log(` ${dim}No lock waits detected.${reset}`);
|
|
4723
|
+
} else {
|
|
4724
|
+
console.log(`${bold}${red} Lock Waits (${report.waitingLocks.length})${reset}`);
|
|
4725
|
+
for (const lw of report.waitingLocks) {
|
|
4726
|
+
console.log(`
|
|
4727
|
+
${red}BLOCKED${reset} pid=${lw.blockedPid} waiting ${lw.blockedDuration}`);
|
|
4728
|
+
console.log(` Query: ${dim}${lw.blockedQuery.slice(0, 100)}${reset}`);
|
|
4729
|
+
console.log(` ${yellow}BLOCKING${reset} pid=${lw.blockingPid} running ${lw.blockingDuration}`);
|
|
4730
|
+
console.log(` Query: ${dim}${lw.blockingQuery.slice(0, 100)}${reset}`);
|
|
4731
|
+
if (lw.table) console.log(` Table: ${lw.table} Lock: ${lw.lockType}`);
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
if (report.longRunningQueries.length > 0) {
|
|
4735
|
+
console.log(`
|
|
4736
|
+
${bold}${yellow} Long-running Queries (${report.longRunningQueries.length})${reset}`);
|
|
4737
|
+
for (const q of report.longRunningQueries) {
|
|
4738
|
+
console.log(`
|
|
4739
|
+
pid=${q.pid} duration=${q.duration} state=${q.state}`);
|
|
4740
|
+
console.log(` ${dim}${q.query.slice(0, 120)}${reset}`);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
if (report.waitingLocks.length === 0 && report.longRunningQueries.length === 0) {
|
|
4744
|
+
console.log(`
|
|
4745
|
+
${dim}No long-running queries.${reset}`);
|
|
4746
|
+
}
|
|
4747
|
+
} catch (err) {
|
|
4748
|
+
console.error(`Error: ${err.message}`);
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
await tick();
|
|
4752
|
+
const timer = setInterval(tick, intervalSec * 1e3);
|
|
4753
|
+
void timer;
|
|
4500
4754
|
} else {
|
|
4501
4755
|
if (subcommand && !isValidConnectionString(subcommand) && KNOWN_SUBCOMMANDS.indexOf(subcommand) === -1) {
|
|
4502
4756
|
console.error(
|
|
@@ -4538,4 +4792,7 @@ Run pg-dash --help for usage.`
|
|
|
4538
4792
|
webhook
|
|
4539
4793
|
});
|
|
4540
4794
|
}
|
|
4795
|
+
var nodeColor2;
|
|
4796
|
+
var renderNode2;
|
|
4797
|
+
var collectNodes3;
|
|
4541
4798
|
//# sourceMappingURL=cli.js.map
|