@yawlabs/postgres-mcp 0.3.3 → 0.4.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/CHANGELOG.md +361 -0
- package/README.md +43 -6
- package/dist/index.js +530 -151
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -35255,6 +35255,12 @@ function getStatementTimeoutMs() {
|
|
|
35255
35255
|
const parsed = Number(raw);
|
|
35256
35256
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3e4;
|
|
35257
35257
|
}
|
|
35258
|
+
function getConnectionTimeoutMs() {
|
|
35259
|
+
const raw = process.env.POSTGRES_CONNECTION_TIMEOUT_MS;
|
|
35260
|
+
if (!raw) return 1e4;
|
|
35261
|
+
const parsed = Number(raw);
|
|
35262
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1e4;
|
|
35263
|
+
}
|
|
35258
35264
|
function getMaxRows() {
|
|
35259
35265
|
const raw = process.env.POSTGRES_MAX_ROWS;
|
|
35260
35266
|
if (!raw) return 1e3;
|
|
@@ -35284,6 +35290,7 @@ function getPool() {
|
|
|
35284
35290
|
pool = new esm_default.Pool({
|
|
35285
35291
|
connectionString: getDatabaseUrl(),
|
|
35286
35292
|
statement_timeout: getStatementTimeoutMs(),
|
|
35293
|
+
connectionTimeoutMillis: getConnectionTimeoutMs(),
|
|
35287
35294
|
max: getPoolMax(),
|
|
35288
35295
|
// MCP sessions can have minutes-long gaps between tool calls. A short
|
|
35289
35296
|
// idleTimeout forces a reconnect on every tool call. 60s keeps the pool
|
|
@@ -35296,6 +35303,29 @@ function getPool() {
|
|
|
35296
35303
|
});
|
|
35297
35304
|
return pool;
|
|
35298
35305
|
}
|
|
35306
|
+
var typeNameCache = null;
|
|
35307
|
+
async function resolveTypeNames(client, oids) {
|
|
35308
|
+
if (oids.length === 0) return {};
|
|
35309
|
+
if (!typeNameCache) {
|
|
35310
|
+
typeNameCache = /* @__PURE__ */ new Map();
|
|
35311
|
+
const res = await client.query("SELECT oid, typname FROM pg_catalog.pg_type");
|
|
35312
|
+
for (const row of res.rows) typeNameCache.set(row.oid, row.typname);
|
|
35313
|
+
}
|
|
35314
|
+
const missing = oids.filter((o) => !typeNameCache?.has(o));
|
|
35315
|
+
if (missing.length > 0) {
|
|
35316
|
+
const res = await client.query(
|
|
35317
|
+
"SELECT oid, typname FROM pg_catalog.pg_type WHERE oid = ANY($1)",
|
|
35318
|
+
[missing]
|
|
35319
|
+
);
|
|
35320
|
+
for (const row of res.rows) typeNameCache.set(row.oid, row.typname);
|
|
35321
|
+
}
|
|
35322
|
+
const out = {};
|
|
35323
|
+
for (const oid of oids) {
|
|
35324
|
+
const n = typeNameCache.get(oid);
|
|
35325
|
+
if (n !== void 0) out[oid] = n;
|
|
35326
|
+
}
|
|
35327
|
+
return out;
|
|
35328
|
+
}
|
|
35299
35329
|
function formatPgError(err) {
|
|
35300
35330
|
if (!(err instanceof Error)) return String(err);
|
|
35301
35331
|
const errObj = err;
|
|
@@ -35308,25 +35338,60 @@ function formatPgError(err) {
|
|
|
35308
35338
|
}
|
|
35309
35339
|
return parts.join(" ");
|
|
35310
35340
|
}
|
|
35311
|
-
function
|
|
35341
|
+
async function runUserQueryBounded(client, sql, params, maxRows) {
|
|
35342
|
+
await client.query("SAVEPOINT __pgmcp_sp");
|
|
35343
|
+
let declareSucceeded = false;
|
|
35344
|
+
try {
|
|
35345
|
+
await client.query({
|
|
35346
|
+
text: `DECLARE __pgmcp_cur NO SCROLL CURSOR FOR ${sql}`,
|
|
35347
|
+
values: params,
|
|
35348
|
+
queryMode: "extended"
|
|
35349
|
+
});
|
|
35350
|
+
declareSucceeded = true;
|
|
35351
|
+
const fetched = await client.query(`FETCH ${maxRows + 1} FROM __pgmcp_cur`);
|
|
35352
|
+
try {
|
|
35353
|
+
await client.query("CLOSE __pgmcp_cur");
|
|
35354
|
+
} catch {
|
|
35355
|
+
}
|
|
35356
|
+
await client.query("RELEASE SAVEPOINT __pgmcp_sp");
|
|
35357
|
+
return fetched;
|
|
35358
|
+
} catch (err) {
|
|
35359
|
+
if (declareSucceeded) {
|
|
35360
|
+
throw err;
|
|
35361
|
+
}
|
|
35362
|
+
await client.query("ROLLBACK TO SAVEPOINT __pgmcp_sp");
|
|
35363
|
+
await client.query("RELEASE SAVEPOINT __pgmcp_sp");
|
|
35364
|
+
return client.query({
|
|
35365
|
+
text: sql,
|
|
35366
|
+
values: params,
|
|
35367
|
+
queryMode: "extended"
|
|
35368
|
+
});
|
|
35369
|
+
}
|
|
35370
|
+
}
|
|
35371
|
+
function toQueryResult(result, maxRows, typeNames = {}) {
|
|
35312
35372
|
const truncated = result.rows.length > maxRows;
|
|
35313
35373
|
const rows = truncated ? result.rows.slice(0, maxRows) : result.rows;
|
|
35314
35374
|
return {
|
|
35315
35375
|
rows,
|
|
35316
35376
|
rowCount: result.rowCount,
|
|
35317
|
-
fields: result.fields.map((f) =>
|
|
35377
|
+
fields: result.fields.map((f) => {
|
|
35378
|
+
const name = typeNames[f.dataTypeID];
|
|
35379
|
+
return name !== void 0 ? { name: f.name, dataTypeID: f.dataTypeID, dataTypeName: name } : { name: f.name, dataTypeID: f.dataTypeID };
|
|
35380
|
+
}),
|
|
35318
35381
|
command: result.command,
|
|
35319
35382
|
...truncated ? { truncated: true } : {}
|
|
35320
35383
|
};
|
|
35321
35384
|
}
|
|
35322
|
-
async function runReadOnly(sql, params = []) {
|
|
35385
|
+
async function runReadOnly(sql, params = [], hooks = {}) {
|
|
35323
35386
|
const client = await getPool().connect();
|
|
35324
35387
|
const maxRows = getMaxRows();
|
|
35325
35388
|
try {
|
|
35326
35389
|
await client.query("BEGIN READ ONLY");
|
|
35327
|
-
|
|
35390
|
+
if (hooks.setup) await hooks.setup(client);
|
|
35391
|
+
const result = await runUserQueryBounded(client, sql, params, maxRows);
|
|
35328
35392
|
await client.query("ROLLBACK");
|
|
35329
|
-
|
|
35393
|
+
const typeNames = await resolveTypeNames(client, [...new Set(result.fields.map((f) => f.dataTypeID))]);
|
|
35394
|
+
return { ok: true, data: toQueryResult(result, maxRows, typeNames) };
|
|
35330
35395
|
} catch (err) {
|
|
35331
35396
|
try {
|
|
35332
35397
|
await client.query("ROLLBACK");
|
|
@@ -35334,6 +35399,12 @@ async function runReadOnly(sql, params = []) {
|
|
|
35334
35399
|
}
|
|
35335
35400
|
return { ok: false, error: formatPgError(err) };
|
|
35336
35401
|
} finally {
|
|
35402
|
+
if (hooks.teardown) {
|
|
35403
|
+
try {
|
|
35404
|
+
await hooks.teardown(client);
|
|
35405
|
+
} catch {
|
|
35406
|
+
}
|
|
35407
|
+
}
|
|
35337
35408
|
client.release();
|
|
35338
35409
|
}
|
|
35339
35410
|
}
|
|
@@ -35348,9 +35419,10 @@ async function runReadWrite(sql, params = []) {
|
|
|
35348
35419
|
const maxRows = getMaxRows();
|
|
35349
35420
|
try {
|
|
35350
35421
|
await client.query("BEGIN");
|
|
35351
|
-
const result = await client
|
|
35422
|
+
const result = await runUserQueryBounded(client, sql, params, maxRows);
|
|
35352
35423
|
await client.query("COMMIT");
|
|
35353
|
-
|
|
35424
|
+
const typeNames = await resolveTypeNames(client, [...new Set(result.fields.map((f) => f.dataTypeID))]);
|
|
35425
|
+
return { ok: true, data: toQueryResult(result, maxRows, typeNames) };
|
|
35354
35426
|
} catch (err) {
|
|
35355
35427
|
try {
|
|
35356
35428
|
await client.query("ROLLBACK");
|
|
@@ -35361,7 +35433,7 @@ async function runReadWrite(sql, params = []) {
|
|
|
35361
35433
|
client.release();
|
|
35362
35434
|
}
|
|
35363
35435
|
}
|
|
35364
|
-
async function runReadWriteRollback(sql, params = []) {
|
|
35436
|
+
async function runReadWriteRollback(sql, params = [], hooks = {}) {
|
|
35365
35437
|
if (!isWritesAllowed()) {
|
|
35366
35438
|
return {
|
|
35367
35439
|
ok: false,
|
|
@@ -35372,9 +35444,11 @@ async function runReadWriteRollback(sql, params = []) {
|
|
|
35372
35444
|
const maxRows = getMaxRows();
|
|
35373
35445
|
try {
|
|
35374
35446
|
await client.query("BEGIN");
|
|
35375
|
-
|
|
35447
|
+
if (hooks.setup) await hooks.setup(client);
|
|
35448
|
+
const result = await runUserQueryBounded(client, sql, params, maxRows);
|
|
35376
35449
|
await client.query("ROLLBACK");
|
|
35377
|
-
|
|
35450
|
+
const typeNames = await resolveTypeNames(client, [...new Set(result.fields.map((f) => f.dataTypeID))]);
|
|
35451
|
+
return { ok: true, data: toQueryResult(result, maxRows, typeNames) };
|
|
35378
35452
|
} catch (err) {
|
|
35379
35453
|
try {
|
|
35380
35454
|
await client.query("ROLLBACK");
|
|
@@ -35382,6 +35456,12 @@ async function runReadWriteRollback(sql, params = []) {
|
|
|
35382
35456
|
}
|
|
35383
35457
|
return { ok: false, error: formatPgError(err) };
|
|
35384
35458
|
} finally {
|
|
35459
|
+
if (hooks.teardown) {
|
|
35460
|
+
try {
|
|
35461
|
+
await hooks.teardown(client);
|
|
35462
|
+
} catch {
|
|
35463
|
+
}
|
|
35464
|
+
}
|
|
35385
35465
|
client.release();
|
|
35386
35466
|
}
|
|
35387
35467
|
}
|
|
@@ -35393,10 +35473,33 @@ async function runInternal(sql, params = []) {
|
|
|
35393
35473
|
return { ok: false, error: formatPgError(err) };
|
|
35394
35474
|
}
|
|
35395
35475
|
}
|
|
35476
|
+
async function withSharedClient(fn) {
|
|
35477
|
+
const client = await getPool().connect();
|
|
35478
|
+
try {
|
|
35479
|
+
const runOnClient = async (sql, params = []) => {
|
|
35480
|
+
try {
|
|
35481
|
+
const result = await client.query(sql, params);
|
|
35482
|
+
return { ok: true, data: result.rows };
|
|
35483
|
+
} catch (err) {
|
|
35484
|
+
return { ok: false, error: formatPgError(err) };
|
|
35485
|
+
}
|
|
35486
|
+
};
|
|
35487
|
+
return await fn(runOnClient);
|
|
35488
|
+
} finally {
|
|
35489
|
+
client.release();
|
|
35490
|
+
}
|
|
35491
|
+
}
|
|
35396
35492
|
async function shutdown() {
|
|
35397
|
-
|
|
35398
|
-
|
|
35399
|
-
|
|
35493
|
+
typeNameCache = null;
|
|
35494
|
+
if (!pool) return;
|
|
35495
|
+
const ending = pool;
|
|
35496
|
+
pool = null;
|
|
35497
|
+
try {
|
|
35498
|
+
await Promise.race([
|
|
35499
|
+
ending.end(),
|
|
35500
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("pool shutdown timed out after 5s")), 5e3))
|
|
35501
|
+
]);
|
|
35502
|
+
} catch {
|
|
35400
35503
|
}
|
|
35401
35504
|
}
|
|
35402
35505
|
|
|
@@ -35490,7 +35593,7 @@ var adminTools = [
|
|
|
35490
35593
|
},
|
|
35491
35594
|
{
|
|
35492
35595
|
name: "pg_table_privileges",
|
|
35493
|
-
description: "Show which roles have which privileges (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER) on a table or on
|
|
35596
|
+
description: "Show which roles have which privileges (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER) on a table or on every table in a schema. If `table` is omitted, the result spans every table in `schema`, ordered by table then grantee. Use this to answer 'who can write to this table?' or to audit schema-wide access before a migration.",
|
|
35494
35597
|
annotations: {
|
|
35495
35598
|
title: "Show table privileges",
|
|
35496
35599
|
readOnlyHint: true,
|
|
@@ -35570,49 +35673,137 @@ var adminTools = [
|
|
|
35570
35673
|
},
|
|
35571
35674
|
inputSchema: external_exports3.object({}),
|
|
35572
35675
|
handler: async () => {
|
|
35573
|
-
|
|
35574
|
-
|
|
35575
|
-
|
|
35576
|
-
|
|
35577
|
-
|
|
35578
|
-
|
|
35579
|
-
|
|
35580
|
-
|
|
35581
|
-
|
|
35582
|
-
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35592
|
-
|
|
35593
|
-
|
|
35594
|
-
|
|
35595
|
-
|
|
35596
|
-
|
|
35597
|
-
|
|
35598
|
-
|
|
35599
|
-
|
|
35600
|
-
|
|
35601
|
-
|
|
35602
|
-
|
|
35603
|
-
|
|
35604
|
-
|
|
35605
|
-
|
|
35606
|
-
|
|
35607
|
-
|
|
35608
|
-
|
|
35609
|
-
|
|
35610
|
-
|
|
35611
|
-
|
|
35612
|
-
|
|
35613
|
-
|
|
35614
|
-
|
|
35615
|
-
|
|
35676
|
+
return withSharedClient(async (run) => {
|
|
35677
|
+
const [slotsRes, replicasRes, walRes] = await Promise.all([
|
|
35678
|
+
run(
|
|
35679
|
+
`SELECT
|
|
35680
|
+
slot_name, slot_type, active,
|
|
35681
|
+
restart_lsn::text AS restart_lsn,
|
|
35682
|
+
confirmed_flush_lsn::text AS confirmed_flush_lsn,
|
|
35683
|
+
wal_status, database, plugin
|
|
35684
|
+
FROM pg_catalog.pg_replication_slots
|
|
35685
|
+
ORDER BY slot_name`
|
|
35686
|
+
),
|
|
35687
|
+
run(
|
|
35688
|
+
`SELECT
|
|
35689
|
+
application_name,
|
|
35690
|
+
client_addr::text AS client_addr,
|
|
35691
|
+
state,
|
|
35692
|
+
sync_state,
|
|
35693
|
+
EXTRACT(EPOCH FROM write_lag)::numeric(10, 2)::float8 AS write_lag_seconds,
|
|
35694
|
+
EXTRACT(EPOCH FROM flush_lag)::numeric(10, 2)::float8 AS flush_lag_seconds,
|
|
35695
|
+
EXTRACT(EPOCH FROM replay_lag)::numeric(10, 2)::float8 AS replay_lag_seconds
|
|
35696
|
+
FROM pg_catalog.pg_stat_replication
|
|
35697
|
+
ORDER BY application_name`
|
|
35698
|
+
),
|
|
35699
|
+
run(
|
|
35700
|
+
`SELECT
|
|
35701
|
+
pg_is_in_recovery() AS is_in_recovery,
|
|
35702
|
+
CASE
|
|
35703
|
+
WHEN pg_is_in_recovery() THEN pg_last_wal_receive_lsn()::text
|
|
35704
|
+
ELSE pg_current_wal_lsn()::text
|
|
35705
|
+
END AS wal_position`
|
|
35706
|
+
)
|
|
35707
|
+
]);
|
|
35708
|
+
if (!slotsRes.ok) return slotsRes;
|
|
35709
|
+
if (!replicasRes.ok) return replicasRes;
|
|
35710
|
+
if (!walRes.ok) return walRes;
|
|
35711
|
+
return {
|
|
35712
|
+
ok: true,
|
|
35713
|
+
data: {
|
|
35714
|
+
is_replica: walRes.data?.[0]?.is_in_recovery ?? false,
|
|
35715
|
+
wal_position: walRes.data?.[0]?.wal_position ?? null,
|
|
35716
|
+
slots: slotsRes.data ?? [],
|
|
35717
|
+
replicas: replicasRes.data ?? []
|
|
35718
|
+
}
|
|
35719
|
+
};
|
|
35720
|
+
});
|
|
35721
|
+
}
|
|
35722
|
+
},
|
|
35723
|
+
{
|
|
35724
|
+
name: "pg_advisor",
|
|
35725
|
+
description: "Rolled-up DBA lint pass. One call returns three categories of findings:\n- sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class.\n- tables_without_primary_key: user tables with no PK. Bloat candidates and a sign of design drift; some replication setups also need PKs.\n- public_tables_without_rls: tables in `public` (or any schema in `rlsSchemas`) with row-level security disabled. Useful as a security baseline check.\nUse this as the 'what should I be looking at?' starting point, then drill into `pg_unused_indexes`, `pg_table_bloat`, `pg_seq_scan_tables` for the perf side.",
|
|
35726
|
+
annotations: {
|
|
35727
|
+
title: "Database advisor (DBA lints)",
|
|
35728
|
+
readOnlyHint: true,
|
|
35729
|
+
destructiveHint: false,
|
|
35730
|
+
idempotentHint: true,
|
|
35731
|
+
openWorldHint: true
|
|
35732
|
+
},
|
|
35733
|
+
inputSchema: external_exports3.object({
|
|
35734
|
+
seqExhaustionThreshold: external_exports3.number().min(0).max(1).default(0.5).describe("Minimum used-fraction (last_value / max_value) to flag a sequence (default 0.5 = 50%)."),
|
|
35735
|
+
rlsSchemas: external_exports3.array(external_exports3.string().min(1).max(63)).default(["public"]).describe("Schemas where RLS-missing should be flagged. Defaults to ['public']."),
|
|
35736
|
+
limit: external_exports3.number().int().min(1).max(500).default(50).describe("Max rows per category (default 50).")
|
|
35737
|
+
}),
|
|
35738
|
+
handler: async (input) => {
|
|
35739
|
+
const { seqExhaustionThreshold, rlsSchemas, limit } = input;
|
|
35740
|
+
return withSharedClient(async (run) => {
|
|
35741
|
+
const [seqRes, noPkRes, rlsRes] = await Promise.all([
|
|
35742
|
+
run(
|
|
35743
|
+
// pg_sequences was added in PG10. last_value can be NULL on a never-
|
|
35744
|
+
// touched sequence; we filter those out (nothing to report yet).
|
|
35745
|
+
`SELECT
|
|
35746
|
+
schemaname AS schema,
|
|
35747
|
+
sequencename AS sequence,
|
|
35748
|
+
last_value::text AS last_value,
|
|
35749
|
+
max_value::text AS max_value,
|
|
35750
|
+
(last_value::float8 / NULLIF(max_value::float8, 0))::numeric(6, 4)::float8 AS pct_used
|
|
35751
|
+
FROM pg_catalog.pg_sequences
|
|
35752
|
+
WHERE last_value IS NOT NULL
|
|
35753
|
+
AND max_value > 0
|
|
35754
|
+
AND (last_value::float8 / max_value::float8) >= $1
|
|
35755
|
+
ORDER BY pct_used DESC NULLS LAST
|
|
35756
|
+
LIMIT $2`,
|
|
35757
|
+
[seqExhaustionThreshold, limit]
|
|
35758
|
+
),
|
|
35759
|
+
run(
|
|
35760
|
+
// Declarative-partition children inherit the parent's primary key
|
|
35761
|
+
// as an indisprimary index, so the NOT EXISTS clause already
|
|
35762
|
+
// excludes them. Nothing extra needed for partitioned schemas.
|
|
35763
|
+
`SELECT
|
|
35764
|
+
n.nspname AS schema,
|
|
35765
|
+
c.relname AS "table"
|
|
35766
|
+
FROM pg_catalog.pg_class c
|
|
35767
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
35768
|
+
WHERE c.relkind = 'r'
|
|
35769
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
35770
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
35771
|
+
AND NOT EXISTS (
|
|
35772
|
+
SELECT 1 FROM pg_catalog.pg_index i
|
|
35773
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
35774
|
+
)
|
|
35775
|
+
ORDER BY n.nspname, c.relname
|
|
35776
|
+
LIMIT $1`,
|
|
35777
|
+
[limit]
|
|
35778
|
+
),
|
|
35779
|
+
run(
|
|
35780
|
+
`SELECT
|
|
35781
|
+
n.nspname AS schema,
|
|
35782
|
+
c.relname AS "table"
|
|
35783
|
+
FROM pg_catalog.pg_class c
|
|
35784
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
35785
|
+
WHERE c.relkind IN ('r', 'p')
|
|
35786
|
+
AND n.nspname = ANY($1)
|
|
35787
|
+
AND NOT c.relrowsecurity
|
|
35788
|
+
ORDER BY n.nspname, c.relname
|
|
35789
|
+
LIMIT $2`,
|
|
35790
|
+
[rlsSchemas, limit]
|
|
35791
|
+
)
|
|
35792
|
+
]);
|
|
35793
|
+
const warnings = [];
|
|
35794
|
+
if (!seqRes.ok) warnings.push(`sequence_exhaustion fetch failed: ${seqRes.error}`);
|
|
35795
|
+
if (!noPkRes.ok) warnings.push(`tables_without_primary_key fetch failed: ${noPkRes.error}`);
|
|
35796
|
+
if (!rlsRes.ok) warnings.push(`public_tables_without_rls fetch failed: ${rlsRes.error}`);
|
|
35797
|
+
return {
|
|
35798
|
+
ok: true,
|
|
35799
|
+
data: {
|
|
35800
|
+
sequence_exhaustion: seqRes.ok ? seqRes.data : [],
|
|
35801
|
+
tables_without_primary_key: noPkRes.ok ? noPkRes.data : [],
|
|
35802
|
+
public_tables_without_rls: rlsRes.ok ? rlsRes.data : [],
|
|
35803
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
35804
|
+
}
|
|
35805
|
+
};
|
|
35806
|
+
});
|
|
35616
35807
|
}
|
|
35617
35808
|
},
|
|
35618
35809
|
{
|
|
@@ -35627,7 +35818,7 @@ var adminTools = [
|
|
|
35627
35818
|
},
|
|
35628
35819
|
inputSchema: external_exports3.object({
|
|
35629
35820
|
schema: external_exports3.string().min(1).max(63).optional().describe("Limit to one schema. If omitted, all user schemas are included."),
|
|
35630
|
-
minDeadRatio: external_exports3.number().min(0).max(1).default(0.1).describe("Minimum dead
|
|
35821
|
+
minDeadRatio: external_exports3.number().min(0).max(1).default(0.1).describe("Minimum dead-tuple fraction to include \u2014 dead / (live + dead). Default 0.1 = 10%."),
|
|
35631
35822
|
limit: external_exports3.number().int().min(1).max(200).default(50).describe("Max rows to return (default 50).")
|
|
35632
35823
|
}),
|
|
35633
35824
|
handler: async (input) => {
|
|
@@ -35636,22 +35827,23 @@ var adminTools = [
|
|
|
35636
35827
|
const params = [minDeadRatio, limit];
|
|
35637
35828
|
if (schema) params.push(schema);
|
|
35638
35829
|
return runInternal(
|
|
35830
|
+
// dead_ratio = dead / (live + dead): bounded [0, 1]. A 100%-dead table
|
|
35831
|
+
// (live=0, dead>0) correctly reports 1.0 instead of 0. Tables with both
|
|
35832
|
+
// counters at 0 are filtered out -- nothing to report.
|
|
35639
35833
|
`SELECT
|
|
35640
35834
|
schemaname AS schema,
|
|
35641
35835
|
relname AS "table",
|
|
35642
35836
|
n_live_tup::text AS live_tuples,
|
|
35643
35837
|
n_dead_tup::text AS dead_tuples,
|
|
35644
|
-
|
|
35645
|
-
WHEN n_live_tup = 0 THEN 0
|
|
35646
|
-
ELSE (n_dead_tup::float8 / GREATEST(n_live_tup, 1))::numeric(6, 3)::float8
|
|
35647
|
-
END AS dead_ratio,
|
|
35838
|
+
(n_dead_tup::float8 / (n_live_tup + n_dead_tup))::numeric(6, 3)::float8 AS dead_ratio,
|
|
35648
35839
|
pg_size_pretty(pg_total_relation_size(relid)) AS size_pretty,
|
|
35649
35840
|
pg_total_relation_size(relid)::text AS size_bytes,
|
|
35650
35841
|
last_vacuum::text AS last_vacuum,
|
|
35651
35842
|
last_autovacuum::text AS last_autovacuum,
|
|
35652
35843
|
last_analyze::text AS last_analyze
|
|
35653
35844
|
FROM pg_catalog.pg_stat_user_tables
|
|
35654
|
-
WHERE (
|
|
35845
|
+
WHERE (n_live_tup + n_dead_tup) > 0
|
|
35846
|
+
AND (n_dead_tup::float8 / (n_live_tup + n_dead_tup)) >= $1
|
|
35655
35847
|
${schemaFilter}
|
|
35656
35848
|
ORDER BY n_dead_tup DESC
|
|
35657
35849
|
LIMIT $2`,
|
|
@@ -35667,10 +35859,56 @@ var paramValue = external_exports3.lazy(
|
|
|
35667
35859
|
);
|
|
35668
35860
|
|
|
35669
35861
|
// src/tools/explain.ts
|
|
35862
|
+
var indexAccessMethod = external_exports3.enum(["btree", "hash", "gin", "gist", "brin", "spgist"]);
|
|
35863
|
+
var hypotheticalIndex = external_exports3.object({
|
|
35864
|
+
table: external_exports3.string().min(1).max(127).describe("Target table. Use `schema.table` (e.g. `public.users`) or just `table` for the search_path."),
|
|
35865
|
+
columns: external_exports3.array(external_exports3.string().min(1).max(63)).min(1).describe("Column names in index order. Quoted identifiers are not supported here -- pass plain names."),
|
|
35866
|
+
using: indexAccessMethod.default("btree").describe("Index access method. btree is the right answer for almost every query.")
|
|
35867
|
+
});
|
|
35868
|
+
function quoteIdent(name) {
|
|
35869
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
35870
|
+
}
|
|
35871
|
+
function quoteQualifiedTable(name) {
|
|
35872
|
+
return name.split(".").map((p) => quoteIdent(p)).join(".");
|
|
35873
|
+
}
|
|
35874
|
+
function validateHypoIndex(idx) {
|
|
35875
|
+
for (const piece of idx.table.split(".")) {
|
|
35876
|
+
if (piece.includes('"')) {
|
|
35877
|
+
return `Hypothetical index table ${JSON.stringify(idx.table)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
35878
|
+
}
|
|
35879
|
+
}
|
|
35880
|
+
for (const col of idx.columns) {
|
|
35881
|
+
if (col.includes('"')) {
|
|
35882
|
+
return `Hypothetical index column ${JSON.stringify(col)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
35883
|
+
}
|
|
35884
|
+
}
|
|
35885
|
+
return null;
|
|
35886
|
+
}
|
|
35887
|
+
function buildHypopgHooks(indexes) {
|
|
35888
|
+
return {
|
|
35889
|
+
setup: async (client) => {
|
|
35890
|
+
for (const idx of indexes) {
|
|
35891
|
+
const cols = idx.columns.map(quoteIdent).join(", ");
|
|
35892
|
+
const tbl = quoteQualifiedTable(idx.table);
|
|
35893
|
+
const createSql = `CREATE INDEX ON ${tbl} USING ${idx.using} (${cols})`;
|
|
35894
|
+
const r = await client.query(
|
|
35895
|
+
"SELECT (hypopg_create_index($1)).indexname AS indexname",
|
|
35896
|
+
[createSql]
|
|
35897
|
+
);
|
|
35898
|
+
if (!r.rows[0]?.indexname) {
|
|
35899
|
+
throw new Error(`hypopg_create_index returned no index for: ${createSql}`);
|
|
35900
|
+
}
|
|
35901
|
+
}
|
|
35902
|
+
},
|
|
35903
|
+
teardown: async (client) => {
|
|
35904
|
+
await client.query("SELECT hypopg_reset()");
|
|
35905
|
+
}
|
|
35906
|
+
};
|
|
35907
|
+
}
|
|
35670
35908
|
var explainTools = [
|
|
35671
35909
|
{
|
|
35672
35910
|
name: "pg_explain",
|
|
35673
|
-
description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE \u2014 for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are always rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the mutation. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement).",
|
|
35911
|
+
description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE \u2014 for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are always rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the mutation. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement). Set `hypothetical_indexes` to a list of `{table, columns, using?}` to ask the planner 'what would the plan be if these indexes existed?' -- requires the HypoPG extension (`CREATE EXTENSION hypopg`). The hypothetical indexes are torn down at the end of the call, never touching real disk.",
|
|
35674
35912
|
annotations: {
|
|
35675
35913
|
title: "Explain query plan",
|
|
35676
35914
|
readOnlyHint: false,
|
|
@@ -35682,10 +35920,13 @@ var explainTools = [
|
|
|
35682
35920
|
sql: external_exports3.string().min(1).max(1e6).describe("The SQL statement to explain. Do NOT prefix with EXPLAIN."),
|
|
35683
35921
|
analyze: external_exports3.boolean().default(false).describe("Run EXPLAIN ANALYZE (actually executes the query)."),
|
|
35684
35922
|
format: external_exports3.enum(["text", "json"]).default("text").describe("Output format."),
|
|
35685
|
-
params: external_exports3.array(paramValue).optional().describe("Positional parameters referenced as $1, $2, ... in the SQL.")
|
|
35923
|
+
params: external_exports3.array(paramValue).optional().describe("Positional parameters referenced as $1, $2, ... in the SQL."),
|
|
35924
|
+
hypothetical_indexes: external_exports3.array(hypotheticalIndex).optional().describe(
|
|
35925
|
+
"List of indexes the planner should pretend exist for this EXPLAIN. Requires the HypoPG extension. Indexes are session-scoped and reset at the end of the call."
|
|
35926
|
+
)
|
|
35686
35927
|
}),
|
|
35687
35928
|
handler: async (input) => {
|
|
35688
|
-
const { sql, analyze, format, params } = input;
|
|
35929
|
+
const { sql, analyze, format, params, hypothetical_indexes } = input;
|
|
35689
35930
|
if (/^\s*EXPLAIN\b/i.test(sql)) {
|
|
35690
35931
|
return {
|
|
35691
35932
|
ok: false,
|
|
@@ -35696,7 +35937,27 @@ var explainTools = [
|
|
|
35696
35937
|
if (analyze) flags.push("ANALYZE");
|
|
35697
35938
|
if (format === "json") flags.push("FORMAT JSON");
|
|
35698
35939
|
const explainSql = flags.length > 0 ? `EXPLAIN (${flags.join(", ")}) ${sql}` : `EXPLAIN ${sql}`;
|
|
35699
|
-
const
|
|
35940
|
+
const hypoIndexes = hypothetical_indexes ?? [];
|
|
35941
|
+
for (const idx of hypoIndexes) {
|
|
35942
|
+
const err = validateHypoIndex(idx);
|
|
35943
|
+
if (err) return { ok: false, error: err };
|
|
35944
|
+
}
|
|
35945
|
+
const hooks = hypoIndexes.length > 0 ? buildHypopgHooks(hypoIndexes) : {};
|
|
35946
|
+
if (hypoIndexes.length > 0) {
|
|
35947
|
+
const check2 = await runInternal(
|
|
35948
|
+
`SELECT EXISTS (
|
|
35949
|
+
SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'hypopg'
|
|
35950
|
+
) AS installed`
|
|
35951
|
+
);
|
|
35952
|
+
if (!check2.ok) return check2;
|
|
35953
|
+
if (!check2.data?.[0]?.installed) {
|
|
35954
|
+
return {
|
|
35955
|
+
ok: false,
|
|
35956
|
+
error: "hypothetical_indexes requires the HypoPG extension. Install with `CREATE EXTENSION hypopg;` (a superuser-equivalent role usually). HypoPG is read-only at the disk level \u2014 it lives entirely in shared memory."
|
|
35957
|
+
};
|
|
35958
|
+
}
|
|
35959
|
+
}
|
|
35960
|
+
const result = analyze && isWritesAllowed() ? await runReadWriteRollback(explainSql, params ?? [], hooks) : await runReadOnly(explainSql, params ?? [], hooks);
|
|
35700
35961
|
if (!result.ok) return result;
|
|
35701
35962
|
const data = result.data;
|
|
35702
35963
|
const rows = data?.rows ?? [];
|
|
@@ -35727,61 +35988,69 @@ var healthTools = [
|
|
|
35727
35988
|
}),
|
|
35728
35989
|
handler: async (input) => {
|
|
35729
35990
|
const { activeQueryLimit } = input;
|
|
35730
|
-
|
|
35731
|
-
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35743
|
-
|
|
35744
|
-
|
|
35745
|
-
|
|
35746
|
-
|
|
35747
|
-
|
|
35748
|
-
|
|
35749
|
-
|
|
35750
|
-
|
|
35751
|
-
|
|
35752
|
-
|
|
35753
|
-
|
|
35754
|
-
|
|
35755
|
-
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35760
|
-
|
|
35761
|
-
|
|
35762
|
-
|
|
35763
|
-
|
|
35764
|
-
|
|
35765
|
-
|
|
35766
|
-
|
|
35767
|
-
|
|
35768
|
-
|
|
35769
|
-
|
|
35770
|
-
|
|
35771
|
-
|
|
35772
|
-
|
|
35773
|
-
|
|
35774
|
-
|
|
35775
|
-
|
|
35776
|
-
|
|
35777
|
-
|
|
35778
|
-
|
|
35779
|
-
|
|
35780
|
-
|
|
35781
|
-
|
|
35782
|
-
|
|
35783
|
-
|
|
35784
|
-
|
|
35991
|
+
return withSharedClient(async (run) => {
|
|
35992
|
+
const [versionRes, sizeRes, connsRes, activeRes, tableCountRes] = await Promise.all([
|
|
35993
|
+
run(`SELECT version() AS version`),
|
|
35994
|
+
run(
|
|
35995
|
+
`SELECT
|
|
35996
|
+
current_database() AS database,
|
|
35997
|
+
pg_size_pretty(pg_database_size(current_database())) AS size_pretty,
|
|
35998
|
+
pg_database_size(current_database())::text AS size_bytes`
|
|
35999
|
+
),
|
|
36000
|
+
run(
|
|
36001
|
+
`SELECT
|
|
36002
|
+
count(*)::text AS total,
|
|
36003
|
+
count(*) FILTER (WHERE state = 'active')::text AS active,
|
|
36004
|
+
count(*) FILTER (WHERE state = 'idle')::text AS idle,
|
|
36005
|
+
count(*) FILTER (WHERE state = 'idle in transaction')::text AS idle_in_transaction
|
|
36006
|
+
FROM pg_stat_activity
|
|
36007
|
+
WHERE datname = current_database()`
|
|
36008
|
+
),
|
|
36009
|
+
run(
|
|
36010
|
+
`SELECT
|
|
36011
|
+
pid,
|
|
36012
|
+
state,
|
|
36013
|
+
EXTRACT(EPOCH FROM (now() - query_start))::numeric(10, 2)::float8 AS duration_seconds,
|
|
36014
|
+
query,
|
|
36015
|
+
application_name
|
|
36016
|
+
FROM pg_stat_activity
|
|
36017
|
+
WHERE datname = current_database()
|
|
36018
|
+
AND state IS NOT NULL
|
|
36019
|
+
AND state <> 'idle'
|
|
36020
|
+
AND pid <> pg_backend_pid()
|
|
36021
|
+
ORDER BY query_start ASC NULLS LAST
|
|
36022
|
+
LIMIT $1`,
|
|
36023
|
+
[activeQueryLimit]
|
|
36024
|
+
),
|
|
36025
|
+
run(
|
|
36026
|
+
`SELECT count(*)::text AS count
|
|
36027
|
+
FROM pg_catalog.pg_class c
|
|
36028
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
36029
|
+
WHERE c.relkind IN ('r', 'p')
|
|
36030
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
36031
|
+
AND n.nspname NOT LIKE 'pg_toast%'
|
|
36032
|
+
AND n.nspname NOT LIKE 'pg_temp_%'`
|
|
36033
|
+
)
|
|
36034
|
+
]);
|
|
36035
|
+
if (!versionRes.ok) return versionRes;
|
|
36036
|
+
const warnings = [];
|
|
36037
|
+
if (!sizeRes.ok) warnings.push(`database fetch failed: ${sizeRes.error}`);
|
|
36038
|
+
if (!connsRes.ok) warnings.push(`connections fetch failed: ${connsRes.error}`);
|
|
36039
|
+
if (!activeRes.ok) warnings.push(`active_queries fetch failed: ${activeRes.error}`);
|
|
36040
|
+
if (!tableCountRes.ok) warnings.push(`table_count fetch failed: ${tableCountRes.error}`);
|
|
36041
|
+
return {
|
|
36042
|
+
ok: true,
|
|
36043
|
+
data: {
|
|
36044
|
+
connected: true,
|
|
36045
|
+
version: versionRes.data?.[0]?.version,
|
|
36046
|
+
database: sizeRes.ok ? sizeRes.data?.[0] : null,
|
|
36047
|
+
connections: connsRes.ok ? connsRes.data?.[0] : null,
|
|
36048
|
+
active_queries: activeRes.ok ? activeRes.data : [],
|
|
36049
|
+
table_count: tableCountRes.ok ? tableCountRes.data?.[0]?.count : null,
|
|
36050
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
36051
|
+
}
|
|
36052
|
+
};
|
|
36053
|
+
});
|
|
35785
36054
|
}
|
|
35786
36055
|
}
|
|
35787
36056
|
];
|
|
@@ -35883,7 +36152,7 @@ var schemaTools = [
|
|
|
35883
36152
|
},
|
|
35884
36153
|
{
|
|
35885
36154
|
name: "pg_describe_table",
|
|
35886
|
-
description: "Describe a
|
|
36155
|
+
description: "Describe a relation: kind (table / view / materialized_view / partitioned_table / foreign_table), columns (name, type, nullable, default), primary key, foreign keys (outgoing), `referenced_by` (other tables whose FKs point at this one), `constraints` (CHECK / UNIQUE non-PK / EXCLUDE), indexes, and partition info (`partition_of` parent, `partitions` children). Works on views and materialized views too -- PK/FK/constraint/index lists will simply be empty for a plain view. Use `kind` to disambiguate before assuming you can write to the relation.",
|
|
35887
36156
|
annotations: {
|
|
35888
36157
|
title: "Describe table",
|
|
35889
36158
|
readOnlyHint: true,
|
|
@@ -35897,6 +36166,20 @@ var schemaTools = [
|
|
|
35897
36166
|
}),
|
|
35898
36167
|
handler: async (input) => {
|
|
35899
36168
|
const { schema, table } = input;
|
|
36169
|
+
const kindQuery = `
|
|
36170
|
+
SELECT
|
|
36171
|
+
CASE c.relkind
|
|
36172
|
+
WHEN 'r' THEN 'table'
|
|
36173
|
+
WHEN 'p' THEN 'partitioned_table'
|
|
36174
|
+
WHEN 'v' THEN 'view'
|
|
36175
|
+
WHEN 'm' THEN 'materialized_view'
|
|
36176
|
+
WHEN 'f' THEN 'foreign_table'
|
|
36177
|
+
ELSE c.relkind::text
|
|
36178
|
+
END AS kind
|
|
36179
|
+
FROM pg_catalog.pg_class c
|
|
36180
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
36181
|
+
WHERE n.nspname = $1 AND c.relname = $2
|
|
36182
|
+
`;
|
|
35900
36183
|
const columnsQuery = `
|
|
35901
36184
|
SELECT
|
|
35902
36185
|
a.attname AS name,
|
|
@@ -35961,32 +36244,121 @@ var schemaTools = [
|
|
|
35961
36244
|
AND c.relname = $2
|
|
35962
36245
|
ORDER BY i.relname
|
|
35963
36246
|
`;
|
|
35964
|
-
const
|
|
35965
|
-
|
|
35966
|
-
|
|
35967
|
-
|
|
35968
|
-
|
|
35969
|
-
|
|
35970
|
-
|
|
35971
|
-
|
|
35972
|
-
|
|
35973
|
-
|
|
35974
|
-
|
|
35975
|
-
|
|
35976
|
-
|
|
35977
|
-
|
|
35978
|
-
|
|
35979
|
-
|
|
35980
|
-
|
|
35981
|
-
|
|
35982
|
-
|
|
35983
|
-
|
|
35984
|
-
|
|
35985
|
-
|
|
35986
|
-
|
|
35987
|
-
|
|
36247
|
+
const constraintsQuery = `
|
|
36248
|
+
SELECT
|
|
36249
|
+
con.conname AS name,
|
|
36250
|
+
CASE con.contype
|
|
36251
|
+
WHEN 'c' THEN 'check'
|
|
36252
|
+
WHEN 'u' THEN 'unique'
|
|
36253
|
+
WHEN 'x' THEN 'exclude'
|
|
36254
|
+
ELSE con.contype::text
|
|
36255
|
+
END AS type,
|
|
36256
|
+
pg_catalog.pg_get_constraintdef(con.oid, true) AS definition
|
|
36257
|
+
FROM pg_catalog.pg_constraint con
|
|
36258
|
+
JOIN pg_catalog.pg_class c ON c.oid = con.conrelid
|
|
36259
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
36260
|
+
WHERE n.nspname = $1
|
|
36261
|
+
AND c.relname = $2
|
|
36262
|
+
AND con.contype IN ('c', 'u', 'x')
|
|
36263
|
+
ORDER BY con.contype, con.conname
|
|
36264
|
+
`;
|
|
36265
|
+
const referencedByQuery = `
|
|
36266
|
+
SELECT
|
|
36267
|
+
con.conname AS constraint_name,
|
|
36268
|
+
srcn.nspname AS schema,
|
|
36269
|
+
src.relname AS "table",
|
|
36270
|
+
array_agg(srcatt.attname::text ORDER BY u.attposition) AS columns,
|
|
36271
|
+
array_agg(refatt.attname::text ORDER BY u.attposition) AS referenced_columns
|
|
36272
|
+
FROM pg_catalog.pg_constraint con
|
|
36273
|
+
JOIN pg_catalog.pg_class src ON src.oid = con.conrelid
|
|
36274
|
+
JOIN pg_catalog.pg_namespace srcn ON srcn.oid = src.relnamespace
|
|
36275
|
+
JOIN pg_catalog.pg_class ref ON ref.oid = con.confrelid
|
|
36276
|
+
JOIN pg_catalog.pg_namespace refn ON refn.oid = ref.relnamespace
|
|
36277
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
|
|
36278
|
+
JOIN pg_catalog.pg_attribute srcatt ON srcatt.attrelid = con.conrelid AND srcatt.attnum = u.attnum
|
|
36279
|
+
JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS fu(attnum, attposition) ON fu.attposition = u.attposition
|
|
36280
|
+
JOIN pg_catalog.pg_attribute refatt ON refatt.attrelid = con.confrelid AND refatt.attnum = fu.attnum
|
|
36281
|
+
WHERE refn.nspname = $1
|
|
36282
|
+
AND ref.relname = $2
|
|
36283
|
+
AND con.contype = 'f'
|
|
36284
|
+
GROUP BY con.conname, srcn.nspname, src.relname
|
|
36285
|
+
ORDER BY srcn.nspname, src.relname, con.conname
|
|
36286
|
+
`;
|
|
36287
|
+
const partitionParentQuery = `
|
|
36288
|
+
SELECT
|
|
36289
|
+
parn.nspname AS schema,
|
|
36290
|
+
par.relname AS "table"
|
|
36291
|
+
FROM pg_catalog.pg_inherits i
|
|
36292
|
+
JOIN pg_catalog.pg_class child ON child.oid = i.inhrelid
|
|
36293
|
+
JOIN pg_catalog.pg_namespace childn ON childn.oid = child.relnamespace
|
|
36294
|
+
JOIN pg_catalog.pg_class par ON par.oid = i.inhparent
|
|
36295
|
+
JOIN pg_catalog.pg_namespace parn ON parn.oid = par.relnamespace
|
|
36296
|
+
WHERE childn.nspname = $1
|
|
36297
|
+
AND child.relname = $2
|
|
36298
|
+
AND child.relispartition
|
|
36299
|
+
`;
|
|
36300
|
+
const partitionChildrenQuery = `
|
|
36301
|
+
SELECT
|
|
36302
|
+
childn.nspname AS schema,
|
|
36303
|
+
child.relname AS "table",
|
|
36304
|
+
pg_catalog.pg_get_expr(child.relpartbound, child.oid) AS bound
|
|
36305
|
+
FROM pg_catalog.pg_inherits i
|
|
36306
|
+
JOIN pg_catalog.pg_class par ON par.oid = i.inhparent
|
|
36307
|
+
JOIN pg_catalog.pg_namespace parn ON parn.oid = par.relnamespace
|
|
36308
|
+
JOIN pg_catalog.pg_class child ON child.oid = i.inhrelid
|
|
36309
|
+
JOIN pg_catalog.pg_namespace childn ON childn.oid = child.relnamespace
|
|
36310
|
+
WHERE parn.nspname = $1
|
|
36311
|
+
AND par.relname = $2
|
|
36312
|
+
AND child.relispartition
|
|
36313
|
+
ORDER BY childn.nspname, child.relname
|
|
36314
|
+
`;
|
|
36315
|
+
return withSharedClient(async (run) => {
|
|
36316
|
+
const [kindRes, cols, pk, fks, idxs, constraints, referencedBy, partitionParent, partitionChildren] = await Promise.all([
|
|
36317
|
+
run(kindQuery, [schema, table]),
|
|
36318
|
+
run(columnsQuery, [schema, table]),
|
|
36319
|
+
run(primaryKeyQuery, [schema, table]),
|
|
36320
|
+
run(foreignKeysQuery, [schema, table]),
|
|
36321
|
+
run(indexesQuery, [schema, table]),
|
|
36322
|
+
run(constraintsQuery, [schema, table]),
|
|
36323
|
+
run(referencedByQuery, [schema, table]),
|
|
36324
|
+
run(partitionParentQuery, [schema, table]),
|
|
36325
|
+
run(partitionChildrenQuery, [schema, table])
|
|
36326
|
+
]);
|
|
36327
|
+
if (!cols.ok) return cols;
|
|
36328
|
+
if (!cols.data || cols.data.length === 0) {
|
|
36329
|
+
return {
|
|
36330
|
+
ok: false,
|
|
36331
|
+
error: `Table ${JSON.stringify(schema)}.${JSON.stringify(table)} not found.`
|
|
36332
|
+
};
|
|
35988
36333
|
}
|
|
35989
|
-
|
|
36334
|
+
const kind = kindRes.ok ? kindRes.data?.[0]?.kind ?? "table" : "table";
|
|
36335
|
+
const warnings = [];
|
|
36336
|
+
if (!pk.ok) warnings.push(`primary_key fetch failed: ${pk.error}`);
|
|
36337
|
+
if (!fks.ok) warnings.push(`foreign_keys fetch failed: ${fks.error}`);
|
|
36338
|
+
if (!idxs.ok) warnings.push(`indexes fetch failed: ${idxs.error}`);
|
|
36339
|
+
if (!constraints.ok) warnings.push(`constraints fetch failed: ${constraints.error}`);
|
|
36340
|
+
if (!referencedBy.ok) warnings.push(`referenced_by fetch failed: ${referencedBy.error}`);
|
|
36341
|
+
if (!partitionParent.ok) warnings.push(`partition_of fetch failed: ${partitionParent.error}`);
|
|
36342
|
+
if (!partitionChildren.ok) warnings.push(`partitions fetch failed: ${partitionChildren.error}`);
|
|
36343
|
+
const parentRow = partitionParent.ok ? partitionParent.data?.[0] : void 0;
|
|
36344
|
+
return {
|
|
36345
|
+
ok: true,
|
|
36346
|
+
data: {
|
|
36347
|
+
schema,
|
|
36348
|
+
table,
|
|
36349
|
+
kind,
|
|
36350
|
+
columns: cols.data,
|
|
36351
|
+
primary_key: pk.ok ? (pk.data ?? []).map((r) => r.column_name) : [],
|
|
36352
|
+
foreign_keys: fks.ok ? fks.data : [],
|
|
36353
|
+
referenced_by: referencedBy.ok ? referencedBy.data : [],
|
|
36354
|
+
constraints: constraints.ok ? constraints.data : [],
|
|
36355
|
+
indexes: idxs.ok ? idxs.data : [],
|
|
36356
|
+
...parentRow ? { partition_of: parentRow } : {},
|
|
36357
|
+
...partitionChildren.ok && (partitionChildren.data ?? []).length > 0 ? { partitions: partitionChildren.data } : {},
|
|
36358
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
36359
|
+
}
|
|
36360
|
+
};
|
|
36361
|
+
});
|
|
35990
36362
|
}
|
|
35991
36363
|
},
|
|
35992
36364
|
{
|
|
@@ -36165,8 +36537,12 @@ var statsTools = [
|
|
|
36165
36537
|
const meanCol = useExecSuffix ? "mean_exec_time" : "mean_time";
|
|
36166
36538
|
const minCol = useExecSuffix ? "min_exec_time" : "min_time";
|
|
36167
36539
|
const maxCol = useExecSuffix ? "max_exec_time" : "max_time";
|
|
36168
|
-
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "calls";
|
|
36540
|
+
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "pg_stat_statements.calls";
|
|
36169
36541
|
return runInternal(
|
|
36542
|
+
// bigint counters (calls, rows) come back as `.text` for lossless
|
|
36543
|
+
// serialization, matching pg_seq_scan_tables / pg_unused_indexes /
|
|
36544
|
+
// pg_table_bloat. Timing fields stay as float8 because they are
|
|
36545
|
+
// inherently fractional milliseconds.
|
|
36170
36546
|
`SELECT
|
|
36171
36547
|
query,
|
|
36172
36548
|
calls::text AS calls,
|
|
@@ -36271,7 +36647,10 @@ var statsTools = [
|
|
|
36271
36647
|
}
|
|
36272
36648
|
];
|
|
36273
36649
|
function compareVersions(a, b) {
|
|
36274
|
-
const parse3 = (v) => v.split(".").map((
|
|
36650
|
+
const parse3 = (v) => v.split(".").map((seg) => {
|
|
36651
|
+
const m = seg.match(/^\d+/);
|
|
36652
|
+
return m ? Number.parseInt(m[0], 10) : 0;
|
|
36653
|
+
});
|
|
36275
36654
|
const aa = parse3(a);
|
|
36276
36655
|
const bb = parse3(b);
|
|
36277
36656
|
const len = Math.max(aa.length, bb.length);
|
|
@@ -36283,7 +36662,7 @@ function compareVersions(a, b) {
|
|
|
36283
36662
|
}
|
|
36284
36663
|
|
|
36285
36664
|
// src/index.ts
|
|
36286
|
-
var version2 = true ? "0.
|
|
36665
|
+
var version2 = true ? "0.4.1" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
36287
36666
|
var subcommand = process.argv[2];
|
|
36288
36667
|
if (subcommand === "version" || subcommand === "--version") {
|
|
36289
36668
|
console.log(version2);
|