postgresai 0.16.0-rc.4 → 0.16.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 +154 -0
- package/dist/bin/postgres-ai.js +2911 -255
- package/package.json +12 -3
- package/schemas/A002.schema.json +63 -0
- package/schemas/A003.schema.json +73 -0
- package/schemas/A004.schema.json +81 -0
- package/schemas/A007.schema.json +71 -0
- package/schemas/A013.schema.json +61 -0
- package/schemas/D001.schema.json +71 -0
- package/schemas/D004.schema.json +136 -0
- package/schemas/F001.schema.json +73 -0
- package/schemas/F002.schema.json +108 -0
- package/schemas/F003.schema.json +138 -0
- package/schemas/F004.schema.json +125 -0
- package/schemas/F005.schema.json +131 -0
- package/schemas/F009.schema.json +155 -0
- package/schemas/G001.schema.json +135 -0
- package/schemas/G003.schema.json +90 -0
- package/schemas/H001.schema.json +141 -0
- package/schemas/H002.schema.json +129 -0
- package/schemas/H004.schema.json +128 -0
- package/schemas/I001.schema.json +149 -0
- package/schemas/K001.schema.json +161 -0
- package/schemas/K003.schema.json +163 -0
- package/schemas/K004.schema.json +110 -0
- package/schemas/K005.schema.json +110 -0
- package/schemas/K006.schema.json +110 -0
- package/schemas/K007.schema.json +110 -0
- package/schemas/K008.schema.json +110 -0
- package/schemas/M001.schema.json +119 -0
- package/schemas/M002.schema.json +110 -0
- package/schemas/M003.schema.json +128 -0
- package/schemas/N001.schema.json +161 -0
- package/schemas/query.schema.json +62 -0
- package/CHANGELOG.md +0 -11
- package/bin/postgres-ai.ts +0 -5578
- package/bun.lock +0 -258
- package/bunfig.toml +0 -20
- package/lib/aas-onboard.ts +0 -251
- package/lib/auth-server.ts +0 -285
- package/lib/checkup-api.ts +0 -526
- package/lib/checkup-dictionary.ts +0 -103
- package/lib/checkup-summary.ts +0 -338
- package/lib/checkup.ts +0 -2261
- package/lib/config.ts +0 -171
- package/lib/init.ts +0 -1152
- package/lib/instances.ts +0 -245
- package/lib/issues.ts +0 -1060
- package/lib/mcp-server.ts +0 -667
- package/lib/metrics-loader.ts +0 -134
- package/lib/pkce.ts +0 -79
- package/lib/reports.ts +0 -373
- package/lib/storage.ts +0 -367
- package/lib/supabase.ts +0 -826
- package/lib/util.ts +0 -134
- package/packages/postgres-ai/README.md +0 -26
- package/packages/postgres-ai/bin/postgres-ai.js +0 -27
- package/packages/postgres-ai/package.json +0 -27
- package/scripts/embed-checkup-dictionary.ts +0 -115
- package/scripts/embed-metrics.ts +0 -160
- package/scripts/generate-release-notes.ts +0 -668
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
- package/test/aas-onboard.test.ts +0 -301
- package/test/auth.test.ts +0 -287
- package/test/checkup.integration.test.ts +0 -413
- package/test/checkup.test.ts +0 -3626
- package/test/compose-cmd.test.ts +0 -120
- package/test/config-consistency.test.ts +0 -352
- package/test/init.integration.test.ts +0 -438
- package/test/init.test.ts +0 -1816
- package/test/issues.cli.test.ts +0 -1162
- package/test/issues.test.ts +0 -456
- package/test/mcp-server.test.ts +0 -2530
- package/test/monitoring.test.ts +0 -746
- package/test/permission-check-sql.test.ts +0 -116
- package/test/reports.cli.test.ts +0 -793
- package/test/reports.test.ts +0 -977
- package/test/schema-validation.test.ts +0 -231
- package/test/storage.test.ts +0 -935
- package/test/supabase.test.ts +0 -709
- package/test/targets-add-config.test.ts +0 -28
- package/test/test-utils.ts +0 -190
- package/test/upgrade.test.ts +0 -1056
- package/test/util.test.ts +0 -44
- package/tsconfig.json +0 -20
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13323,43 +13323,92 @@ var require_dist2 = __commonJS((exports, module) => {
|
|
|
13323
13323
|
var exports_util2 = {};
|
|
13324
13324
|
__export(exports_util2, {
|
|
13325
13325
|
resolveBaseUrls: () => resolveBaseUrls2,
|
|
13326
|
+
requestTimeoutSignal: () => requestTimeoutSignal2,
|
|
13327
|
+
redactTextSecrets: () => redactTextSecrets2,
|
|
13328
|
+
redactSecretsForLog: () => redactSecretsForLog2,
|
|
13329
|
+
redactSecrets: () => redactSecrets2,
|
|
13326
13330
|
normalizeBaseUrl: () => normalizeBaseUrl2,
|
|
13327
13331
|
maskSecret: () => maskSecret2,
|
|
13328
|
-
|
|
13332
|
+
isRetryableHttpStatus: () => isRetryableHttpStatus2,
|
|
13333
|
+
isFetchTimeout: () => isFetchTimeout2,
|
|
13334
|
+
formatHttpError: () => formatHttpError2,
|
|
13335
|
+
describeFetchError: () => describeFetchError2,
|
|
13336
|
+
HttpStatusError: () => HttpStatusError2,
|
|
13337
|
+
HttpRequestTimeoutError: () => HttpRequestTimeoutError2,
|
|
13338
|
+
DEFAULT_HTTP_REQUEST_TIMEOUT_MS: () => DEFAULT_HTTP_REQUEST_TIMEOUT_MS2
|
|
13329
13339
|
});
|
|
13330
13340
|
function isHtmlContent2(text) {
|
|
13331
13341
|
const trimmed = text.trim();
|
|
13332
13342
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
13333
13343
|
}
|
|
13334
|
-
function formatHttpError2(operation, status, responseBody) {
|
|
13335
|
-
const
|
|
13336
|
-
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
13344
|
+
function formatHttpError2(operation, status, responseBody, statusText) {
|
|
13345
|
+
const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
|
|
13337
13346
|
const remediation = status === 401 ? `
|
|
13338
13347
|
${AUTH_REMEDIATION_HINT2}` : "";
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13348
|
+
let bodyMessage;
|
|
13349
|
+
let bodyDetails;
|
|
13350
|
+
let bodyCode;
|
|
13351
|
+
let bodyHint;
|
|
13352
|
+
if (responseBody && !isHtmlContent2(responseBody)) {
|
|
13343
13353
|
try {
|
|
13344
13354
|
const errObj = JSON.parse(responseBody);
|
|
13345
|
-
const message = errObj.message
|
|
13346
|
-
if (
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13355
|
+
const message = errObj.message ?? errObj.error;
|
|
13356
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
13357
|
+
bodyMessage = redactTextSecrets2(message.trim());
|
|
13358
|
+
}
|
|
13359
|
+
const details = errObj.details ?? errObj.detail;
|
|
13360
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
13361
|
+
bodyDetails = redactTextSecrets2(details.trim());
|
|
13362
|
+
}
|
|
13363
|
+
if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
|
|
13364
|
+
bodyCode = errObj.code.trim();
|
|
13365
|
+
}
|
|
13366
|
+
if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
|
|
13367
|
+
bodyHint = redactTextSecrets2(errObj.hint.trim());
|
|
13368
|
+
}
|
|
13369
|
+
if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
|
|
13370
|
+
bodyDetails = redactSecretsForLog2(JSON.stringify(errObj));
|
|
13352
13371
|
}
|
|
13353
13372
|
} catch {
|
|
13354
13373
|
const trimmed = responseBody.trim();
|
|
13355
13374
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
13356
|
-
|
|
13357
|
-
${trimmed}`;
|
|
13375
|
+
bodyDetails = redactTextSecrets2(trimmed);
|
|
13358
13376
|
}
|
|
13359
13377
|
}
|
|
13360
13378
|
}
|
|
13379
|
+
const trimmedReason = statusText?.trim();
|
|
13380
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
13381
|
+
const safeReasonPhrase = reasonPhrase ? redactTextSecrets2(reasonPhrase) : undefined;
|
|
13382
|
+
const headline = bodyMessage ?? safeReasonPhrase ?? generic;
|
|
13383
|
+
const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
|
|
13384
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
|
|
13385
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
13386
|
+
errMsg += `
|
|
13387
|
+
${bodyDetails}`;
|
|
13388
|
+
}
|
|
13389
|
+
if (bodyHint) {
|
|
13390
|
+
errMsg += `
|
|
13391
|
+
Hint: ${bodyHint}`;
|
|
13392
|
+
}
|
|
13361
13393
|
return errMsg + remediation;
|
|
13362
13394
|
}
|
|
13395
|
+
function describeFetchError2(operation, url, err) {
|
|
13396
|
+
const cause = err?.cause;
|
|
13397
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
13398
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
13399
|
+
}
|
|
13400
|
+
function isRetryableHttpStatus2(status) {
|
|
13401
|
+
return status >= 500 || status === 429;
|
|
13402
|
+
}
|
|
13403
|
+
function requestTimeoutSignal2(timeoutMs) {
|
|
13404
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS2;
|
|
13405
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS2, Math.floor(requested)));
|
|
13406
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
13407
|
+
}
|
|
13408
|
+
function isFetchTimeout2(err) {
|
|
13409
|
+
const name = err?.name;
|
|
13410
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
13411
|
+
}
|
|
13363
13412
|
function maskSecret2(secret) {
|
|
13364
13413
|
if (!secret)
|
|
13365
13414
|
return "";
|
|
@@ -13369,6 +13418,35 @@ function maskSecret2(secret) {
|
|
|
13369
13418
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
13370
13419
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
13371
13420
|
}
|
|
13421
|
+
function isSensitiveLogKey2(key) {
|
|
13422
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
13423
|
+
return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
|
|
13424
|
+
}
|
|
13425
|
+
function redactSecrets2(value) {
|
|
13426
|
+
if (Array.isArray(value)) {
|
|
13427
|
+
return value.map(redactSecrets2);
|
|
13428
|
+
}
|
|
13429
|
+
if (value && typeof value === "object") {
|
|
13430
|
+
const out = {};
|
|
13431
|
+
for (const [key, child] of Object.entries(value)) {
|
|
13432
|
+
out[key] = isSensitiveLogKey2(key) && child != null ? "[REDACTED]" : redactSecrets2(child);
|
|
13433
|
+
}
|
|
13434
|
+
return out;
|
|
13435
|
+
}
|
|
13436
|
+
return value;
|
|
13437
|
+
}
|
|
13438
|
+
function redactSecretsForLog2(text) {
|
|
13439
|
+
let parsed;
|
|
13440
|
+
try {
|
|
13441
|
+
parsed = JSON.parse(text);
|
|
13442
|
+
} catch {
|
|
13443
|
+
return redactTextSecrets2(text);
|
|
13444
|
+
}
|
|
13445
|
+
return JSON.stringify(redactSecrets2(parsed));
|
|
13446
|
+
}
|
|
13447
|
+
function redactTextSecrets2(text) {
|
|
13448
|
+
return text.replace(TEXT_URL_USERINFO2, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR2, "$1[REDACTED]");
|
|
13449
|
+
}
|
|
13372
13450
|
function normalizeBaseUrl2(value) {
|
|
13373
13451
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
13374
13452
|
try {
|
|
@@ -13391,7 +13469,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
13391
13469
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13392
13470
|
};
|
|
13393
13471
|
}
|
|
13394
|
-
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
13472
|
+
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2, HttpStatusError2, DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, HttpRequestTimeoutError2, TEXT_URL_USERINFO2, TEXT_SENSITIVE_PAIR2;
|
|
13395
13473
|
var init_util = __esm(() => {
|
|
13396
13474
|
HTTP_STATUS_MESSAGES2 = {
|
|
13397
13475
|
400: "Bad Request",
|
|
@@ -13405,6 +13483,36 @@ var init_util = __esm(() => {
|
|
|
13405
13483
|
503: "Service Unavailable - server temporarily unavailable",
|
|
13406
13484
|
504: "Gateway Timeout - server temporarily unavailable"
|
|
13407
13485
|
};
|
|
13486
|
+
STANDARD_REASON_PHRASES2 = {
|
|
13487
|
+
400: "Bad Request",
|
|
13488
|
+
401: "Unauthorized",
|
|
13489
|
+
403: "Forbidden",
|
|
13490
|
+
404: "Not Found",
|
|
13491
|
+
408: "Request Timeout",
|
|
13492
|
+
409: "Conflict",
|
|
13493
|
+
413: "Payload Too Large",
|
|
13494
|
+
429: "Too Many Requests",
|
|
13495
|
+
500: "Internal Server Error",
|
|
13496
|
+
502: "Bad Gateway",
|
|
13497
|
+
503: "Service Unavailable",
|
|
13498
|
+
504: "Gateway Timeout"
|
|
13499
|
+
};
|
|
13500
|
+
HttpStatusError2 = class HttpStatusError2 extends Error {
|
|
13501
|
+
status;
|
|
13502
|
+
constructor(message, status) {
|
|
13503
|
+
super(message);
|
|
13504
|
+
this.name = "HttpStatusError";
|
|
13505
|
+
this.status = status;
|
|
13506
|
+
}
|
|
13507
|
+
};
|
|
13508
|
+
HttpRequestTimeoutError2 = class HttpRequestTimeoutError2 extends Error {
|
|
13509
|
+
constructor(operation, timeoutMs) {
|
|
13510
|
+
super(`${operation}: request timed out after ${timeoutMs}ms`);
|
|
13511
|
+
this.name = "HttpRequestTimeoutError";
|
|
13512
|
+
}
|
|
13513
|
+
};
|
|
13514
|
+
TEXT_URL_USERINFO2 = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
|
|
13515
|
+
TEXT_SENSITIVE_PAIR2 = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
13408
13516
|
});
|
|
13409
13517
|
|
|
13410
13518
|
// node_modules/commander/esm.mjs
|
|
@@ -13425,7 +13533,7 @@ var {
|
|
|
13425
13533
|
// package.json
|
|
13426
13534
|
var package_default = {
|
|
13427
13535
|
name: "postgresai",
|
|
13428
|
-
version: "0.16.0
|
|
13536
|
+
version: "0.16.0",
|
|
13429
13537
|
description: "postgres_ai CLI",
|
|
13430
13538
|
license: "Apache-2.0",
|
|
13431
13539
|
private: false,
|
|
@@ -13443,8 +13551,15 @@ var package_default = {
|
|
|
13443
13551
|
},
|
|
13444
13552
|
exports: {
|
|
13445
13553
|
".": "./dist/bin/postgres-ai.js",
|
|
13446
|
-
"./cli": "./dist/bin/postgres-ai.js"
|
|
13554
|
+
"./cli": "./dist/bin/postgres-ai.js",
|
|
13555
|
+
"./schemas/*.schema.json": "./schemas/*.schema.json"
|
|
13447
13556
|
},
|
|
13557
|
+
files: [
|
|
13558
|
+
"dist/",
|
|
13559
|
+
"instances.demo.yml",
|
|
13560
|
+
"schemas/",
|
|
13561
|
+
"sql/"
|
|
13562
|
+
],
|
|
13448
13563
|
type: "module",
|
|
13449
13564
|
engines: {
|
|
13450
13565
|
node: ">=18"
|
|
@@ -13453,7 +13568,9 @@ var package_default = {
|
|
|
13453
13568
|
"embed-metrics": "bun run scripts/embed-metrics.ts",
|
|
13454
13569
|
"embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
|
|
13455
13570
|
"embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
|
|
13456
|
-
|
|
13571
|
+
"sync-schemas": "node scripts/sync-schemas.mjs",
|
|
13572
|
+
build: `bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
|
|
13573
|
+
prepack: "npm run sync-schemas",
|
|
13457
13574
|
prepublishOnly: "npm run build",
|
|
13458
13575
|
start: "bun ./bin/postgres-ai.ts --help",
|
|
13459
13576
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
@@ -13504,7 +13621,8 @@ function readConfig() {
|
|
|
13504
13621
|
storageBaseUrl: null,
|
|
13505
13622
|
orgId: null,
|
|
13506
13623
|
defaultProject: null,
|
|
13507
|
-
projectName: null
|
|
13624
|
+
projectName: null,
|
|
13625
|
+
feedbackTipLastShownAt: null
|
|
13508
13626
|
};
|
|
13509
13627
|
const userConfigPath = getConfigPath();
|
|
13510
13628
|
if (fs.existsSync(userConfigPath)) {
|
|
@@ -13517,6 +13635,7 @@ function readConfig() {
|
|
|
13517
13635
|
config.orgId = parsed.orgId ?? null;
|
|
13518
13636
|
config.defaultProject = parsed.defaultProject ?? null;
|
|
13519
13637
|
config.projectName = parsed.projectName ?? null;
|
|
13638
|
+
config.feedbackTipLastShownAt = parsed.feedbackTipLastShownAt ?? null;
|
|
13520
13639
|
return config;
|
|
13521
13640
|
} catch (err) {
|
|
13522
13641
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16256,7 +16375,7 @@ var Result = import_lib.default.Result;
|
|
|
16256
16375
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16257
16376
|
var defaults = import_lib.default.defaults;
|
|
16258
16377
|
// package.json
|
|
16259
|
-
var version = "0.16.0
|
|
16378
|
+
var version = "0.16.0";
|
|
16260
16379
|
var package_default2 = {
|
|
16261
16380
|
name: "postgresai",
|
|
16262
16381
|
version,
|
|
@@ -16277,8 +16396,15 @@ var package_default2 = {
|
|
|
16277
16396
|
},
|
|
16278
16397
|
exports: {
|
|
16279
16398
|
".": "./dist/bin/postgres-ai.js",
|
|
16280
|
-
"./cli": "./dist/bin/postgres-ai.js"
|
|
16399
|
+
"./cli": "./dist/bin/postgres-ai.js",
|
|
16400
|
+
"./schemas/*.schema.json": "./schemas/*.schema.json"
|
|
16281
16401
|
},
|
|
16402
|
+
files: [
|
|
16403
|
+
"dist/",
|
|
16404
|
+
"instances.demo.yml",
|
|
16405
|
+
"schemas/",
|
|
16406
|
+
"sql/"
|
|
16407
|
+
],
|
|
16282
16408
|
type: "module",
|
|
16283
16409
|
engines: {
|
|
16284
16410
|
node: ">=18"
|
|
@@ -16287,7 +16413,9 @@ var package_default2 = {
|
|
|
16287
16413
|
"embed-metrics": "bun run scripts/embed-metrics.ts",
|
|
16288
16414
|
"embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
|
|
16289
16415
|
"embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
|
|
16290
|
-
|
|
16416
|
+
"sync-schemas": "node scripts/sync-schemas.mjs",
|
|
16417
|
+
build: `bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
|
|
16418
|
+
prepack: "npm run sync-schemas",
|
|
16291
16419
|
prepublishOnly: "npm run build",
|
|
16292
16420
|
start: "bun ./bin/postgres-ai.ts --help",
|
|
16293
16421
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
@@ -16338,7 +16466,8 @@ function readConfig2() {
|
|
|
16338
16466
|
storageBaseUrl: null,
|
|
16339
16467
|
orgId: null,
|
|
16340
16468
|
defaultProject: null,
|
|
16341
|
-
projectName: null
|
|
16469
|
+
projectName: null,
|
|
16470
|
+
feedbackTipLastShownAt: null
|
|
16342
16471
|
};
|
|
16343
16472
|
const userConfigPath = getConfigPath2();
|
|
16344
16473
|
if (fs2.existsSync(userConfigPath)) {
|
|
@@ -16351,6 +16480,7 @@ function readConfig2() {
|
|
|
16351
16480
|
config.orgId = parsed.orgId ?? null;
|
|
16352
16481
|
config.defaultProject = parsed.defaultProject ?? null;
|
|
16353
16482
|
config.projectName = parsed.projectName ?? null;
|
|
16483
|
+
config.feedbackTipLastShownAt = parsed.feedbackTipLastShownAt ?? null;
|
|
16354
16484
|
return config;
|
|
16355
16485
|
} catch (err) {
|
|
16356
16486
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16379,6 +16509,28 @@ function readConfig2() {
|
|
|
16379
16509
|
}
|
|
16380
16510
|
return config;
|
|
16381
16511
|
}
|
|
16512
|
+
function writeConfig2(config) {
|
|
16513
|
+
const configDir = getConfigDir2();
|
|
16514
|
+
const configPath = getConfigPath2();
|
|
16515
|
+
if (!fs2.existsSync(configDir)) {
|
|
16516
|
+
fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
16517
|
+
}
|
|
16518
|
+
let existingConfig = {};
|
|
16519
|
+
if (fs2.existsSync(configPath)) {
|
|
16520
|
+
try {
|
|
16521
|
+
const content = fs2.readFileSync(configPath, "utf8");
|
|
16522
|
+
existingConfig = JSON.parse(content);
|
|
16523
|
+
} catch (err) {}
|
|
16524
|
+
}
|
|
16525
|
+
const mergedConfig = {
|
|
16526
|
+
...existingConfig,
|
|
16527
|
+
...config
|
|
16528
|
+
};
|
|
16529
|
+
fs2.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2) + `
|
|
16530
|
+
`, {
|
|
16531
|
+
mode: 384
|
|
16532
|
+
});
|
|
16533
|
+
}
|
|
16382
16534
|
|
|
16383
16535
|
// lib/util.ts
|
|
16384
16536
|
var HTTP_STATUS_MESSAGES = {
|
|
@@ -16398,35 +16550,105 @@ function isHtmlContent(text) {
|
|
|
16398
16550
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
16399
16551
|
}
|
|
16400
16552
|
var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16553
|
+
var STANDARD_REASON_PHRASES = {
|
|
16554
|
+
400: "Bad Request",
|
|
16555
|
+
401: "Unauthorized",
|
|
16556
|
+
403: "Forbidden",
|
|
16557
|
+
404: "Not Found",
|
|
16558
|
+
408: "Request Timeout",
|
|
16559
|
+
409: "Conflict",
|
|
16560
|
+
413: "Payload Too Large",
|
|
16561
|
+
429: "Too Many Requests",
|
|
16562
|
+
500: "Internal Server Error",
|
|
16563
|
+
502: "Bad Gateway",
|
|
16564
|
+
503: "Service Unavailable",
|
|
16565
|
+
504: "Gateway Timeout"
|
|
16566
|
+
};
|
|
16567
|
+
function formatHttpError(operation, status, responseBody, statusText) {
|
|
16568
|
+
const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
|
|
16404
16569
|
const remediation = status === 401 ? `
|
|
16405
16570
|
${AUTH_REMEDIATION_HINT}` : "";
|
|
16406
|
-
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
16571
|
+
let bodyMessage;
|
|
16572
|
+
let bodyDetails;
|
|
16573
|
+
let bodyCode;
|
|
16574
|
+
let bodyHint;
|
|
16575
|
+
if (responseBody && !isHtmlContent(responseBody)) {
|
|
16410
16576
|
try {
|
|
16411
16577
|
const errObj = JSON.parse(responseBody);
|
|
16412
|
-
const message = errObj.message
|
|
16413
|
-
if (
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16578
|
+
const message = errObj.message ?? errObj.error;
|
|
16579
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
16580
|
+
bodyMessage = redactTextSecrets(message.trim());
|
|
16581
|
+
}
|
|
16582
|
+
const details = errObj.details ?? errObj.detail;
|
|
16583
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
16584
|
+
bodyDetails = redactTextSecrets(details.trim());
|
|
16585
|
+
}
|
|
16586
|
+
if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
|
|
16587
|
+
bodyCode = errObj.code.trim();
|
|
16588
|
+
}
|
|
16589
|
+
if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
|
|
16590
|
+
bodyHint = redactTextSecrets(errObj.hint.trim());
|
|
16591
|
+
}
|
|
16592
|
+
if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
|
|
16593
|
+
bodyDetails = redactSecretsForLog(JSON.stringify(errObj));
|
|
16419
16594
|
}
|
|
16420
16595
|
} catch {
|
|
16421
16596
|
const trimmed = responseBody.trim();
|
|
16422
16597
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
16423
|
-
|
|
16424
|
-
${trimmed}`;
|
|
16598
|
+
bodyDetails = redactTextSecrets(trimmed);
|
|
16425
16599
|
}
|
|
16426
16600
|
}
|
|
16427
16601
|
}
|
|
16602
|
+
const trimmedReason = statusText?.trim();
|
|
16603
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
16604
|
+
const safeReasonPhrase = reasonPhrase ? redactTextSecrets(reasonPhrase) : undefined;
|
|
16605
|
+
const headline = bodyMessage ?? safeReasonPhrase ?? generic;
|
|
16606
|
+
const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
|
|
16607
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
|
|
16608
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
16609
|
+
errMsg += `
|
|
16610
|
+
${bodyDetails}`;
|
|
16611
|
+
}
|
|
16612
|
+
if (bodyHint) {
|
|
16613
|
+
errMsg += `
|
|
16614
|
+
Hint: ${bodyHint}`;
|
|
16615
|
+
}
|
|
16428
16616
|
return errMsg + remediation;
|
|
16429
16617
|
}
|
|
16618
|
+
function describeFetchError(operation, url, err) {
|
|
16619
|
+
const cause = err?.cause;
|
|
16620
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
16621
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
16622
|
+
}
|
|
16623
|
+
|
|
16624
|
+
class HttpStatusError extends Error {
|
|
16625
|
+
status;
|
|
16626
|
+
constructor(message, status) {
|
|
16627
|
+
super(message);
|
|
16628
|
+
this.name = "HttpStatusError";
|
|
16629
|
+
this.status = status;
|
|
16630
|
+
}
|
|
16631
|
+
}
|
|
16632
|
+
function isRetryableHttpStatus(status) {
|
|
16633
|
+
return status >= 500 || status === 429;
|
|
16634
|
+
}
|
|
16635
|
+
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
|
|
16636
|
+
|
|
16637
|
+
class HttpRequestTimeoutError extends Error {
|
|
16638
|
+
constructor(operation, timeoutMs) {
|
|
16639
|
+
super(`${operation}: request timed out after ${timeoutMs}ms`);
|
|
16640
|
+
this.name = "HttpRequestTimeoutError";
|
|
16641
|
+
}
|
|
16642
|
+
}
|
|
16643
|
+
function requestTimeoutSignal(timeoutMs) {
|
|
16644
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS;
|
|
16645
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, Math.floor(requested)));
|
|
16646
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
16647
|
+
}
|
|
16648
|
+
function isFetchTimeout(err) {
|
|
16649
|
+
const name = err?.name;
|
|
16650
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
16651
|
+
}
|
|
16430
16652
|
function maskSecret(secret) {
|
|
16431
16653
|
if (!secret)
|
|
16432
16654
|
return "";
|
|
@@ -16436,6 +16658,37 @@ function maskSecret(secret) {
|
|
|
16436
16658
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
16437
16659
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
16438
16660
|
}
|
|
16661
|
+
function isSensitiveLogKey(key) {
|
|
16662
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
16663
|
+
return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
|
|
16664
|
+
}
|
|
16665
|
+
function redactSecrets(value) {
|
|
16666
|
+
if (Array.isArray(value)) {
|
|
16667
|
+
return value.map(redactSecrets);
|
|
16668
|
+
}
|
|
16669
|
+
if (value && typeof value === "object") {
|
|
16670
|
+
const out = {};
|
|
16671
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16672
|
+
out[key] = isSensitiveLogKey(key) && child != null ? "[REDACTED]" : redactSecrets(child);
|
|
16673
|
+
}
|
|
16674
|
+
return out;
|
|
16675
|
+
}
|
|
16676
|
+
return value;
|
|
16677
|
+
}
|
|
16678
|
+
function redactSecretsForLog(text) {
|
|
16679
|
+
let parsed;
|
|
16680
|
+
try {
|
|
16681
|
+
parsed = JSON.parse(text);
|
|
16682
|
+
} catch {
|
|
16683
|
+
return redactTextSecrets(text);
|
|
16684
|
+
}
|
|
16685
|
+
return JSON.stringify(redactSecrets(parsed));
|
|
16686
|
+
}
|
|
16687
|
+
var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
|
|
16688
|
+
var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
16689
|
+
function redactTextSecrets(text) {
|
|
16690
|
+
return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
|
|
16691
|
+
}
|
|
16439
16692
|
function normalizeBaseUrl(value) {
|
|
16440
16693
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
16441
16694
|
try {
|
|
@@ -25801,117 +26054,846 @@ function renderMarkdownForTerminal(md) {
|
|
|
25801
26054
|
`);
|
|
25802
26055
|
}
|
|
25803
26056
|
|
|
25804
|
-
//
|
|
25805
|
-
|
|
25806
|
-
|
|
25807
|
-
|
|
25808
|
-
|
|
25809
|
-
|
|
25810
|
-
|
|
25811
|
-
|
|
25812
|
-
|
|
25813
|
-
|
|
25814
|
-
|
|
25815
|
-
|
|
25816
|
-
|
|
26057
|
+
// lib/joe.ts
|
|
26058
|
+
var DEFAULT_BUDGET_MS = 25000;
|
|
26059
|
+
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
26060
|
+
async function callRpc(params) {
|
|
26061
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26062
|
+
if (!apiKey) {
|
|
26063
|
+
throw new Error("API key is required");
|
|
26064
|
+
}
|
|
26065
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26066
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
26067
|
+
const payload = JSON.stringify(body);
|
|
26068
|
+
const headers = {
|
|
26069
|
+
"access-token": apiKey,
|
|
26070
|
+
"Content-Type": "application/json",
|
|
26071
|
+
Connection: "close"
|
|
26072
|
+
};
|
|
26073
|
+
if (debug) {
|
|
26074
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26075
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26076
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26077
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
26078
|
+
}
|
|
26079
|
+
let response;
|
|
26080
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
26081
|
+
try {
|
|
26082
|
+
response = await fetch(url.toString(), {
|
|
26083
|
+
method: "POST",
|
|
26084
|
+
headers,
|
|
26085
|
+
body: payload,
|
|
26086
|
+
signal: requestTimeout.signal
|
|
26087
|
+
});
|
|
26088
|
+
} catch (err) {
|
|
26089
|
+
if (isFetchTimeout(err)) {
|
|
26090
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26091
|
+
}
|
|
26092
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26093
|
+
}
|
|
26094
|
+
const text = await response.text();
|
|
26095
|
+
if (debug) {
|
|
26096
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26097
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26098
|
+
}
|
|
26099
|
+
if (!response.ok) {
|
|
26100
|
+
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26101
|
+
}
|
|
26102
|
+
try {
|
|
26103
|
+
return JSON.parse(text);
|
|
26104
|
+
} catch {
|
|
26105
|
+
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26106
|
+
}
|
|
25817
26107
|
}
|
|
25818
|
-
function
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
|
|
25822
|
-
|
|
25823
|
-
|
|
26108
|
+
async function startCommand(params) {
|
|
26109
|
+
const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
|
|
26110
|
+
if (!String(command ?? "").trim()) {
|
|
26111
|
+
throw new Error("command text is required");
|
|
26112
|
+
}
|
|
26113
|
+
const commandId = await callRpc({
|
|
26114
|
+
apiKey,
|
|
26115
|
+
apiBaseUrl,
|
|
26116
|
+
fn: "joe_command_run",
|
|
26117
|
+
body: { instance_id: instanceId, command },
|
|
26118
|
+
operation: "Failed to run Joe command",
|
|
26119
|
+
debug,
|
|
26120
|
+
timeoutMs
|
|
26121
|
+
});
|
|
26122
|
+
if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
|
|
26123
|
+
throw new Error(`Failed to run Joe command: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(commandId))}`);
|
|
26124
|
+
}
|
|
26125
|
+
return commandId;
|
|
25824
26126
|
}
|
|
25825
|
-
function
|
|
25826
|
-
|
|
25827
|
-
if (
|
|
25828
|
-
|
|
25829
|
-
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
25830
|
-
key = sourceKeys[index];
|
|
25831
|
-
target[key] = source[key];
|
|
25832
|
-
}
|
|
26127
|
+
async function getCommandOutput(params) {
|
|
26128
|
+
const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
|
|
26129
|
+
if (!commandId) {
|
|
26130
|
+
throw new Error("commandId is required");
|
|
25833
26131
|
}
|
|
25834
|
-
return
|
|
26132
|
+
return callRpc({
|
|
26133
|
+
apiKey,
|
|
26134
|
+
apiBaseUrl,
|
|
26135
|
+
fn: "joe_command_output",
|
|
26136
|
+
body: { command_id: commandId },
|
|
26137
|
+
operation: "Failed to fetch command output",
|
|
26138
|
+
debug,
|
|
26139
|
+
timeoutMs
|
|
26140
|
+
});
|
|
25835
26141
|
}
|
|
25836
|
-
function
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
26142
|
+
function preserveIntegerId(value) {
|
|
26143
|
+
if (typeof value === "number")
|
|
26144
|
+
return value;
|
|
26145
|
+
const parsed = Number(value);
|
|
26146
|
+
return Number.isSafeInteger(parsed) ? parsed : value;
|
|
26147
|
+
}
|
|
26148
|
+
function normalizeProjectRow(row) {
|
|
26149
|
+
return {
|
|
26150
|
+
project_id: preserveIntegerId(row.project_id ?? 0),
|
|
26151
|
+
alias: row.alias ?? null,
|
|
26152
|
+
name: row.name ?? null,
|
|
26153
|
+
joe_ready: Boolean(row.joe_ready ?? false),
|
|
26154
|
+
tunnel: Boolean(row.tunnel ?? false),
|
|
26155
|
+
instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
|
|
26156
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
|
|
26157
|
+
};
|
|
26158
|
+
}
|
|
26159
|
+
async function listProjects(params) {
|
|
26160
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
26161
|
+
const body = {};
|
|
26162
|
+
if (typeof orgId === "number") {
|
|
26163
|
+
body.org_id = orgId;
|
|
25840
26164
|
}
|
|
25841
|
-
|
|
26165
|
+
const rows = await callRpc({
|
|
26166
|
+
apiKey,
|
|
26167
|
+
apiBaseUrl,
|
|
26168
|
+
fn: "projects_list",
|
|
26169
|
+
body,
|
|
26170
|
+
operation: "Failed to list projects",
|
|
26171
|
+
debug
|
|
26172
|
+
});
|
|
26173
|
+
if (!Array.isArray(rows)) {
|
|
26174
|
+
return [];
|
|
26175
|
+
}
|
|
26176
|
+
return rows.map(normalizeProjectRow);
|
|
25842
26177
|
}
|
|
25843
|
-
function
|
|
25844
|
-
return
|
|
26178
|
+
function isNumericProjectRef(ref) {
|
|
26179
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
25845
26180
|
}
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
25850
|
-
var isNegativeZero_12 = isNegativeZero2;
|
|
25851
|
-
var extend_12 = extend4;
|
|
25852
|
-
var common2 = {
|
|
25853
|
-
isNothing: isNothing_12,
|
|
25854
|
-
isObject: isObject_12,
|
|
25855
|
-
toArray: toArray_12,
|
|
25856
|
-
repeat: repeat_12,
|
|
25857
|
-
isNegativeZero: isNegativeZero_12,
|
|
25858
|
-
extend: extend_12
|
|
25859
|
-
};
|
|
25860
|
-
function formatError3(exception2, compact) {
|
|
25861
|
-
var where = "", message = exception2.reason || "(unknown reason)";
|
|
25862
|
-
if (!exception2.mark)
|
|
25863
|
-
return message;
|
|
25864
|
-
if (exception2.mark.name) {
|
|
25865
|
-
where += 'in "' + exception2.mark.name + '" ';
|
|
26181
|
+
async function resolveJoeInstanceId(params) {
|
|
26182
|
+
const ref = String(params.project ?? "").trim();
|
|
26183
|
+
if (!ref) {
|
|
26184
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
25866
26185
|
}
|
|
25867
|
-
|
|
25868
|
-
|
|
25869
|
-
|
|
25870
|
-
|
|
25871
|
-
|
|
26186
|
+
const projects = await listProjects({
|
|
26187
|
+
apiKey: params.apiKey,
|
|
26188
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26189
|
+
orgId: params.orgId,
|
|
26190
|
+
debug: params.debug
|
|
26191
|
+
});
|
|
26192
|
+
const needle = ref.toLowerCase();
|
|
26193
|
+
const match = isNumericProjectRef(ref) ? projects.find((p) => String(p.project_id) === String(preserveIntegerId(ref))) : projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
26194
|
+
if (!match) {
|
|
26195
|
+
throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
26196
|
+
}
|
|
26197
|
+
if (match.instance_id == null) {
|
|
26198
|
+
throw new Error(`Project '${ref}' has no Joe instance. Run 'pgai projects' to see which projects have Joe ready.`);
|
|
26199
|
+
}
|
|
26200
|
+
return match.instance_id;
|
|
26201
|
+
}
|
|
26202
|
+
var DESCRIBE_VARIANTS = [
|
|
26203
|
+
"\\d",
|
|
26204
|
+
"\\d+",
|
|
26205
|
+
"\\dt",
|
|
26206
|
+
"\\dt+",
|
|
26207
|
+
"\\di",
|
|
26208
|
+
"\\di+",
|
|
26209
|
+
"\\l",
|
|
26210
|
+
"\\l+",
|
|
26211
|
+
"\\dv",
|
|
26212
|
+
"\\dv+",
|
|
26213
|
+
"\\dm",
|
|
26214
|
+
"\\dm+"
|
|
26215
|
+
];
|
|
26216
|
+
function buildJoeCommandText(command, input = {}) {
|
|
26217
|
+
const arg = String(input.arg ?? "").trim();
|
|
26218
|
+
switch (command) {
|
|
26219
|
+
case "plan":
|
|
26220
|
+
case "explain":
|
|
26221
|
+
case "exec":
|
|
26222
|
+
case "hypo": {
|
|
26223
|
+
if (!arg) {
|
|
26224
|
+
throw new Error(`${command} requires an argument`);
|
|
26225
|
+
}
|
|
26226
|
+
return `${command} ${arg}`;
|
|
26227
|
+
}
|
|
26228
|
+
case "activity":
|
|
26229
|
+
case "reset":
|
|
26230
|
+
return command;
|
|
26231
|
+
case "terminate": {
|
|
26232
|
+
if (!/^[1-9][0-9]*$/.test(arg)) {
|
|
26233
|
+
throw new Error("pid must be a positive integer");
|
|
26234
|
+
}
|
|
26235
|
+
return `terminate ${arg}`;
|
|
26236
|
+
}
|
|
26237
|
+
case "describe": {
|
|
26238
|
+
if (!arg) {
|
|
26239
|
+
throw new Error("describe requires an object name");
|
|
26240
|
+
}
|
|
26241
|
+
const variant = String(input.variant ?? "\\d").trim();
|
|
26242
|
+
if (!DESCRIBE_VARIANTS.includes(variant)) {
|
|
26243
|
+
throw new Error(`Unsupported describe variant '${variant}'. Supported: ${DESCRIBE_VARIANTS.join(" ")}`);
|
|
26244
|
+
}
|
|
26245
|
+
return `${variant} ${arg}`;
|
|
26246
|
+
}
|
|
26247
|
+
}
|
|
26248
|
+
}
|
|
26249
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26250
|
+
async function runCommand(params) {
|
|
26251
|
+
const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
|
|
26252
|
+
const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : DEFAULT_BUDGET_MS;
|
|
26253
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
26254
|
+
const now = params.now ?? Date.now;
|
|
26255
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
26256
|
+
const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
|
|
26257
|
+
const deadline = now() + budgetMs;
|
|
26258
|
+
let status = "pending";
|
|
26259
|
+
const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
|
|
26260
|
+
if (now() >= deadline) {
|
|
26261
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26262
|
+
}
|
|
26263
|
+
for (;; ) {
|
|
26264
|
+
let output;
|
|
26265
|
+
try {
|
|
26266
|
+
output = await getCommandOutput({
|
|
26267
|
+
apiKey,
|
|
26268
|
+
apiBaseUrl,
|
|
26269
|
+
commandId,
|
|
26270
|
+
debug,
|
|
26271
|
+
timeoutMs: remainingRequestMs()
|
|
26272
|
+
});
|
|
26273
|
+
} catch (err) {
|
|
26274
|
+
if (err instanceof HttpRequestTimeoutError) {
|
|
26275
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26276
|
+
}
|
|
26277
|
+
if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
|
|
26278
|
+
if (now() >= deadline) {
|
|
26279
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26280
|
+
}
|
|
26281
|
+
await sleep(pollIntervalMs);
|
|
26282
|
+
continue;
|
|
26283
|
+
}
|
|
26284
|
+
throw err;
|
|
26285
|
+
}
|
|
26286
|
+
status = output.status;
|
|
26287
|
+
if (status === "ok" || status === "error") {
|
|
26288
|
+
return { commandId, status, output, budgetExpired: false };
|
|
26289
|
+
}
|
|
26290
|
+
if (now() >= deadline) {
|
|
26291
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26292
|
+
}
|
|
26293
|
+
await sleep(pollIntervalMs);
|
|
25872
26294
|
}
|
|
25873
|
-
return message + " " + where;
|
|
25874
26295
|
}
|
|
25875
|
-
function
|
|
25876
|
-
|
|
25877
|
-
|
|
25878
|
-
|
|
25879
|
-
|
|
25880
|
-
|
|
25881
|
-
|
|
25882
|
-
|
|
26296
|
+
async function executeJoeCommand(params) {
|
|
26297
|
+
const commandText = buildJoeCommandText(params.command, params.input);
|
|
26298
|
+
let instanceId;
|
|
26299
|
+
const directRef = String(params.instanceId ?? "").trim();
|
|
26300
|
+
if (directRef) {
|
|
26301
|
+
if (!/^[0-9]+$/.test(directRef)) {
|
|
26302
|
+
throw new Error("instanceId must be a numeric Joe instance id");
|
|
26303
|
+
}
|
|
26304
|
+
instanceId = directRef;
|
|
26305
|
+
} else if (String(params.project ?? "").trim()) {
|
|
26306
|
+
instanceId = await resolveJoeInstanceId({
|
|
26307
|
+
apiKey: params.apiKey,
|
|
26308
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26309
|
+
project: String(params.project),
|
|
26310
|
+
orgId: params.orgId,
|
|
26311
|
+
debug: params.debug
|
|
26312
|
+
});
|
|
25883
26313
|
} else {
|
|
25884
|
-
|
|
25885
|
-
}
|
|
26314
|
+
throw new Error("either instanceId or project is required");
|
|
26315
|
+
}
|
|
26316
|
+
const outcome = await runCommand({
|
|
26317
|
+
apiKey: params.apiKey,
|
|
26318
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26319
|
+
instanceId,
|
|
26320
|
+
command: commandText,
|
|
26321
|
+
budgetMs: params.budgetMs,
|
|
26322
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
26323
|
+
debug: params.debug,
|
|
26324
|
+
now: params.now,
|
|
26325
|
+
sleep: params.sleep
|
|
26326
|
+
});
|
|
26327
|
+
return { ...outcome, command: params.command, instanceId, commandText };
|
|
25886
26328
|
}
|
|
25887
|
-
|
|
25888
|
-
|
|
25889
|
-
|
|
25890
|
-
|
|
25891
|
-
|
|
25892
|
-
|
|
25893
|
-
|
|
25894
|
-
|
|
25895
|
-
|
|
25896
|
-
|
|
25897
|
-
|
|
25898
|
-
|
|
25899
|
-
|
|
26329
|
+
function clientSidePlanFlags(planJson) {
|
|
26330
|
+
const flags = [];
|
|
26331
|
+
const walk = (node) => {
|
|
26332
|
+
if (!node || typeof node !== "object") {
|
|
26333
|
+
return;
|
|
26334
|
+
}
|
|
26335
|
+
const nodeType = node["Node Type"];
|
|
26336
|
+
if (nodeType === "Seq Scan") {
|
|
26337
|
+
const rel = node["Relation Name"];
|
|
26338
|
+
flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
|
|
26339
|
+
}
|
|
26340
|
+
if (Array.isArray(node.Plans)) {
|
|
26341
|
+
for (const child of node.Plans) {
|
|
26342
|
+
walk(child);
|
|
26343
|
+
}
|
|
26344
|
+
}
|
|
26345
|
+
};
|
|
26346
|
+
if (Array.isArray(planJson)) {
|
|
26347
|
+
for (const entry of planJson) {
|
|
26348
|
+
if (entry && typeof entry === "object") {
|
|
26349
|
+
walk(entry.Plan ?? entry);
|
|
26350
|
+
}
|
|
26351
|
+
}
|
|
26352
|
+
return flags;
|
|
25900
26353
|
}
|
|
25901
|
-
if (
|
|
25902
|
-
|
|
25903
|
-
|
|
26354
|
+
if (planJson && typeof planJson === "object") {
|
|
26355
|
+
const root = planJson;
|
|
26356
|
+
walk(root.Plan ?? planJson);
|
|
25904
26357
|
}
|
|
25905
|
-
return
|
|
25906
|
-
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
25907
|
-
pos: position - lineStart + head.length
|
|
25908
|
-
};
|
|
26358
|
+
return flags;
|
|
25909
26359
|
}
|
|
25910
|
-
function
|
|
25911
|
-
|
|
26360
|
+
function formatJoeOutput(output) {
|
|
26361
|
+
const lines = [];
|
|
26362
|
+
const section = (value, label) => {
|
|
26363
|
+
if (value == null || value.trim() === "")
|
|
26364
|
+
return;
|
|
26365
|
+
if (lines.length > 0)
|
|
26366
|
+
lines.push("");
|
|
26367
|
+
if (label)
|
|
26368
|
+
lines.push(`${label}:`);
|
|
26369
|
+
lines.push(value);
|
|
26370
|
+
};
|
|
26371
|
+
section(output.response);
|
|
26372
|
+
section(output.plan_text, "plan");
|
|
26373
|
+
const flags = clientSidePlanFlags(output.plan_json).map((flag) => `⚑ ${flag}`);
|
|
26374
|
+
if (flags.length > 0) {
|
|
26375
|
+
lines.push(...flags);
|
|
26376
|
+
}
|
|
26377
|
+
section(output.plan_execution_text, "execution plan (EXPLAIN ANALYZE)");
|
|
26378
|
+
section(output.stats, "stats");
|
|
26379
|
+
section(output.recommendations, "recommendations");
|
|
26380
|
+
if (output.queryid) {
|
|
26381
|
+
if (lines.length > 0)
|
|
26382
|
+
lines.push("");
|
|
26383
|
+
lines.push(`(queryid ${output.queryid})`);
|
|
26384
|
+
}
|
|
26385
|
+
return lines.join(`
|
|
26386
|
+
`);
|
|
25912
26387
|
}
|
|
25913
|
-
function
|
|
25914
|
-
|
|
26388
|
+
function formatProjectsTable(projects) {
|
|
26389
|
+
const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
|
|
26390
|
+
const rows = projects.map((p) => [
|
|
26391
|
+
String(p.project_id),
|
|
26392
|
+
p.alias ?? "-",
|
|
26393
|
+
p.name ?? "-",
|
|
26394
|
+
p.joe_ready ? "ready" : "no",
|
|
26395
|
+
p.tunnel ? "yes" : "no"
|
|
26396
|
+
]);
|
|
26397
|
+
const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
|
|
26398
|
+
const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
|
|
26399
|
+
return [pad(header), ...rows.map(pad)].join(`
|
|
26400
|
+
`);
|
|
26401
|
+
}
|
|
26402
|
+
|
|
26403
|
+
// lib/joe.ts
|
|
26404
|
+
async function callRpc2(params) {
|
|
26405
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26406
|
+
if (!apiKey) {
|
|
26407
|
+
throw new Error("API key is required");
|
|
26408
|
+
}
|
|
26409
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26410
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
26411
|
+
const payload = JSON.stringify(body);
|
|
26412
|
+
const headers = {
|
|
26413
|
+
"access-token": apiKey,
|
|
26414
|
+
"Content-Type": "application/json",
|
|
26415
|
+
Connection: "close"
|
|
26416
|
+
};
|
|
26417
|
+
if (debug) {
|
|
26418
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26419
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26420
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26421
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
26422
|
+
}
|
|
26423
|
+
let response;
|
|
26424
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
26425
|
+
try {
|
|
26426
|
+
response = await fetch(url.toString(), {
|
|
26427
|
+
method: "POST",
|
|
26428
|
+
headers,
|
|
26429
|
+
body: payload,
|
|
26430
|
+
signal: requestTimeout.signal
|
|
26431
|
+
});
|
|
26432
|
+
} catch (err) {
|
|
26433
|
+
if (isFetchTimeout(err)) {
|
|
26434
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26435
|
+
}
|
|
26436
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26437
|
+
}
|
|
26438
|
+
const text = await response.text();
|
|
26439
|
+
if (debug) {
|
|
26440
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26441
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26442
|
+
}
|
|
26443
|
+
if (!response.ok) {
|
|
26444
|
+
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26445
|
+
}
|
|
26446
|
+
try {
|
|
26447
|
+
return JSON.parse(text);
|
|
26448
|
+
} catch {
|
|
26449
|
+
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26450
|
+
}
|
|
26451
|
+
}
|
|
26452
|
+
function preserveIntegerId2(value) {
|
|
26453
|
+
if (typeof value === "number")
|
|
26454
|
+
return value;
|
|
26455
|
+
const parsed = Number(value);
|
|
26456
|
+
return Number.isSafeInteger(parsed) ? parsed : value;
|
|
26457
|
+
}
|
|
26458
|
+
function normalizeProjectRow2(row) {
|
|
26459
|
+
return {
|
|
26460
|
+
project_id: preserveIntegerId2(row.project_id ?? 0),
|
|
26461
|
+
alias: row.alias ?? null,
|
|
26462
|
+
name: row.name ?? null,
|
|
26463
|
+
joe_ready: Boolean(row.joe_ready ?? false),
|
|
26464
|
+
tunnel: Boolean(row.tunnel ?? false),
|
|
26465
|
+
instance_id: row.instance_id == null ? null : preserveIntegerId2(row.instance_id),
|
|
26466
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id)
|
|
26467
|
+
};
|
|
26468
|
+
}
|
|
26469
|
+
async function listProjects2(params) {
|
|
26470
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
26471
|
+
const body = {};
|
|
26472
|
+
if (typeof orgId === "number") {
|
|
26473
|
+
body.org_id = orgId;
|
|
26474
|
+
}
|
|
26475
|
+
const rows = await callRpc2({
|
|
26476
|
+
apiKey,
|
|
26477
|
+
apiBaseUrl,
|
|
26478
|
+
fn: "projects_list",
|
|
26479
|
+
body,
|
|
26480
|
+
operation: "Failed to list projects",
|
|
26481
|
+
debug
|
|
26482
|
+
});
|
|
26483
|
+
if (!Array.isArray(rows)) {
|
|
26484
|
+
return [];
|
|
26485
|
+
}
|
|
26486
|
+
return rows.map(normalizeProjectRow2);
|
|
26487
|
+
}
|
|
26488
|
+
function isNumericProjectRef2(ref) {
|
|
26489
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
26490
|
+
}
|
|
26491
|
+
|
|
26492
|
+
// lib/dblab.ts
|
|
26493
|
+
async function resolveDblabInstanceId(params) {
|
|
26494
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
26495
|
+
if (!apiKey) {
|
|
26496
|
+
throw new Error("API key is required");
|
|
26497
|
+
}
|
|
26498
|
+
const ref = String(params.project ?? "").trim();
|
|
26499
|
+
if (!ref) {
|
|
26500
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
26501
|
+
}
|
|
26502
|
+
let projects;
|
|
26503
|
+
try {
|
|
26504
|
+
projects = await listProjects2({ apiKey, apiBaseUrl, orgId, debug });
|
|
26505
|
+
} catch (err) {
|
|
26506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26507
|
+
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
26508
|
+
}
|
|
26509
|
+
const numeric = isNumericProjectRef2(ref);
|
|
26510
|
+
const needle = ref.toLowerCase();
|
|
26511
|
+
const match = projects.find((p) => {
|
|
26512
|
+
if (numeric) {
|
|
26513
|
+
return String(p.project_id) === ref;
|
|
26514
|
+
}
|
|
26515
|
+
return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle;
|
|
26516
|
+
});
|
|
26517
|
+
if (!match) {
|
|
26518
|
+
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
26519
|
+
}
|
|
26520
|
+
if (match.dblab_instance_id == null) {
|
|
26521
|
+
throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
|
|
26522
|
+
}
|
|
26523
|
+
return String(match.dblab_instance_id);
|
|
26524
|
+
}
|
|
26525
|
+
async function callDblabApi(params) {
|
|
26526
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
26527
|
+
if (!apiKey) {
|
|
26528
|
+
throw new Error("API key is required");
|
|
26529
|
+
}
|
|
26530
|
+
if (!instanceId) {
|
|
26531
|
+
throw new Error("instanceId is required");
|
|
26532
|
+
}
|
|
26533
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26534
|
+
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
26535
|
+
const bodyObj = {
|
|
26536
|
+
instance_id: instanceId,
|
|
26537
|
+
action,
|
|
26538
|
+
method
|
|
26539
|
+
};
|
|
26540
|
+
if (data !== undefined) {
|
|
26541
|
+
bodyObj.data = data;
|
|
26542
|
+
}
|
|
26543
|
+
const body = JSON.stringify(bodyObj);
|
|
26544
|
+
const headers = {
|
|
26545
|
+
"access-token": apiKey,
|
|
26546
|
+
"Content-Type": "application/json",
|
|
26547
|
+
Connection: "close"
|
|
26548
|
+
};
|
|
26549
|
+
if (debug) {
|
|
26550
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26551
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26552
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26553
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
|
|
26554
|
+
}
|
|
26555
|
+
let response;
|
|
26556
|
+
const requestTimeout = requestTimeoutSignal();
|
|
26557
|
+
try {
|
|
26558
|
+
response = await fetch(url.toString(), {
|
|
26559
|
+
method: "POST",
|
|
26560
|
+
headers,
|
|
26561
|
+
body,
|
|
26562
|
+
signal: requestTimeout.signal
|
|
26563
|
+
});
|
|
26564
|
+
} catch (err) {
|
|
26565
|
+
if (isFetchTimeout(err)) {
|
|
26566
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26567
|
+
}
|
|
26568
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26569
|
+
}
|
|
26570
|
+
const text = await response.text();
|
|
26571
|
+
if (debug) {
|
|
26572
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26573
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26574
|
+
}
|
|
26575
|
+
if (!response.ok) {
|
|
26576
|
+
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26577
|
+
}
|
|
26578
|
+
if (text.trim() === "") {
|
|
26579
|
+
return null;
|
|
26580
|
+
}
|
|
26581
|
+
try {
|
|
26582
|
+
return JSON.parse(text);
|
|
26583
|
+
} catch {
|
|
26584
|
+
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26585
|
+
}
|
|
26586
|
+
}
|
|
26587
|
+
async function createClone(params) {
|
|
26588
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
26589
|
+
const data = { protected: Boolean(isProtected) };
|
|
26590
|
+
if (cloneId)
|
|
26591
|
+
data.id = cloneId;
|
|
26592
|
+
if (branch)
|
|
26593
|
+
data.branch = branch;
|
|
26594
|
+
if (snapshotId)
|
|
26595
|
+
data.snapshot = { id: snapshotId };
|
|
26596
|
+
if (dbUser && dbPassword)
|
|
26597
|
+
data.db = { username: dbUser, password: dbPassword };
|
|
26598
|
+
return callDblabApi({
|
|
26599
|
+
apiKey,
|
|
26600
|
+
apiBaseUrl,
|
|
26601
|
+
instanceId,
|
|
26602
|
+
action: "/clone",
|
|
26603
|
+
method: "post",
|
|
26604
|
+
data,
|
|
26605
|
+
operation: "Failed to create clone",
|
|
26606
|
+
debug
|
|
26607
|
+
});
|
|
26608
|
+
}
|
|
26609
|
+
async function listClones(params) {
|
|
26610
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
26611
|
+
return callDblabApi({
|
|
26612
|
+
apiKey,
|
|
26613
|
+
apiBaseUrl,
|
|
26614
|
+
instanceId,
|
|
26615
|
+
action: "/clones",
|
|
26616
|
+
method: "get",
|
|
26617
|
+
operation: "Failed to list clones",
|
|
26618
|
+
debug
|
|
26619
|
+
});
|
|
26620
|
+
}
|
|
26621
|
+
async function getClone(params) {
|
|
26622
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
26623
|
+
if (!cloneId)
|
|
26624
|
+
throw new Error("cloneId is required");
|
|
26625
|
+
return callDblabApi({
|
|
26626
|
+
apiKey,
|
|
26627
|
+
apiBaseUrl,
|
|
26628
|
+
instanceId,
|
|
26629
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
26630
|
+
method: "get",
|
|
26631
|
+
operation: "Failed to get clone",
|
|
26632
|
+
debug
|
|
26633
|
+
});
|
|
26634
|
+
}
|
|
26635
|
+
async function resetClone(params) {
|
|
26636
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
26637
|
+
if (!cloneId)
|
|
26638
|
+
throw new Error("cloneId is required");
|
|
26639
|
+
const data = { latest: latest ?? !snapshotId };
|
|
26640
|
+
if (snapshotId)
|
|
26641
|
+
data.snapshotID = snapshotId;
|
|
26642
|
+
return callDblabApi({
|
|
26643
|
+
apiKey,
|
|
26644
|
+
apiBaseUrl,
|
|
26645
|
+
instanceId,
|
|
26646
|
+
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
26647
|
+
method: "post",
|
|
26648
|
+
data,
|
|
26649
|
+
operation: "Failed to reset clone",
|
|
26650
|
+
debug
|
|
26651
|
+
});
|
|
26652
|
+
}
|
|
26653
|
+
async function destroyClone(params) {
|
|
26654
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
26655
|
+
if (!cloneId)
|
|
26656
|
+
throw new Error("cloneId is required");
|
|
26657
|
+
return callDblabApi({
|
|
26658
|
+
apiKey,
|
|
26659
|
+
apiBaseUrl,
|
|
26660
|
+
instanceId,
|
|
26661
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
26662
|
+
method: "delete",
|
|
26663
|
+
operation: "Failed to destroy clone",
|
|
26664
|
+
debug
|
|
26665
|
+
});
|
|
26666
|
+
}
|
|
26667
|
+
async function listBranches(params) {
|
|
26668
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
26669
|
+
return callDblabApi({
|
|
26670
|
+
apiKey,
|
|
26671
|
+
apiBaseUrl,
|
|
26672
|
+
instanceId,
|
|
26673
|
+
action: "/branches",
|
|
26674
|
+
method: "get",
|
|
26675
|
+
operation: "Failed to list branches",
|
|
26676
|
+
debug
|
|
26677
|
+
});
|
|
26678
|
+
}
|
|
26679
|
+
async function createBranch(params) {
|
|
26680
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
26681
|
+
if (!branchName)
|
|
26682
|
+
throw new Error("branchName is required");
|
|
26683
|
+
const data = { branchName };
|
|
26684
|
+
if (baseBranch)
|
|
26685
|
+
data.baseBranch = baseBranch;
|
|
26686
|
+
if (snapshotId)
|
|
26687
|
+
data.snapshotID = snapshotId;
|
|
26688
|
+
return callDblabApi({
|
|
26689
|
+
apiKey,
|
|
26690
|
+
apiBaseUrl,
|
|
26691
|
+
instanceId,
|
|
26692
|
+
action: "/branch",
|
|
26693
|
+
method: "post",
|
|
26694
|
+
data,
|
|
26695
|
+
operation: "Failed to create branch",
|
|
26696
|
+
debug
|
|
26697
|
+
});
|
|
26698
|
+
}
|
|
26699
|
+
async function deleteBranch(params) {
|
|
26700
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
26701
|
+
if (!branchName)
|
|
26702
|
+
throw new Error("branchName is required");
|
|
26703
|
+
return callDblabApi({
|
|
26704
|
+
apiKey,
|
|
26705
|
+
apiBaseUrl,
|
|
26706
|
+
instanceId,
|
|
26707
|
+
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
26708
|
+
method: "delete",
|
|
26709
|
+
operation: "Failed to delete branch",
|
|
26710
|
+
debug
|
|
26711
|
+
});
|
|
26712
|
+
}
|
|
26713
|
+
async function branchLog(params) {
|
|
26714
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
26715
|
+
if (!branchName)
|
|
26716
|
+
throw new Error("branchName is required");
|
|
26717
|
+
return callDblabApi({
|
|
26718
|
+
apiKey,
|
|
26719
|
+
apiBaseUrl,
|
|
26720
|
+
instanceId,
|
|
26721
|
+
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
26722
|
+
method: "get",
|
|
26723
|
+
operation: "Failed to fetch branch log",
|
|
26724
|
+
debug
|
|
26725
|
+
});
|
|
26726
|
+
}
|
|
26727
|
+
async function listSnapshots(params) {
|
|
26728
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
26729
|
+
const qs = new URLSearchParams;
|
|
26730
|
+
const branch = branchName?.trim();
|
|
26731
|
+
if (branch)
|
|
26732
|
+
qs.append("branch", branch);
|
|
26733
|
+
if (dataset)
|
|
26734
|
+
qs.append("dataset", dataset);
|
|
26735
|
+
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
26736
|
+
return callDblabApi({
|
|
26737
|
+
apiKey,
|
|
26738
|
+
apiBaseUrl,
|
|
26739
|
+
instanceId,
|
|
26740
|
+
action,
|
|
26741
|
+
method: "get",
|
|
26742
|
+
operation: "Failed to list snapshots",
|
|
26743
|
+
debug
|
|
26744
|
+
});
|
|
26745
|
+
}
|
|
26746
|
+
async function createSnapshot(params) {
|
|
26747
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
26748
|
+
if (!cloneId)
|
|
26749
|
+
throw new Error("cloneId is required");
|
|
26750
|
+
const data = { cloneID: cloneId };
|
|
26751
|
+
if (message)
|
|
26752
|
+
data.message = message;
|
|
26753
|
+
return callDblabApi({
|
|
26754
|
+
apiKey,
|
|
26755
|
+
apiBaseUrl,
|
|
26756
|
+
instanceId,
|
|
26757
|
+
action: "/branch/snapshot",
|
|
26758
|
+
method: "post",
|
|
26759
|
+
data,
|
|
26760
|
+
operation: "Failed to create snapshot",
|
|
26761
|
+
debug
|
|
26762
|
+
});
|
|
26763
|
+
}
|
|
26764
|
+
async function destroySnapshot(params) {
|
|
26765
|
+
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
26766
|
+
if (!snapshotId)
|
|
26767
|
+
throw new Error("snapshotId is required");
|
|
26768
|
+
if (!/^[a-zA-Z0-9_.@/:-]+$/.test(snapshotId)) {
|
|
26769
|
+
throw new Error("snapshotId contains invalid characters");
|
|
26770
|
+
}
|
|
26771
|
+
if (snapshotId.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
26772
|
+
throw new Error("snapshotId contains an invalid path segment");
|
|
26773
|
+
}
|
|
26774
|
+
const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
|
|
26775
|
+
return callDblabApi({
|
|
26776
|
+
apiKey,
|
|
26777
|
+
apiBaseUrl,
|
|
26778
|
+
instanceId,
|
|
26779
|
+
action,
|
|
26780
|
+
method: "delete",
|
|
26781
|
+
operation: "Failed to destroy snapshot",
|
|
26782
|
+
debug
|
|
26783
|
+
});
|
|
26784
|
+
}
|
|
26785
|
+
|
|
26786
|
+
// bin/postgres-ai.ts
|
|
26787
|
+
init_util();
|
|
26788
|
+
|
|
26789
|
+
// lib/instances.ts
|
|
26790
|
+
import * as fs4 from "fs";
|
|
26791
|
+
|
|
26792
|
+
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
26793
|
+
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
26794
|
+
function isNothing2(subject) {
|
|
26795
|
+
return typeof subject === "undefined" || subject === null;
|
|
26796
|
+
}
|
|
26797
|
+
function isObject3(subject) {
|
|
26798
|
+
return typeof subject === "object" && subject !== null;
|
|
26799
|
+
}
|
|
26800
|
+
function toArray2(sequence) {
|
|
26801
|
+
if (Array.isArray(sequence))
|
|
26802
|
+
return sequence;
|
|
26803
|
+
else if (isNothing2(sequence))
|
|
26804
|
+
return [];
|
|
26805
|
+
return [sequence];
|
|
26806
|
+
}
|
|
26807
|
+
function extend4(target, source) {
|
|
26808
|
+
var index, length, key, sourceKeys;
|
|
26809
|
+
if (source) {
|
|
26810
|
+
sourceKeys = Object.keys(source);
|
|
26811
|
+
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
26812
|
+
key = sourceKeys[index];
|
|
26813
|
+
target[key] = source[key];
|
|
26814
|
+
}
|
|
26815
|
+
}
|
|
26816
|
+
return target;
|
|
26817
|
+
}
|
|
26818
|
+
function repeat2(string4, count) {
|
|
26819
|
+
var result = "", cycle;
|
|
26820
|
+
for (cycle = 0;cycle < count; cycle += 1) {
|
|
26821
|
+
result += string4;
|
|
26822
|
+
}
|
|
26823
|
+
return result;
|
|
26824
|
+
}
|
|
26825
|
+
function isNegativeZero2(number4) {
|
|
26826
|
+
return number4 === 0 && Number.NEGATIVE_INFINITY === 1 / number4;
|
|
26827
|
+
}
|
|
26828
|
+
var isNothing_12 = isNothing2;
|
|
26829
|
+
var isObject_12 = isObject3;
|
|
26830
|
+
var toArray_12 = toArray2;
|
|
26831
|
+
var repeat_12 = repeat2;
|
|
26832
|
+
var isNegativeZero_12 = isNegativeZero2;
|
|
26833
|
+
var extend_12 = extend4;
|
|
26834
|
+
var common2 = {
|
|
26835
|
+
isNothing: isNothing_12,
|
|
26836
|
+
isObject: isObject_12,
|
|
26837
|
+
toArray: toArray_12,
|
|
26838
|
+
repeat: repeat_12,
|
|
26839
|
+
isNegativeZero: isNegativeZero_12,
|
|
26840
|
+
extend: extend_12
|
|
26841
|
+
};
|
|
26842
|
+
function formatError3(exception2, compact) {
|
|
26843
|
+
var where = "", message = exception2.reason || "(unknown reason)";
|
|
26844
|
+
if (!exception2.mark)
|
|
26845
|
+
return message;
|
|
26846
|
+
if (exception2.mark.name) {
|
|
26847
|
+
where += 'in "' + exception2.mark.name + '" ';
|
|
26848
|
+
}
|
|
26849
|
+
where += "(" + (exception2.mark.line + 1) + ":" + (exception2.mark.column + 1) + ")";
|
|
26850
|
+
if (!compact && exception2.mark.snippet) {
|
|
26851
|
+
where += `
|
|
26852
|
+
|
|
26853
|
+
` + exception2.mark.snippet;
|
|
26854
|
+
}
|
|
26855
|
+
return message + " " + where;
|
|
26856
|
+
}
|
|
26857
|
+
function YAMLException$12(reason, mark) {
|
|
26858
|
+
Error.call(this);
|
|
26859
|
+
this.name = "YAMLException";
|
|
26860
|
+
this.reason = reason;
|
|
26861
|
+
this.mark = mark;
|
|
26862
|
+
this.message = formatError3(this, false);
|
|
26863
|
+
if (Error.captureStackTrace) {
|
|
26864
|
+
Error.captureStackTrace(this, this.constructor);
|
|
26865
|
+
} else {
|
|
26866
|
+
this.stack = new Error().stack || "";
|
|
26867
|
+
}
|
|
26868
|
+
}
|
|
26869
|
+
YAMLException$12.prototype = Object.create(Error.prototype);
|
|
26870
|
+
YAMLException$12.prototype.constructor = YAMLException$12;
|
|
26871
|
+
YAMLException$12.prototype.toString = function toString2(compact) {
|
|
26872
|
+
return this.name + ": " + formatError3(this, compact);
|
|
26873
|
+
};
|
|
26874
|
+
var exception2 = YAMLException$12;
|
|
26875
|
+
function getLine2(buffer, lineStart, lineEnd, position, maxLineLength) {
|
|
26876
|
+
var head = "";
|
|
26877
|
+
var tail = "";
|
|
26878
|
+
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
|
|
26879
|
+
if (position - lineStart > maxHalfLength) {
|
|
26880
|
+
head = " ... ";
|
|
26881
|
+
lineStart = position - maxHalfLength + head.length;
|
|
26882
|
+
}
|
|
26883
|
+
if (lineEnd - position > maxHalfLength) {
|
|
26884
|
+
tail = " ...";
|
|
26885
|
+
lineEnd = position + maxHalfLength - tail.length;
|
|
26886
|
+
}
|
|
26887
|
+
return {
|
|
26888
|
+
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
26889
|
+
pos: position - lineStart + head.length
|
|
26890
|
+
};
|
|
26891
|
+
}
|
|
26892
|
+
function padStart2(string4, max) {
|
|
26893
|
+
return common2.repeat(" ", max - string4.length) + string4;
|
|
26894
|
+
}
|
|
26895
|
+
function makeSnippet2(mark, options) {
|
|
26896
|
+
options = Object.create(options || null);
|
|
25915
26897
|
if (!mark.buffer)
|
|
25916
26898
|
return null;
|
|
25917
26899
|
if (!options.maxLength)
|
|
@@ -28595,6 +29577,12 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
28595
29577
|
try {
|
|
28596
29578
|
if (!apiKey || !instanceId)
|
|
28597
29579
|
return { ok: false, reason: "missing api key or instance id" };
|
|
29580
|
+
if (!(opts.vcpus > 0)) {
|
|
29581
|
+
return {
|
|
29582
|
+
ok: false,
|
|
29583
|
+
reason: "vcpus is unknown (0 or omitted); AAS collection stays off until a real value is set — re-run with --vcpus <n>"
|
|
29584
|
+
};
|
|
29585
|
+
}
|
|
28598
29586
|
const labels = resolveAasLabels(opts.instancesPath);
|
|
28599
29587
|
if (!labels)
|
|
28600
29588
|
return { ok: false, reason: "could not determine a single (cluster, node_name) target" };
|
|
@@ -30281,16 +31269,93 @@ function createCallbackServer(port = 0, expectedState = null, timeoutMs = 300000
|
|
|
30281
31269
|
}
|
|
30282
31270
|
resolveReady(actualPort);
|
|
30283
31271
|
});
|
|
30284
|
-
return {
|
|
30285
|
-
server: { stop: stopServer },
|
|
30286
|
-
promise: promise2,
|
|
30287
|
-
ready,
|
|
30288
|
-
getPort: () => actualPort
|
|
30289
|
-
};
|
|
31272
|
+
return {
|
|
31273
|
+
server: { stop: stopServer },
|
|
31274
|
+
promise: promise2,
|
|
31275
|
+
ready,
|
|
31276
|
+
getPort: () => actualPort
|
|
31277
|
+
};
|
|
31278
|
+
}
|
|
31279
|
+
|
|
31280
|
+
// bin/postgres-ai.ts
|
|
31281
|
+
init_util();
|
|
31282
|
+
|
|
31283
|
+
// lib/feedback.ts
|
|
31284
|
+
var FEEDBACK_URL = "https://gitlab.com/postgres-ai/postgresai/-/work_items/300";
|
|
31285
|
+
var FEEDBACK_SUPPRESS_ENV = "PGAI_NO_FEEDBACK_TIP";
|
|
31286
|
+
var FEEDBACK_TIP_MIN_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
31287
|
+
function feedbackTipSuppressed(suppress) {
|
|
31288
|
+
if (suppress === undefined)
|
|
31289
|
+
return false;
|
|
31290
|
+
const value = suppress.trim().toLowerCase();
|
|
31291
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no";
|
|
31292
|
+
}
|
|
31293
|
+
function feedbackTipAllowed(ctx) {
|
|
31294
|
+
if (ctx.json)
|
|
31295
|
+
return false;
|
|
31296
|
+
if (!ctx.isTTY)
|
|
31297
|
+
return false;
|
|
31298
|
+
if (feedbackTipSuppressed(ctx.suppress))
|
|
31299
|
+
return false;
|
|
31300
|
+
return true;
|
|
31301
|
+
}
|
|
31302
|
+
function isFeedbackTipDue(lastShownMs, nowMs) {
|
|
31303
|
+
if (lastShownMs === null || lastShownMs === undefined || !Number.isFinite(lastShownMs)) {
|
|
31304
|
+
return true;
|
|
31305
|
+
}
|
|
31306
|
+
return nowMs - lastShownMs >= FEEDBACK_TIP_MIN_INTERVAL_MS;
|
|
31307
|
+
}
|
|
31308
|
+
function feedbackTipLine() {
|
|
31309
|
+
return `\x1B[2m\uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL} (silence: ${FEEDBACK_SUPPRESS_ENV}=1)\x1B[0m`;
|
|
31310
|
+
}
|
|
31311
|
+
function readLastShown() {
|
|
31312
|
+
const value = readConfig2().feedbackTipLastShownAt;
|
|
31313
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
31314
|
+
}
|
|
31315
|
+
function persistLastShown(nowMs) {
|
|
31316
|
+
writeConfig2({ feedbackTipLastShownAt: nowMs });
|
|
31317
|
+
}
|
|
31318
|
+
function maybeEmitFeedbackTip(ctx, io = {}) {
|
|
31319
|
+
if (!feedbackTipAllowed(ctx))
|
|
31320
|
+
return false;
|
|
31321
|
+
const now = (io.now ?? Date.now)();
|
|
31322
|
+
const read = io.readLastShown ?? readLastShown;
|
|
31323
|
+
const persist = io.persistLastShown ?? persistLastShown;
|
|
31324
|
+
const write = io.write ?? ((line) => {
|
|
31325
|
+
process.stderr.write(line);
|
|
31326
|
+
});
|
|
31327
|
+
let lastShown = null;
|
|
31328
|
+
try {
|
|
31329
|
+
lastShown = read();
|
|
31330
|
+
} catch {
|
|
31331
|
+
lastShown = null;
|
|
31332
|
+
}
|
|
31333
|
+
if (!isFeedbackTipDue(lastShown, now))
|
|
31334
|
+
return false;
|
|
31335
|
+
try {
|
|
31336
|
+
persist(now);
|
|
31337
|
+
} catch {}
|
|
31338
|
+
write(feedbackTipLine() + `
|
|
31339
|
+
`);
|
|
31340
|
+
return true;
|
|
31341
|
+
}
|
|
31342
|
+
function feedbackJson() {
|
|
31343
|
+
return JSON.stringify({ url: FEEDBACK_URL }, null, 2);
|
|
31344
|
+
}
|
|
31345
|
+
function feedbackMessage() {
|
|
31346
|
+
return [
|
|
31347
|
+
"\uD83D\uDCA1 Help shape the PostgresAI CLI",
|
|
31348
|
+
"",
|
|
31349
|
+
"We read every idea and bug report. Share yours here:",
|
|
31350
|
+
"",
|
|
31351
|
+
` ${FEEDBACK_URL}`,
|
|
31352
|
+
"",
|
|
31353
|
+
`Tip: set ${FEEDBACK_SUPPRESS_ENV}=1 to silence the occasional reminder.`
|
|
31354
|
+
].join(`
|
|
31355
|
+
`);
|
|
30290
31356
|
}
|
|
30291
31357
|
|
|
30292
31358
|
// bin/postgres-ai.ts
|
|
30293
|
-
init_util();
|
|
30294
31359
|
import { createInterface } from "readline";
|
|
30295
31360
|
import * as childProcess from "child_process";
|
|
30296
31361
|
|
|
@@ -30426,7 +31491,9 @@ where
|
|
|
30426
31491
|
blk_read_time,
|
|
30427
31492
|
blk_write_time,
|
|
30428
31493
|
extract(epoch from (now() - pg_postmaster_start_time()))::int8 as postmaster_uptime_s,
|
|
30429
|
-
|
|
31494
|
+
case when to_regproc('aurora_version') is null
|
|
31495
|
+
then extract(epoch from (now() - pg_backup_start_time()))::int8
|
|
31496
|
+
end as backup_duration_s,
|
|
30430
31497
|
checksum_failures,
|
|
30431
31498
|
extract(epoch from (now() - checksum_last_failure))::int8 as checksum_last_failure_s,
|
|
30432
31499
|
case when pg_is_in_recovery() then 1 else 0 end as in_recovery_int,
|
|
@@ -30464,7 +31531,9 @@ where
|
|
|
30464
31531
|
blk_read_time,
|
|
30465
31532
|
blk_write_time,
|
|
30466
31533
|
extract(epoch from (now() - pg_postmaster_start_time()))::int8 as postmaster_uptime_s,
|
|
30467
|
-
|
|
31534
|
+
case when to_regproc('aurora_version') is null
|
|
31535
|
+
then extract(epoch from (now() - pg_backup_start_time()))::int8
|
|
31536
|
+
end as backup_duration_s,
|
|
30468
31537
|
checksum_failures,
|
|
30469
31538
|
extract(epoch from (now() - checksum_last_failure))::int8 as checksum_last_failure_s,
|
|
30470
31539
|
case when pg_is_in_recovery() then 1 else 0 end as in_recovery_int,
|
|
@@ -31029,6 +32098,246 @@ where datname = current_database()
|
|
|
31029
32098
|
gauges: ["stats_reset_epoch", "seconds_since_reset"],
|
|
31030
32099
|
statement_timeout_seconds: 15
|
|
31031
32100
|
},
|
|
32101
|
+
pg_database_wraparound: {
|
|
32102
|
+
sqls: {
|
|
32103
|
+
11: `select /* pgwatch_generated */
|
|
32104
|
+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
|
|
32105
|
+
datname as tag_datname,
|
|
32106
|
+
age(d.datfrozenxid) as age_datfrozenxid,
|
|
32107
|
+
mxid_age(d.datminmxid) as age_datminmxid
|
|
32108
|
+
from
|
|
32109
|
+
pg_catalog.pg_database d
|
|
32110
|
+
where
|
|
32111
|
+
d.datallowconn
|
|
32112
|
+
`
|
|
32113
|
+
},
|
|
32114
|
+
gauges: ["age_datfrozenxid", "age_datminmxid"],
|
|
32115
|
+
statement_timeout_seconds: 15
|
|
32116
|
+
},
|
|
32117
|
+
pg_settings_wraparound: {
|
|
32118
|
+
description: "Snapshots wraparound-related pg_settings values so dashboards can plot threshold lines (soft autovacuum_freeze_max_age, failsafe age) that reflect actual cluster configuration instead of compiled defaults. pg_settings is cluster-global, so this metric is is_instance_level — pgwatch scrapes it once per cluster instead of once per database.",
|
|
32119
|
+
sqls: {
|
|
32120
|
+
11: `select /* pgwatch_generated */
|
|
32121
|
+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
|
|
32122
|
+
(select setting::int8 from pg_settings where name = 'autovacuum_freeze_max_age') as autovacuum_freeze_max_age,
|
|
32123
|
+
(select setting::int8 from pg_settings where name = 'vacuum_freeze_min_age') as vacuum_freeze_min_age,
|
|
32124
|
+
(select setting::int8 from pg_settings where name = 'vacuum_freeze_table_age') as vacuum_freeze_table_age,
|
|
32125
|
+
(select setting::int8 from pg_settings where name = 'autovacuum_multixact_freeze_max_age') as autovacuum_multixact_freeze_max_age,
|
|
32126
|
+
(select setting::int8 from pg_settings where name = 'vacuum_multixact_freeze_min_age') as vacuum_multixact_freeze_min_age,
|
|
32127
|
+
(select setting::int8 from pg_settings where name = 'vacuum_multixact_freeze_table_age') as vacuum_multixact_freeze_table_age,
|
|
32128
|
+
coalesce((select setting::int8 from pg_settings where name = 'vacuum_failsafe_age'), 0) as vacuum_failsafe_age,
|
|
32129
|
+
coalesce((select setting::int8 from pg_settings where name = 'vacuum_multixact_failsafe_age'), 0) as vacuum_multixact_failsafe_age`
|
|
32130
|
+
},
|
|
32131
|
+
gauges: ["*"],
|
|
32132
|
+
statement_timeout_seconds: 15
|
|
32133
|
+
},
|
|
32134
|
+
pg_table_wraparound: {
|
|
32135
|
+
description: "Top relations at risk of transaction ID or MultiXact wraparound. Ranking reads pg_class only and is independently bounded to 50 relations for each age. Exact relation sizes are evaluated only after the bounded candidate set has been selected. Per-table freeze age overrides are exposed so F002 can evaluate the effective emergency threshold.",
|
|
32136
|
+
sqls: {
|
|
32137
|
+
11: `with candidates as ( /* pgwatch_generated */
|
|
32138
|
+
select
|
|
32139
|
+
c.oid,
|
|
32140
|
+
n.nspname,
|
|
32141
|
+
c.relname,
|
|
32142
|
+
age(c.relfrozenxid)::int8 as xid_age,
|
|
32143
|
+
mxid_age(c.relminmxid)::int8 as multixact_age,
|
|
32144
|
+
coalesce(
|
|
32145
|
+
-- Follow-up: toast.* overrides live on the parent heap's
|
|
32146
|
+
-- reloptions, so relkind 't' currently uses the global value.
|
|
32147
|
+
(select split_part(option, '=', 2)::int8
|
|
32148
|
+
from unnest(coalesce(c.reloptions, '{}'::text[])) option
|
|
32149
|
+
where split_part(option, '=', 1) = 'autovacuum_freeze_max_age'),
|
|
32150
|
+
current_setting('autovacuum_freeze_max_age')::int8
|
|
32151
|
+
) as effective_freeze_max_age,
|
|
32152
|
+
coalesce(
|
|
32153
|
+
(select split_part(option, '=', 2)::int8
|
|
32154
|
+
from unnest(coalesce(c.reloptions, '{}'::text[])) option
|
|
32155
|
+
where split_part(option, '=', 1) = 'autovacuum_multixact_freeze_max_age'),
|
|
32156
|
+
current_setting('autovacuum_multixact_freeze_max_age')::int8
|
|
32157
|
+
) as effective_multixact_freeze_max_age
|
|
32158
|
+
from pg_catalog.pg_class c
|
|
32159
|
+
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
32160
|
+
where c.relkind in ('r', 'm', 't')
|
|
32161
|
+
and c.relpersistence <> 't'
|
|
32162
|
+
and n.nspname <> 'information_schema'
|
|
32163
|
+
),
|
|
32164
|
+
ranked as (
|
|
32165
|
+
(select *, 'xid'::text as ranked_by
|
|
32166
|
+
from candidates
|
|
32167
|
+
order by xid_age desc, nspname, relname
|
|
32168
|
+
limit 50)
|
|
32169
|
+
union all
|
|
32170
|
+
(select *, 'multixact'::text as ranked_by
|
|
32171
|
+
from candidates
|
|
32172
|
+
order by multixact_age desc, nspname, relname
|
|
32173
|
+
limit 50)
|
|
32174
|
+
),
|
|
32175
|
+
selected as (
|
|
32176
|
+
select
|
|
32177
|
+
oid, nspname, relname, xid_age, multixact_age,
|
|
32178
|
+
effective_freeze_max_age, effective_multixact_freeze_max_age,
|
|
32179
|
+
string_agg(ranked_by, ',' order by ranked_by) as ranked_by
|
|
32180
|
+
from ranked
|
|
32181
|
+
group by oid, nspname, relname, xid_age, multixact_age,
|
|
32182
|
+
effective_freeze_max_age, effective_multixact_freeze_max_age
|
|
32183
|
+
)
|
|
32184
|
+
select
|
|
32185
|
+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
|
|
32186
|
+
current_database() as tag_datname,
|
|
32187
|
+
s.nspname as tag_schema_name,
|
|
32188
|
+
s.relname as tag_table_name,
|
|
32189
|
+
s.ranked_by as tag_ranked_by,
|
|
32190
|
+
s.xid_age,
|
|
32191
|
+
s.multixact_age,
|
|
32192
|
+
s.effective_freeze_max_age,
|
|
32193
|
+
s.effective_multixact_freeze_max_age,
|
|
32194
|
+
pg_catalog.pg_total_relation_size(s.oid)::int8 as table_size_bytes
|
|
32195
|
+
from selected s`
|
|
32196
|
+
},
|
|
32197
|
+
gauges: ["xid_age", "multixact_age", "effective_freeze_max_age", "effective_multixact_freeze_max_age", "table_size_bytes"],
|
|
32198
|
+
statement_timeout_seconds: 15
|
|
32199
|
+
},
|
|
32200
|
+
multixact_size: {
|
|
32201
|
+
sqls: {
|
|
32202
|
+
11: `with env as ( /* pgwatch_generated */
|
|
32203
|
+
select
|
|
32204
|
+
exists (
|
|
32205
|
+
select
|
|
32206
|
+
from pg_proc p
|
|
32207
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
32208
|
+
where p.proname = 'pg_ls_multixactdir' and n.nspname = 'rds_tools'
|
|
32209
|
+
) as has_rds_fn,
|
|
32210
|
+
exists (
|
|
32211
|
+
select
|
|
32212
|
+
from pg_proc p
|
|
32213
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
32214
|
+
where p.proname = 'aurora_stat_file' and n.nspname = 'pg_catalog'
|
|
32215
|
+
) as has_aurora_fn,
|
|
32216
|
+
exists (select from pg_proc where proname = 'pg_ls_dir') as has_pg_ls_dir_func,
|
|
32217
|
+
exists (select from pg_proc where proname = 'pg_stat_file') as has_pg_stat_file_func
|
|
32218
|
+
),
|
|
32219
|
+
can_local as (
|
|
32220
|
+
-- to_regprocedure returns NULL instead of erroring when the signature
|
|
32221
|
+
-- does not exist, so this stays safe even on databases where
|
|
32222
|
+
-- pg_stat_file has been dropped or has a different signature.
|
|
32223
|
+
-- coalesce guards against NULL propagating through the AND chain.
|
|
32224
|
+
-- The execute-privilege check prevents "permission denied" on
|
|
32225
|
+
-- managed databases (Supabase etc.) where pg_stat_file is
|
|
32226
|
+
-- restricted to superusers (#142).
|
|
32227
|
+
select (has_pg_ls_dir_func and has_pg_stat_file_func
|
|
32228
|
+
and coalesce(
|
|
32229
|
+
has_function_privilege(
|
|
32230
|
+
to_regprocedure('pg_stat_file(text,boolean)'), 'execute'),
|
|
32231
|
+
false)
|
|
32232
|
+
) as ok from env
|
|
32233
|
+
),
|
|
32234
|
+
-- Use query_to_xml to safely execute Aurora-specific multixact query.
|
|
32235
|
+
-- Aurora uses aurora_stat_file() function instead of rds_tools.pg_ls_multixactdir().
|
|
32236
|
+
aurora_probe_xml as (
|
|
32237
|
+
select query_to_xml($q$
|
|
32238
|
+
with files as (
|
|
32239
|
+
select filename, allocated_bytes, used_bytes
|
|
32240
|
+
from aurora_stat_file()
|
|
32241
|
+
where filename like 'pg_multixact/%'
|
|
32242
|
+
),
|
|
32243
|
+
members as (
|
|
32244
|
+
select sum(used_bytes)::bigint as sz from files where filename like 'pg_multixact/members%'
|
|
32245
|
+
),
|
|
32246
|
+
offsets as (
|
|
32247
|
+
select sum(used_bytes)::bigint as sz from files where filename like 'pg_multixact/offsets%'
|
|
32248
|
+
),
|
|
32249
|
+
has_rows as (
|
|
32250
|
+
select exists(select 1 from files) as any_rows
|
|
32251
|
+
)
|
|
32252
|
+
select
|
|
32253
|
+
case when (select any_rows from has_rows) then coalesce((select sz from members), 0) end as members_bytes,
|
|
32254
|
+
case when (select any_rows from has_rows) then coalesce((select sz from offsets), 0) end as offsets_bytes,
|
|
32255
|
+
case when (select any_rows from has_rows) then 0 else 1 end as status_code
|
|
32256
|
+
$q$, true, true, '') as x
|
|
32257
|
+
where (select has_aurora_fn from env)
|
|
32258
|
+
),
|
|
32259
|
+
-- Use query_to_xml to safely execute RDS-specific multixact directory listing query.
|
|
32260
|
+
-- The XML wrapper allows the query to fail gracefully if rds_tools.pg_ls_multixactdir()
|
|
32261
|
+
-- is unavailable or returns errors, preventing the entire metric from failing.
|
|
32262
|
+
rds_probe_xml as (
|
|
32263
|
+
select query_to_xml($q$
|
|
32264
|
+
with files as (
|
|
32265
|
+
select name, size
|
|
32266
|
+
from rds_tools.pg_ls_multixactdir()
|
|
32267
|
+
),
|
|
32268
|
+
members as (
|
|
32269
|
+
select sum(size)::bigint as sz from files where name like 'pg_multixact/members%'
|
|
32270
|
+
),
|
|
32271
|
+
offsets as (
|
|
32272
|
+
select sum(size)::bigint as sz from files where name like 'pg_multixact/offsets%'
|
|
32273
|
+
),
|
|
32274
|
+
has_rows as (
|
|
32275
|
+
select exists(select 1 from files where name like 'pg_multixact/%') as any_rows
|
|
32276
|
+
)
|
|
32277
|
+
select
|
|
32278
|
+
case when (select any_rows from has_rows) then coalesce((select sz from members), 0) end as members_bytes,
|
|
32279
|
+
case when (select any_rows from has_rows) then coalesce((select sz from offsets), 0) end as offsets_bytes,
|
|
32280
|
+
case when (select any_rows from has_rows) then 0 else 1 end as status_code
|
|
32281
|
+
$q$, true, true, '') as x
|
|
32282
|
+
where (select has_rds_fn from env) and not (select has_aurora_fn from env)
|
|
32283
|
+
),
|
|
32284
|
+
-- Use query_to_xml to safely execute standard Postgres multixact directory listing query.
|
|
32285
|
+
-- The XML wrapper allows the query to fail gracefully if pg_stat_file() or pg_ls_dir()
|
|
32286
|
+
-- are unavailable or return permission errors, preventing the entire metric from failing.
|
|
32287
|
+
local_probe_xml as (
|
|
32288
|
+
select query_to_xml($q$
|
|
32289
|
+
with dirs as (
|
|
32290
|
+
select
|
|
32291
|
+
(pg_stat_file('pg_multixact/members', true)).isdir as has_members,
|
|
32292
|
+
(pg_stat_file('pg_multixact/offsets', true)).isdir as has_offsets
|
|
32293
|
+
),
|
|
32294
|
+
flags as (
|
|
32295
|
+
select ((select has_members from dirs) or (select has_offsets from dirs)) as has_any
|
|
32296
|
+
),
|
|
32297
|
+
members as (
|
|
32298
|
+
select sum((pg_stat_file(format('pg_multixact/members/%s', d), true)).size)::bigint as sz
|
|
32299
|
+
from pg_ls_dir('pg_multixact/members') as d(d)
|
|
32300
|
+
where (select has_members from dirs)
|
|
32301
|
+
),
|
|
32302
|
+
offsets as (
|
|
32303
|
+
select sum((pg_stat_file(format('pg_multixact/offsets/%s', d), true)).size)::bigint as sz
|
|
32304
|
+
from pg_ls_dir('pg_multixact/offsets') as d(d)
|
|
32305
|
+
where (select has_offsets from dirs)
|
|
32306
|
+
)
|
|
32307
|
+
select
|
|
32308
|
+
case when (select has_any from flags) then coalesce((select sz from members), 0) end as members_bytes,
|
|
32309
|
+
case when (select has_any from flags) then coalesce((select sz from offsets), 0) end as offsets_bytes,
|
|
32310
|
+
case when (select has_any from flags) then 0 else 1 end as status_code
|
|
32311
|
+
$q$, true, true, '') as x
|
|
32312
|
+
where not (select has_rds_fn from env) and not (select has_aurora_fn from env) and (select ok from can_local)
|
|
32313
|
+
),
|
|
32314
|
+
picked as (
|
|
32315
|
+
select * from aurora_probe_xml
|
|
32316
|
+
union all
|
|
32317
|
+
select * from rds_probe_xml
|
|
32318
|
+
union all
|
|
32319
|
+
select * from local_probe_xml
|
|
32320
|
+
limit 1
|
|
32321
|
+
),
|
|
32322
|
+
parsed as (
|
|
32323
|
+
select
|
|
32324
|
+
(xpath('//members_bytes/text()', x))[1]::text::bigint as members_bytes,
|
|
32325
|
+
(xpath('//offsets_bytes/text()', x))[1]::text::bigint as offsets_bytes,
|
|
32326
|
+
(xpath('//status_code/text()', x))[1]::text::int as status_code
|
|
32327
|
+
from picked
|
|
32328
|
+
)
|
|
32329
|
+
select * from parsed
|
|
32330
|
+
union all
|
|
32331
|
+
select
|
|
32332
|
+
null::bigint as members_bytes,
|
|
32333
|
+
null::bigint as offsets_bytes,
|
|
32334
|
+
2::int as status_code
|
|
32335
|
+
where not exists (select 1 from parsed);
|
|
32336
|
+
`
|
|
32337
|
+
},
|
|
32338
|
+
gauges: ["members_bytes", "offsets_bytes", "status_code"],
|
|
32339
|
+
statement_timeout_seconds: 15
|
|
32340
|
+
},
|
|
31032
32341
|
pg_dead_tuples: {
|
|
31033
32342
|
description: "Per-table dead-tuple accumulation and per-table autovacuum overrides for the F003 express check (\"Autovacuum: dead tuples\"). Reads the live counters in pg_stat_user_tables joined to pg_class for reloptions, so dead tuples that have NEVER been vacuumed are visible — unlike the statistical bloat estimators (F004/F005), which completely miss a table where autovacuum is disabled and dead tuples pile up unvacuumed. Returns relations that either carry dead tuples or have autovacuum disabled per-table, bounded to the top 100 by n_dead_tup so pathological schemas can't blow up the report. autovacuum_disabled recognises every boolean spelling Postgres accepts and stores verbatim in reloptions (false/off/no/0 and their unique prefixes; confirmed against PG13 and PG16 that `alter table .. set (autovacuum_enabled = false|off|0)` stores the literal text, so matching only 'autovacuum_enabled=off' misses the common '=false' spelling). Timestamps are exported as epoch seconds (0 = never) because Prometheus gauges must be numeric. The AccessExclusiveLock guard mirrors table_stats: pg_table_size() takes a lock and would block behind a fully locked relation. Compatible with all supported Postgres versions.",
|
|
31034
32343
|
sqls: {
|
|
@@ -31361,6 +32670,200 @@ having count(*) > 0
|
|
|
31361
32670
|
gauges: ["real_size_mib", "table_size_mib", "extra_size", "extra_pct", "fillfactor", "bloat_size", "bloat_pct", "is_na", "reltuples"],
|
|
31362
32671
|
statement_timeout_seconds: 15
|
|
31363
32672
|
},
|
|
32673
|
+
xmin_horizon_snapshot: {
|
|
32674
|
+
description: "Express-checkup single snapshot of xmin horizon ages and the oldest blocker for each holder class. Returns one row with component JSON, privilege/privacy metadata, and a pg_flight_recorder availability hint. Primary-only analysis is enforced in SQL; replicas return an informational skip row.",
|
|
32675
|
+
sqls: {
|
|
32676
|
+
12: `with context as materialized (
|
|
32677
|
+
select
|
|
32678
|
+
pg_is_in_recovery() as is_in_recovery,
|
|
32679
|
+
case when pg_is_in_recovery() then null
|
|
32680
|
+
else txid_snapshot_xmin(txid_current_snapshot())::int8 end as snapshot_xmin,
|
|
32681
|
+
current_setting('autovacuum_freeze_max_age')::int8 as autovacuum_freeze_max_age,
|
|
32682
|
+
coalesce(
|
|
32683
|
+
(select rolsuper from pg_roles where rolname = current_user), false
|
|
32684
|
+
) or coalesce(
|
|
32685
|
+
(select pg_has_role(current_user, oid, 'member') from pg_roles where rolname = 'pg_monitor'), false
|
|
32686
|
+
) or coalesce(
|
|
32687
|
+
(select pg_has_role(current_user, oid, 'member') from pg_roles where rolname = 'pg_read_all_stats'), false
|
|
32688
|
+
)
|
|
32689
|
+
as has_full_visibility,
|
|
32690
|
+
lower(coalesce(current_setting('postgresai.capture_query_preview', true), 'on'))
|
|
32691
|
+
not in ('', 'off', 'false', '0') as query_preview_enabled,
|
|
32692
|
+
to_regnamespace('pgfr_analyze') is not null as pg_flight_recorder_detected
|
|
32693
|
+
),
|
|
32694
|
+
activity_rows as materialized (
|
|
32695
|
+
select
|
|
32696
|
+
greatest(age(a.backend_xmin), age(a.backend_xid))::int8 as age_tx,
|
|
32697
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
32698
|
+
'pid', a.pid,
|
|
32699
|
+
'database', a.datname,
|
|
32700
|
+
'user', a.usename,
|
|
32701
|
+
'application_name', a.application_name,
|
|
32702
|
+
'state', a.state,
|
|
32703
|
+
'backend_type', to_jsonb(a)->>'backend_type',
|
|
32704
|
+
'queryid', to_jsonb(a)->>'query_id',
|
|
32705
|
+
'xact_age_seconds', extract(epoch from now() - a.xact_start)::int8,
|
|
32706
|
+
'state_age_seconds', extract(epoch from now() - a.state_change)::int8,
|
|
32707
|
+
'query_age_seconds', extract(epoch from now() - a.query_start)::int8,
|
|
32708
|
+
'query_preview', case
|
|
32709
|
+
when c.has_full_visibility and c.query_preview_enabled
|
|
32710
|
+
then left(regexp_replace(a.query, E'[\\\\r\\\\n\\\\t]+', ' ', 'g'), 500)
|
|
32711
|
+
else null
|
|
32712
|
+
end
|
|
32713
|
+
)) as detail
|
|
32714
|
+
from pg_stat_activity a
|
|
32715
|
+
cross join context c
|
|
32716
|
+
where not c.is_in_recovery
|
|
32717
|
+
and a.pid <> pg_backend_pid()
|
|
32718
|
+
and (a.backend_xmin is not null or a.backend_xid is not null)
|
|
32719
|
+
and coalesce(to_jsonb(a)->>'leader_pid', '') = ''
|
|
32720
|
+
and coalesce(to_jsonb(a)->>'backend_type', 'client backend')
|
|
32721
|
+
in ('client backend', 'autovacuum worker')
|
|
32722
|
+
),
|
|
32723
|
+
activity as (
|
|
32724
|
+
select
|
|
32725
|
+
coalesce(max(age_tx), 0)::int8 as age_tx,
|
|
32726
|
+
count(*)::int8 as count,
|
|
32727
|
+
(array_agg(detail order by age_tx desc, (detail->>'pid')::int asc))[1] as top_blocker
|
|
32728
|
+
from activity_rows
|
|
32729
|
+
),
|
|
32730
|
+
slot_rows as materialized (
|
|
32731
|
+
select
|
|
32732
|
+
age(s.xmin)::int8 as age_tx,
|
|
32733
|
+
age(s.catalog_xmin)::int8 as catalog_age_tx,
|
|
32734
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
32735
|
+
'slot_name', s.slot_name,
|
|
32736
|
+
'slot_type', s.slot_type,
|
|
32737
|
+
'plugin', s.plugin,
|
|
32738
|
+
'database', s.database,
|
|
32739
|
+
'active', s.active,
|
|
32740
|
+
'active_pid', s.active_pid,
|
|
32741
|
+
'status', case
|
|
32742
|
+
when coalesce(to_jsonb(s)->>'invalidation_reason', '') <> '' then 'invalidated'
|
|
32743
|
+
when coalesce(to_jsonb(s)->>'conflicting', 'false') = 'true' then 'conflicting'
|
|
32744
|
+
when not s.active and coalesce(to_jsonb(s)->>'inactive_since', '') <> '' then 'inactive'
|
|
32745
|
+
when not s.active then 'unused'
|
|
32746
|
+
else 'active'
|
|
32747
|
+
end,
|
|
32748
|
+
'inactive_since', to_jsonb(s)->>'inactive_since',
|
|
32749
|
+
'wal_status', to_jsonb(s)->>'wal_status',
|
|
32750
|
+
'conflicting', (to_jsonb(s)->>'conflicting')::boolean,
|
|
32751
|
+
'invalidation_reason', to_jsonb(s)->>'invalidation_reason'
|
|
32752
|
+
)) as detail
|
|
32753
|
+
from pg_replication_slots s
|
|
32754
|
+
cross join context c
|
|
32755
|
+
where not c.is_in_recovery
|
|
32756
|
+
and (s.xmin is not null or s.catalog_xmin is not null)
|
|
32757
|
+
),
|
|
32758
|
+
slots as (
|
|
32759
|
+
select
|
|
32760
|
+
coalesce(max(age_tx), 0)::int8 as age_tx,
|
|
32761
|
+
count(*) filter (where age_tx is not null)::int8 as count,
|
|
32762
|
+
(array_agg(detail order by age_tx desc nulls last, detail->>'slot_name')
|
|
32763
|
+
filter (where age_tx is not null))[1] as top_blocker
|
|
32764
|
+
from slot_rows
|
|
32765
|
+
),
|
|
32766
|
+
slots_catalog as (
|
|
32767
|
+
select
|
|
32768
|
+
coalesce(max(catalog_age_tx), 0)::int8 as age_tx,
|
|
32769
|
+
count(*) filter (where catalog_age_tx is not null)::int8 as count,
|
|
32770
|
+
(array_agg(detail order by catalog_age_tx desc nulls last, detail->>'slot_name')
|
|
32771
|
+
filter (where catalog_age_tx is not null))[1] as top_blocker
|
|
32772
|
+
from slot_rows
|
|
32773
|
+
),
|
|
32774
|
+
replication_rows as materialized (
|
|
32775
|
+
select
|
|
32776
|
+
age(r.backend_xmin)::int8 as age_tx,
|
|
32777
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
32778
|
+
'pid', r.pid,
|
|
32779
|
+
'user', r.usename,
|
|
32780
|
+
'application_name', r.application_name,
|
|
32781
|
+
'client_addr', r.client_addr,
|
|
32782
|
+
'state', r.state,
|
|
32783
|
+
'sync_state', r.sync_state
|
|
32784
|
+
)) as detail
|
|
32785
|
+
from pg_stat_replication r
|
|
32786
|
+
cross join context c
|
|
32787
|
+
where not c.is_in_recovery and r.backend_xmin is not null
|
|
32788
|
+
),
|
|
32789
|
+
replication as (
|
|
32790
|
+
select
|
|
32791
|
+
coalesce(max(age_tx), 0)::int8 as age_tx,
|
|
32792
|
+
count(*)::int8 as count,
|
|
32793
|
+
(array_agg(detail order by age_tx desc, (detail->>'pid')::int asc))[1] as top_blocker
|
|
32794
|
+
from replication_rows
|
|
32795
|
+
),
|
|
32796
|
+
prepared_rows as materialized (
|
|
32797
|
+
select
|
|
32798
|
+
age(p.transaction)::int8 as age_tx,
|
|
32799
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
32800
|
+
'gid', p.gid,
|
|
32801
|
+
'owner', p.owner,
|
|
32802
|
+
'database', p.database,
|
|
32803
|
+
'prepared_at', p.prepared,
|
|
32804
|
+
'prepared_age_seconds', extract(epoch from now() - p.prepared)::int8
|
|
32805
|
+
)) as detail
|
|
32806
|
+
from pg_prepared_xacts p
|
|
32807
|
+
cross join context c
|
|
32808
|
+
where not c.is_in_recovery
|
|
32809
|
+
),
|
|
32810
|
+
prepared as (
|
|
32811
|
+
select
|
|
32812
|
+
coalesce(max(age_tx), 0)::int8 as age_tx,
|
|
32813
|
+
count(*)::int8 as count,
|
|
32814
|
+
(array_agg(detail order by age_tx desc, detail->>'gid' asc))[1] as top_blocker
|
|
32815
|
+
from prepared_rows
|
|
32816
|
+
)
|
|
32817
|
+
select /* pgwatch_generated xmin_horizon_snapshot */
|
|
32818
|
+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
|
|
32819
|
+
current_database() as tag_datname,
|
|
32820
|
+
c.is_in_recovery,
|
|
32821
|
+
case when c.is_in_recovery then 'replica: xmin horizon analysis is primary-only' end as skip_reason,
|
|
32822
|
+
c.snapshot_xmin,
|
|
32823
|
+
c.autovacuum_freeze_max_age,
|
|
32824
|
+
c.has_full_visibility,
|
|
32825
|
+
c.query_preview_enabled,
|
|
32826
|
+
c.pg_flight_recorder_detected,
|
|
32827
|
+
case when c.is_in_recovery then 0 else greatest(a.age_tx, s.age_tx, r.age_tx, p.age_tx) end
|
|
32828
|
+
as data_horizon_age_tx,
|
|
32829
|
+
case when c.is_in_recovery then 0 else sc.age_tx end as catalog_horizon_age_tx,
|
|
32830
|
+
jsonb_build_object(
|
|
32831
|
+
'pg_stat_activity', jsonb_build_object(
|
|
32832
|
+
'age_tx', case when c.is_in_recovery then 0 else a.age_tx end,
|
|
32833
|
+
'count', case when c.is_in_recovery then 0 else a.count end,
|
|
32834
|
+
'top_blocker', case when c.is_in_recovery then null else a.top_blocker end
|
|
32835
|
+
),
|
|
32836
|
+
'pg_replication_slots', jsonb_build_object(
|
|
32837
|
+
'age_tx', case when c.is_in_recovery then 0 else s.age_tx end,
|
|
32838
|
+
'count', case when c.is_in_recovery then 0 else s.count end,
|
|
32839
|
+
'top_blocker', case when c.is_in_recovery then null else s.top_blocker end
|
|
32840
|
+
),
|
|
32841
|
+
'pg_replication_slots_catalog', jsonb_build_object(
|
|
32842
|
+
'age_tx', case when c.is_in_recovery then 0 else sc.age_tx end,
|
|
32843
|
+
'count', case when c.is_in_recovery then 0 else sc.count end,
|
|
32844
|
+
'top_blocker', case when c.is_in_recovery then null else sc.top_blocker end
|
|
32845
|
+
),
|
|
32846
|
+
'pg_stat_replication', jsonb_build_object(
|
|
32847
|
+
'age_tx', case when c.is_in_recovery then 0 else r.age_tx end,
|
|
32848
|
+
'count', case when c.is_in_recovery then 0 else r.count end,
|
|
32849
|
+
'top_blocker', case when c.is_in_recovery then null else r.top_blocker end
|
|
32850
|
+
),
|
|
32851
|
+
'pg_prepared_xacts', jsonb_build_object(
|
|
32852
|
+
'age_tx', case when c.is_in_recovery then 0 else p.age_tx end,
|
|
32853
|
+
'count', case when c.is_in_recovery then 0 else p.count end,
|
|
32854
|
+
'top_blocker', case when c.is_in_recovery then null else p.top_blocker end
|
|
32855
|
+
)
|
|
32856
|
+
) as components
|
|
32857
|
+
from context c
|
|
32858
|
+
cross join activity a
|
|
32859
|
+
cross join slots s
|
|
32860
|
+
cross join slots_catalog sc
|
|
32861
|
+
cross join replication r
|
|
32862
|
+
cross join prepared p`
|
|
32863
|
+
},
|
|
32864
|
+
gauges: ["data_horizon_age_tx", "catalog_horizon_age_tx"],
|
|
32865
|
+
statement_timeout_seconds: 15
|
|
32866
|
+
},
|
|
31364
32867
|
pg_stat_io: {
|
|
31365
32868
|
description: "Collects I/O statistics from the PostgreSQL `pg_stat_io` view (available in PostgreSQL 16+). Provides insights into read and write operations by backend type, including the number of operations, MiB transferred (divided by 1048576), and time spent on I/O. This metric is essential for monitoring I/O performance and identifying potential bottlenecks in disk operations. The data is aggregated by backend type with a total row included via ROLLUP.",
|
|
31366
32869
|
sqls: {
|
|
@@ -31439,9 +32942,14 @@ var METRIC_NAMES = {
|
|
|
31439
32942
|
H001: "pg_invalid_indexes",
|
|
31440
32943
|
H002: "unused_indexes",
|
|
31441
32944
|
H004: "redundant_indexes",
|
|
32945
|
+
F002Database: "pg_database_wraparound",
|
|
32946
|
+
F002Settings: "pg_settings_wraparound",
|
|
32947
|
+
F002Tables: "pg_table_wraparound",
|
|
32948
|
+
F002MultixactSize: "multixact_size",
|
|
31442
32949
|
F003: "pg_dead_tuples",
|
|
31443
32950
|
F004: "pg_table_bloat",
|
|
31444
32951
|
F005: "pg_btree_bloat",
|
|
32952
|
+
F009: "xmin_horizon_snapshot",
|
|
31445
32953
|
settings: "settings",
|
|
31446
32954
|
dbStats: "db_stats",
|
|
31447
32955
|
dbSize: "db_size",
|
|
@@ -32014,26 +33522,377 @@ var CHECKUP_DICTIONARY_DATA = [
|
|
|
32014
33522
|
sort_order: null,
|
|
32015
33523
|
is_system_report: false
|
|
32016
33524
|
}
|
|
32017
|
-
];
|
|
32018
|
-
|
|
32019
|
-
// lib/checkup-dictionary.ts
|
|
32020
|
-
var dictionaryByCode = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
|
|
32021
|
-
function buildCheckInfoMap() {
|
|
32022
|
-
const result = {};
|
|
32023
|
-
for (const entry of CHECKUP_DICTIONARY_DATA) {
|
|
32024
|
-
result[entry.code] = entry.title;
|
|
33525
|
+
];
|
|
33526
|
+
|
|
33527
|
+
// lib/checkup-dictionary.ts
|
|
33528
|
+
var dictionaryByCode = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
|
|
33529
|
+
function buildCheckInfoMap() {
|
|
33530
|
+
const result = {};
|
|
33531
|
+
for (const entry of CHECKUP_DICTIONARY_DATA) {
|
|
33532
|
+
result[entry.code] = entry.title;
|
|
33533
|
+
}
|
|
33534
|
+
return result;
|
|
33535
|
+
}
|
|
33536
|
+
|
|
33537
|
+
// lib/checkup-summary.ts
|
|
33538
|
+
function generateCheckSummary(checkId, report) {
|
|
33539
|
+
const nodeIds = Object.keys(report.results || {});
|
|
33540
|
+
if (nodeIds.length === 0) {
|
|
33541
|
+
return { status: "info", message: "No data" };
|
|
33542
|
+
}
|
|
33543
|
+
const nodeData = report.results[nodeIds[0]];
|
|
33544
|
+
switch (checkId) {
|
|
33545
|
+
case "H001":
|
|
33546
|
+
return summarizeH001(nodeData);
|
|
33547
|
+
case "H002":
|
|
33548
|
+
return summarizeH002(nodeData);
|
|
33549
|
+
case "H004":
|
|
33550
|
+
return summarizeH004(nodeData);
|
|
33551
|
+
case "A002":
|
|
33552
|
+
return summarizeA002(nodeData);
|
|
33553
|
+
case "A013":
|
|
33554
|
+
return summarizeA013(nodeData);
|
|
33555
|
+
case "A003":
|
|
33556
|
+
return summarizeA003(nodeData);
|
|
33557
|
+
case "A004":
|
|
33558
|
+
return summarizeA004(nodeData);
|
|
33559
|
+
case "A007":
|
|
33560
|
+
return summarizeA007(nodeData);
|
|
33561
|
+
case "D001":
|
|
33562
|
+
return summarizeD001(nodeData);
|
|
33563
|
+
case "D004":
|
|
33564
|
+
return summarizeD004(nodeData);
|
|
33565
|
+
case "F001":
|
|
33566
|
+
return summarizeF001(nodeData);
|
|
33567
|
+
case "F002":
|
|
33568
|
+
return summarizeF002(nodeData);
|
|
33569
|
+
case "F003":
|
|
33570
|
+
return summarizeF003(nodeData);
|
|
33571
|
+
case "F004":
|
|
33572
|
+
return summarizeBloat(nodeData, "table");
|
|
33573
|
+
case "F005":
|
|
33574
|
+
return summarizeBloat(nodeData, "index");
|
|
33575
|
+
case "F009":
|
|
33576
|
+
return summarizeF009(nodeData);
|
|
33577
|
+
case "G001":
|
|
33578
|
+
return summarizeG001(nodeData);
|
|
33579
|
+
case "G003":
|
|
33580
|
+
return summarizeG003(nodeData);
|
|
33581
|
+
default:
|
|
33582
|
+
return { status: "info", message: "Check completed" };
|
|
33583
|
+
}
|
|
33584
|
+
}
|
|
33585
|
+
function summarizeA003(nodeData) {
|
|
33586
|
+
const data = nodeData?.data || {};
|
|
33587
|
+
const settingsCount = Object.keys(data).length;
|
|
33588
|
+
if (settingsCount === 0) {
|
|
33589
|
+
return { status: "info", message: "No settings found" };
|
|
33590
|
+
}
|
|
33591
|
+
return {
|
|
33592
|
+
status: "info",
|
|
33593
|
+
message: `${settingsCount} setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33594
|
+
};
|
|
33595
|
+
}
|
|
33596
|
+
function summarizeA004(nodeData) {
|
|
33597
|
+
const data = nodeData?.data;
|
|
33598
|
+
if (!data) {
|
|
33599
|
+
return { status: "info", message: "Cluster information collected" };
|
|
33600
|
+
}
|
|
33601
|
+
const dbCount = Object.keys(data.database_sizes || {}).length;
|
|
33602
|
+
if (dbCount > 0) {
|
|
33603
|
+
return { status: "info", message: `${dbCount} database${dbCount > 1 ? "s" : ""} analyzed` };
|
|
33604
|
+
}
|
|
33605
|
+
return { status: "info", message: "Cluster information collected" };
|
|
33606
|
+
}
|
|
33607
|
+
function summarizeA007(nodeData) {
|
|
33608
|
+
const data = nodeData?.data || {};
|
|
33609
|
+
const alteredCount = Object.keys(data).length;
|
|
33610
|
+
if (alteredCount === 0) {
|
|
33611
|
+
return { status: "ok", message: "No altered settings" };
|
|
33612
|
+
}
|
|
33613
|
+
return {
|
|
33614
|
+
status: "info",
|
|
33615
|
+
message: `${alteredCount} setting${alteredCount > 1 ? "s" : ""} altered from defaults`
|
|
33616
|
+
};
|
|
33617
|
+
}
|
|
33618
|
+
function formatBytes(bytes) {
|
|
33619
|
+
if (bytes === 0)
|
|
33620
|
+
return "0 B";
|
|
33621
|
+
if (bytes < 1024)
|
|
33622
|
+
return `${bytes} B`;
|
|
33623
|
+
if (bytes < 1024 * 1024)
|
|
33624
|
+
return `${(bytes / 1024).toFixed(0)} KiB`;
|
|
33625
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
33626
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
|
|
33627
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
33628
|
+
}
|
|
33629
|
+
function summarizeH001(nodeData) {
|
|
33630
|
+
const data = nodeData?.data || {};
|
|
33631
|
+
let totalCount = 0;
|
|
33632
|
+
let totalSize = 0;
|
|
33633
|
+
for (const dbData of Object.values(data)) {
|
|
33634
|
+
const dbEntry = dbData;
|
|
33635
|
+
totalCount += dbEntry.total_count || 0;
|
|
33636
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
33637
|
+
}
|
|
33638
|
+
if (totalCount === 0) {
|
|
33639
|
+
return { status: "ok", message: "No invalid indexes" };
|
|
33640
|
+
}
|
|
33641
|
+
return {
|
|
33642
|
+
status: "warning",
|
|
33643
|
+
message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
|
|
33644
|
+
};
|
|
33645
|
+
}
|
|
33646
|
+
function summarizeH002(nodeData) {
|
|
33647
|
+
const data = nodeData?.data || {};
|
|
33648
|
+
let totalCount = 0;
|
|
33649
|
+
let totalSize = 0;
|
|
33650
|
+
for (const dbData of Object.values(data)) {
|
|
33651
|
+
const dbEntry = dbData;
|
|
33652
|
+
totalCount += dbEntry.total_count || 0;
|
|
33653
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
33654
|
+
}
|
|
33655
|
+
if (totalCount === 0) {
|
|
33656
|
+
return { status: "ok", message: "All indexes utilized" };
|
|
33657
|
+
}
|
|
33658
|
+
return {
|
|
33659
|
+
status: "warning",
|
|
33660
|
+
message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
|
|
33661
|
+
};
|
|
33662
|
+
}
|
|
33663
|
+
function summarizeH004(nodeData) {
|
|
33664
|
+
const data = nodeData?.data || {};
|
|
33665
|
+
let totalCount = 0;
|
|
33666
|
+
let totalSize = 0;
|
|
33667
|
+
for (const dbData of Object.values(data)) {
|
|
33668
|
+
const dbEntry = dbData;
|
|
33669
|
+
totalCount += dbEntry.total_count || 0;
|
|
33670
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
33671
|
+
}
|
|
33672
|
+
if (totalCount === 0) {
|
|
33673
|
+
return { status: "ok", message: "No redundant indexes" };
|
|
33674
|
+
}
|
|
33675
|
+
return {
|
|
33676
|
+
status: "warning",
|
|
33677
|
+
message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
|
|
33678
|
+
};
|
|
33679
|
+
}
|
|
33680
|
+
function summarizeA002(nodeData) {
|
|
33681
|
+
const ver = nodeData?.data?.version;
|
|
33682
|
+
if (!ver) {
|
|
33683
|
+
return { status: "info", message: "Version checked" };
|
|
32025
33684
|
}
|
|
32026
|
-
|
|
33685
|
+
const major = parseInt(ver.server_major_ver, 10);
|
|
33686
|
+
if (major >= 17) {
|
|
33687
|
+
return { status: "ok", message: `PostgreSQL ${major}` };
|
|
33688
|
+
}
|
|
33689
|
+
if (major >= 15) {
|
|
33690
|
+
return { status: "info", message: `PostgreSQL ${major}` };
|
|
33691
|
+
}
|
|
33692
|
+
return {
|
|
33693
|
+
status: "warning",
|
|
33694
|
+
message: `PostgreSQL ${major} (consider upgrading)`
|
|
33695
|
+
};
|
|
33696
|
+
}
|
|
33697
|
+
function summarizeA013(nodeData) {
|
|
33698
|
+
const ver = nodeData?.data?.version;
|
|
33699
|
+
if (!ver) {
|
|
33700
|
+
return { status: "info", message: "Minor version checked" };
|
|
33701
|
+
}
|
|
33702
|
+
const current = ver.version || "";
|
|
33703
|
+
return {
|
|
33704
|
+
status: "info",
|
|
33705
|
+
message: `Version ${current}`
|
|
33706
|
+
};
|
|
33707
|
+
}
|
|
33708
|
+
function summarizeD001(nodeData) {
|
|
33709
|
+
const data = nodeData?.data || {};
|
|
33710
|
+
const settingsCount = Object.keys(data).length;
|
|
33711
|
+
if (settingsCount === 0) {
|
|
33712
|
+
return { status: "info", message: "No logging settings found" };
|
|
33713
|
+
}
|
|
33714
|
+
return {
|
|
33715
|
+
status: "info",
|
|
33716
|
+
message: `${settingsCount} logging setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33717
|
+
};
|
|
33718
|
+
}
|
|
33719
|
+
function summarizeD004(nodeData) {
|
|
33720
|
+
const data = nodeData?.data || {};
|
|
33721
|
+
const settingsCount = Object.keys(data).length;
|
|
33722
|
+
if (settingsCount === 0) {
|
|
33723
|
+
return { status: "info", message: "No pg_stat_statements settings found" };
|
|
33724
|
+
}
|
|
33725
|
+
return {
|
|
33726
|
+
status: "info",
|
|
33727
|
+
message: `${settingsCount} pg_stat_statements setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33728
|
+
};
|
|
33729
|
+
}
|
|
33730
|
+
function summarizeF001(nodeData) {
|
|
33731
|
+
const data = nodeData?.data || {};
|
|
33732
|
+
const settingsCount = Object.keys(data).length;
|
|
33733
|
+
if (settingsCount === 0) {
|
|
33734
|
+
return { status: "info", message: "No autovacuum settings found" };
|
|
33735
|
+
}
|
|
33736
|
+
return {
|
|
33737
|
+
status: "info",
|
|
33738
|
+
message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33739
|
+
};
|
|
33740
|
+
}
|
|
33741
|
+
function summarizeF002(nodeData) {
|
|
33742
|
+
if (nodeData?.data?.settings_available === false) {
|
|
33743
|
+
return { status: "info", message: "Wraparound settings unavailable" };
|
|
33744
|
+
}
|
|
33745
|
+
const severity = nodeData?.data?.severity;
|
|
33746
|
+
const databases = nodeData?.data?.databases?.length || 0;
|
|
33747
|
+
const tables = nodeData?.data?.tables || [];
|
|
33748
|
+
const offenders = tables.filter((table) => table?.xid?.severity !== "info" || table?.multixact?.severity !== "info").length;
|
|
33749
|
+
if (!severity)
|
|
33750
|
+
return { status: "info", message: "No wraparound data" };
|
|
33751
|
+
if (severity === "info") {
|
|
33752
|
+
return { status: "ok", message: `${databases} database${databases === 1 ? "" : "s"} below wraparound thresholds` };
|
|
33753
|
+
}
|
|
33754
|
+
return {
|
|
33755
|
+
status: "warning",
|
|
33756
|
+
message: `${severity.toUpperCase()} wraparound risk${offenders ? `; ${offenders} table${offenders === 1 ? "" : "s"} affected` : ""}`
|
|
33757
|
+
};
|
|
33758
|
+
}
|
|
33759
|
+
function summarizeF003(nodeData) {
|
|
33760
|
+
const data = nodeData?.data || {};
|
|
33761
|
+
let flaggedCount = 0;
|
|
33762
|
+
let disabledCount = 0;
|
|
33763
|
+
for (const dbData of Object.values(data)) {
|
|
33764
|
+
const dbEntry = dbData;
|
|
33765
|
+
flaggedCount += dbEntry.flagged_count || 0;
|
|
33766
|
+
disabledCount += dbEntry.autovacuum_disabled_flagged_count || 0;
|
|
33767
|
+
}
|
|
33768
|
+
if (flaggedCount === 0 && disabledCount === 0) {
|
|
33769
|
+
return { status: "ok", message: "No significant dead tuple accumulation" };
|
|
33770
|
+
}
|
|
33771
|
+
const parts = [];
|
|
33772
|
+
if (flaggedCount > 0) {
|
|
33773
|
+
parts.push(`${flaggedCount} table${flaggedCount > 1 ? "s" : ""} with excessive dead tuples`);
|
|
33774
|
+
}
|
|
33775
|
+
if (disabledCount > 0) {
|
|
33776
|
+
parts.push(`${disabledCount} table${disabledCount > 1 ? "s" : ""} with autovacuum disabled`);
|
|
33777
|
+
}
|
|
33778
|
+
return { status: "warning", message: parts.join(", ") };
|
|
33779
|
+
}
|
|
33780
|
+
function summarizeF009(nodeData) {
|
|
33781
|
+
const data = nodeData?.data || {};
|
|
33782
|
+
if (data.skipped) {
|
|
33783
|
+
return { status: "info", message: data.skip_reason || "Primary-only check skipped on replica" };
|
|
33784
|
+
}
|
|
33785
|
+
const severity = String(data.severity || "OK");
|
|
33786
|
+
const age = Math.max(Number(data.data_horizon_age_tx || 0), Number(data.catalog_horizon_age_tx || 0));
|
|
33787
|
+
if (severity === "OK") {
|
|
33788
|
+
return { status: "ok", message: `Xmin horizons healthy (oldest holder: ${age} transactions)` };
|
|
33789
|
+
}
|
|
33790
|
+
const holder = data.dominant_holder?.source ? `; dominant holder: ${data.dominant_holder.source}` : "";
|
|
33791
|
+
const activityAgeSeconds = Number(data.components?.pg_stat_activity?.top_blocker?.xact_age_seconds || 0);
|
|
33792
|
+
if (severity === "NOTICE" && data.data_horizon_severity === "OK" && data.catalog_horizon_severity === "OK" && activityAgeSeconds > 0) {
|
|
33793
|
+
return {
|
|
33794
|
+
status: "info",
|
|
33795
|
+
message: `NOTICE: transaction open for ${activityAgeSeconds} seconds${holder}`
|
|
33796
|
+
};
|
|
33797
|
+
}
|
|
33798
|
+
return {
|
|
33799
|
+
status: severity === "NOTICE" ? "info" : "warning",
|
|
33800
|
+
message: `${severity}: xmin horizon age ${age} transactions${holder}`
|
|
33801
|
+
};
|
|
33802
|
+
}
|
|
33803
|
+
function summarizeBloat(nodeData, kind) {
|
|
33804
|
+
const data = nodeData?.data || {};
|
|
33805
|
+
let totalCount = 0;
|
|
33806
|
+
for (const dbData of Object.values(data)) {
|
|
33807
|
+
const dbEntry = dbData;
|
|
33808
|
+
if (dbEntry?.status?.ok === false) {
|
|
33809
|
+
const reason = String(dbEntry.status.reason || "query_error").replaceAll("_", " ");
|
|
33810
|
+
return { status: "warning", message: `Bloat estimate degraded: ${reason}` };
|
|
33811
|
+
}
|
|
33812
|
+
totalCount += dbEntry?.total_count || 0;
|
|
33813
|
+
}
|
|
33814
|
+
if (totalCount === 0) {
|
|
33815
|
+
return { status: "ok", message: `No bloated ${kind}${kind === "index" ? "es" : "s"} found` };
|
|
33816
|
+
}
|
|
33817
|
+
return {
|
|
33818
|
+
status: "warning",
|
|
33819
|
+
message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
|
|
33820
|
+
};
|
|
33821
|
+
}
|
|
33822
|
+
function summarizeG001(nodeData) {
|
|
33823
|
+
const data = nodeData?.data || {};
|
|
33824
|
+
const settingsCount = Object.keys(data).length;
|
|
33825
|
+
if (settingsCount === 0) {
|
|
33826
|
+
return { status: "info", message: "No memory settings found" };
|
|
33827
|
+
}
|
|
33828
|
+
return {
|
|
33829
|
+
status: "info",
|
|
33830
|
+
message: `${settingsCount} memory setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33831
|
+
};
|
|
33832
|
+
}
|
|
33833
|
+
function summarizeG003(nodeData) {
|
|
33834
|
+
const data = nodeData?.data || {};
|
|
33835
|
+
const settings = data.settings || {};
|
|
33836
|
+
const deadlockStats = data.deadlock_stats;
|
|
33837
|
+
const settingsCount = Object.keys(settings).length;
|
|
33838
|
+
if (deadlockStats && typeof deadlockStats.deadlocks === "number" && deadlockStats.deadlocks > 0) {
|
|
33839
|
+
return {
|
|
33840
|
+
status: "warning",
|
|
33841
|
+
message: `${deadlockStats.deadlocks} deadlock${deadlockStats.deadlocks > 1 ? "s" : ""} detected`
|
|
33842
|
+
};
|
|
33843
|
+
}
|
|
33844
|
+
if (settingsCount === 0) {
|
|
33845
|
+
return { status: "info", message: "No timeout/lock settings found" };
|
|
33846
|
+
}
|
|
33847
|
+
return {
|
|
33848
|
+
status: "info",
|
|
33849
|
+
message: `${settingsCount} timeout/lock setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33850
|
+
};
|
|
32027
33851
|
}
|
|
32028
33852
|
|
|
32029
33853
|
// lib/checkup.ts
|
|
32030
33854
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
33855
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
32031
33856
|
var SECONDS_PER_DAY = 86400;
|
|
32032
33857
|
var SECONDS_PER_HOUR = 3600;
|
|
32033
33858
|
var SECONDS_PER_MINUTE = 60;
|
|
32034
33859
|
function toBool(val) {
|
|
32035
33860
|
return val === true || val === 1 || val === "t" || val === "true";
|
|
32036
33861
|
}
|
|
33862
|
+
var F002_WRAPAROUND_LIMIT = 2147483648;
|
|
33863
|
+
var F002_CRITICAL_AGE = 1e9;
|
|
33864
|
+
var F002_FAILSAFE_HIGH_PCT = 80;
|
|
33865
|
+
var severityRank = {
|
|
33866
|
+
info: 0,
|
|
33867
|
+
warning: 1,
|
|
33868
|
+
high: 2,
|
|
33869
|
+
critical: 3
|
|
33870
|
+
};
|
|
33871
|
+
function evaluateWraparoundRisk(age, emergencyAge, failsafeAge) {
|
|
33872
|
+
let severity = "info";
|
|
33873
|
+
if (emergencyAge <= 0) {
|
|
33874
|
+
severity = "info";
|
|
33875
|
+
} else if (age >= F002_CRITICAL_AGE) {
|
|
33876
|
+
severity = "critical";
|
|
33877
|
+
} else if (age >= 2 * emergencyAge || failsafeAge !== null && age >= failsafeAge * (F002_FAILSAFE_HIGH_PCT / 100)) {
|
|
33878
|
+
severity = "high";
|
|
33879
|
+
} else if (age >= emergencyAge) {
|
|
33880
|
+
severity = "warning";
|
|
33881
|
+
}
|
|
33882
|
+
const pct = (value, limit) => limit > 0 ? Math.round(value / limit * 1e4) / 100 : 0;
|
|
33883
|
+
return {
|
|
33884
|
+
age,
|
|
33885
|
+
emergency_age: emergencyAge,
|
|
33886
|
+
failsafe_age: failsafeAge,
|
|
33887
|
+
pct_towards_wraparound: pct(age, F002_WRAPAROUND_LIMIT),
|
|
33888
|
+
pct_towards_emergency: pct(age, emergencyAge),
|
|
33889
|
+
pct_towards_failsafe: failsafeAge === null ? null : pct(age, failsafeAge),
|
|
33890
|
+
severity
|
|
33891
|
+
};
|
|
33892
|
+
}
|
|
33893
|
+
function maxSeverity(...severities) {
|
|
33894
|
+
return severities.reduce((max, value) => severityRank[value] > severityRank[max] ? value : max, "info");
|
|
33895
|
+
}
|
|
32037
33896
|
var F003_DEAD_TUPLES_MIN = 1e5;
|
|
32038
33897
|
var F003_DEAD_PCT_MIN = 20;
|
|
32039
33898
|
var F003_AUTOVACUUM_DISABLED_MIN_ROWS = 1e4;
|
|
@@ -32053,11 +33912,11 @@ function parseVersionNum(versionNum) {
|
|
|
32053
33912
|
return { major: "", minor: "" };
|
|
32054
33913
|
}
|
|
32055
33914
|
}
|
|
32056
|
-
function
|
|
33915
|
+
function formatBytes2(bytes) {
|
|
32057
33916
|
if (bytes === 0)
|
|
32058
33917
|
return "0 B";
|
|
32059
33918
|
if (bytes < 0)
|
|
32060
|
-
return `-${
|
|
33919
|
+
return `-${formatBytes2(-bytes)}`;
|
|
32061
33920
|
if (!Number.isFinite(bytes))
|
|
32062
33921
|
return `${bytes} B`;
|
|
32063
33922
|
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
|
@@ -32069,7 +33928,7 @@ function formatSettingPrettyValue(settingNormalized, unitNormalized, rawValue) {
|
|
|
32069
33928
|
return rawValue;
|
|
32070
33929
|
}
|
|
32071
33930
|
if (unitNormalized === "bytes") {
|
|
32072
|
-
return
|
|
33931
|
+
return formatBytes2(settingNormalized);
|
|
32073
33932
|
}
|
|
32074
33933
|
if (unitNormalized === "seconds") {
|
|
32075
33934
|
const MS_PER_SECOND = 1000;
|
|
@@ -32243,7 +34102,7 @@ async function getClusterInfo(client, pgMajorVersion = 16) {
|
|
|
32243
34102
|
};
|
|
32244
34103
|
const tempBytes = parseInt(stats.temp_bytes || "0", 10);
|
|
32245
34104
|
info.temp_bytes_written = {
|
|
32246
|
-
value:
|
|
34105
|
+
value: formatBytes2(tempBytes),
|
|
32247
34106
|
unit: "bytes",
|
|
32248
34107
|
description: "Total temporary file bytes written"
|
|
32249
34108
|
};
|
|
@@ -32309,7 +34168,7 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
|
|
|
32309
34168
|
index_name: String(transformed.index_name || ""),
|
|
32310
34169
|
relation_name: String(transformed.relation_name || ""),
|
|
32311
34170
|
index_size_bytes: indexSizeBytes,
|
|
32312
|
-
index_size_pretty:
|
|
34171
|
+
index_size_pretty: formatBytes2(indexSizeBytes),
|
|
32313
34172
|
index_definition: String(transformed.index_definition || ""),
|
|
32314
34173
|
supports_fk: toBool(transformed.supports_fk),
|
|
32315
34174
|
is_pk: toBool(transformed.is_pk),
|
|
@@ -32338,7 +34197,7 @@ async function getUnusedIndexes(client, pgMajorVersion = 16) {
|
|
|
32338
34197
|
index_size_bytes: indexSizeBytes,
|
|
32339
34198
|
idx_is_btree: toBool(transformed.idx_is_btree),
|
|
32340
34199
|
supports_fk: toBool(transformed.supports_fk),
|
|
32341
|
-
index_size_pretty:
|
|
34200
|
+
index_size_pretty: formatBytes2(indexSizeBytes)
|
|
32342
34201
|
};
|
|
32343
34202
|
});
|
|
32344
34203
|
}
|
|
@@ -32411,7 +34270,7 @@ async function getRedundantIndexes(client, pgMajorVersion = 16) {
|
|
|
32411
34270
|
index_name: String(item.index_name ?? ""),
|
|
32412
34271
|
index_definition: String(item.index_definition ?? ""),
|
|
32413
34272
|
index_size_bytes: sizeBytes,
|
|
32414
|
-
index_size_pretty:
|
|
34273
|
+
index_size_pretty: formatBytes2(sizeBytes)
|
|
32415
34274
|
};
|
|
32416
34275
|
});
|
|
32417
34276
|
}
|
|
@@ -32433,8 +34292,8 @@ async function getRedundantIndexes(client, pgMajorVersion = 16) {
|
|
|
32433
34292
|
index_usage: parseInt(String(transformed.index_usage || 0), 10),
|
|
32434
34293
|
supports_fk: toBool(transformed.supports_fk),
|
|
32435
34294
|
index_definition: String(transformed.index_definition || ""),
|
|
32436
|
-
index_size_pretty:
|
|
32437
|
-
table_size_pretty:
|
|
34295
|
+
index_size_pretty: formatBytes2(indexSizeBytes),
|
|
34296
|
+
table_size_pretty: formatBytes2(tableSizeBytes),
|
|
32438
34297
|
redundant_to: redundantTo
|
|
32439
34298
|
};
|
|
32440
34299
|
if (parseError) {
|
|
@@ -32469,12 +34328,103 @@ async function getDeadTuples(client, pgMajorVersion = 16) {
|
|
|
32469
34328
|
vacuum_count: parseInt(String(t.vacuum_count || 0), 10),
|
|
32470
34329
|
autovacuum_disabled: autovacuumDisabled,
|
|
32471
34330
|
table_size_bytes: tableSizeBytes,
|
|
32472
|
-
table_size_pretty:
|
|
34331
|
+
table_size_pretty: formatBytes2(tableSizeBytes),
|
|
32473
34332
|
exceeds_dead_tuple_thresholds: nDead >= F003_DEAD_TUPLES_MIN && deadPct >= F003_DEAD_PCT_MIN,
|
|
32474
34333
|
autovacuum_disabled_flagged: autovacuumDisabled && nLive + nDead >= F003_AUTOVACUUM_DISABLED_MIN_ROWS
|
|
32475
34334
|
};
|
|
32476
34335
|
});
|
|
32477
34336
|
}
|
|
34337
|
+
async function getWraparoundData(client, pgMajorVersion = 16) {
|
|
34338
|
+
const [settingsResult, databaseResult, tableResult, multixactSizeResult] = await Promise.all([
|
|
34339
|
+
client.query(getMetricSql(METRIC_NAMES.F002Settings, pgMajorVersion)),
|
|
34340
|
+
client.query(getMetricSql(METRIC_NAMES.F002Database, pgMajorVersion)),
|
|
34341
|
+
client.query(getMetricSql(METRIC_NAMES.F002Tables, pgMajorVersion)),
|
|
34342
|
+
client.query(getMetricSql(METRIC_NAMES.F002MultixactSize, pgMajorVersion))
|
|
34343
|
+
]);
|
|
34344
|
+
const numberValue = (value) => parseInt(String(value ?? 0), 10) || 0;
|
|
34345
|
+
const rawSettings = settingsResult.rows[0] || {};
|
|
34346
|
+
const failsafe3 = pgMajorVersion >= 14 ? numberValue(rawSettings.vacuum_failsafe_age) : 0;
|
|
34347
|
+
const multixactFailsafe = pgMajorVersion >= 14 ? numberValue(rawSettings.vacuum_multixact_failsafe_age) : 0;
|
|
34348
|
+
const settings = {
|
|
34349
|
+
autovacuum_freeze_max_age: numberValue(rawSettings.autovacuum_freeze_max_age),
|
|
34350
|
+
vacuum_freeze_min_age: numberValue(rawSettings.vacuum_freeze_min_age),
|
|
34351
|
+
vacuum_freeze_table_age: numberValue(rawSettings.vacuum_freeze_table_age),
|
|
34352
|
+
autovacuum_multixact_freeze_max_age: numberValue(rawSettings.autovacuum_multixact_freeze_max_age),
|
|
34353
|
+
vacuum_multixact_freeze_min_age: numberValue(rawSettings.vacuum_multixact_freeze_min_age),
|
|
34354
|
+
vacuum_multixact_freeze_table_age: numberValue(rawSettings.vacuum_multixact_freeze_table_age),
|
|
34355
|
+
vacuum_failsafe_age: failsafe3 > 0 ? failsafe3 : null,
|
|
34356
|
+
vacuum_multixact_failsafe_age: multixactFailsafe > 0 ? multixactFailsafe : null
|
|
34357
|
+
};
|
|
34358
|
+
const settings_available = settings.autovacuum_freeze_max_age > 0 && settings.autovacuum_multixact_freeze_max_age > 0;
|
|
34359
|
+
const databases = databaseResult.rows.map((row) => {
|
|
34360
|
+
const xidAge = numberValue(row.age_datfrozenxid);
|
|
34361
|
+
const multixactAge = numberValue(row.age_datminmxid);
|
|
34362
|
+
return {
|
|
34363
|
+
database_name: String(row.tag_datname || ""),
|
|
34364
|
+
xid: evaluateWraparoundRisk(xidAge, settings.autovacuum_freeze_max_age, settings.vacuum_failsafe_age),
|
|
34365
|
+
multixact: evaluateWraparoundRisk(multixactAge, settings.autovacuum_multixact_freeze_max_age, settings.vacuum_multixact_failsafe_age)
|
|
34366
|
+
};
|
|
34367
|
+
}).sort((a, b) => Math.max(b.xid.age, b.multixact.age) - Math.max(a.xid.age, a.multixact.age));
|
|
34368
|
+
const tables = tableResult.rows.map((row) => {
|
|
34369
|
+
const xidAge = numberValue(row.xid_age);
|
|
34370
|
+
const multixactAge = numberValue(row.multixact_age);
|
|
34371
|
+
const tableSizeBytes = numberValue(row.table_size_bytes);
|
|
34372
|
+
return {
|
|
34373
|
+
database_name: String(row.tag_datname || ""),
|
|
34374
|
+
schema_name: String(row.tag_schema_name || ""),
|
|
34375
|
+
table_name: String(row.tag_table_name || ""),
|
|
34376
|
+
ranked_by: String(row.tag_ranked_by || "").split(",").filter(Boolean),
|
|
34377
|
+
table_size_bytes: tableSizeBytes,
|
|
34378
|
+
table_size_pretty: formatBytes2(tableSizeBytes),
|
|
34379
|
+
xid: evaluateWraparoundRisk(xidAge, numberValue(row.effective_freeze_max_age) || settings.autovacuum_freeze_max_age, settings.vacuum_failsafe_age),
|
|
34380
|
+
multixact: evaluateWraparoundRisk(multixactAge, numberValue(row.effective_multixact_freeze_max_age) || settings.autovacuum_multixact_freeze_max_age, settings.vacuum_multixact_failsafe_age)
|
|
34381
|
+
};
|
|
34382
|
+
}).sort((a, b) => Math.max(b.xid.age, b.multixact.age) - Math.max(a.xid.age, a.multixact.age) || a.database_name.localeCompare(b.database_name) || a.schema_name.localeCompare(b.schema_name) || a.table_name.localeCompare(b.table_name));
|
|
34383
|
+
const rawMultixactSize = multixactSizeResult.rows[0] || {};
|
|
34384
|
+
const multixactBytes = rawMultixactSize.members_bytes === null || rawMultixactSize.members_bytes === undefined || rawMultixactSize.offsets_bytes === null || rawMultixactSize.offsets_bytes === undefined ? null : numberValue(rawMultixactSize.members_bytes) + numberValue(rawMultixactSize.offsets_bytes);
|
|
34385
|
+
const multixact_size = {
|
|
34386
|
+
bytes: multixactBytes,
|
|
34387
|
+
size_pretty: multixactBytes === null ? null : formatBytes2(multixactBytes),
|
|
34388
|
+
status_code: numberValue(rawMultixactSize.status_code)
|
|
34389
|
+
};
|
|
34390
|
+
return { settings, databases, tables, multixact_size, settings_available };
|
|
34391
|
+
}
|
|
34392
|
+
function buildWraparoundConclusions(databases, tables, settingsAvailable = true) {
|
|
34393
|
+
if (!settingsAvailable) {
|
|
34394
|
+
return {
|
|
34395
|
+
severity: "info",
|
|
34396
|
+
conclusions: ["Wraparound settings are unavailable; severity could not be evaluated."],
|
|
34397
|
+
recommendations: ["Verify pg_settings_wraparound collection and rerun F002 before assessing wraparound risk."]
|
|
34398
|
+
};
|
|
34399
|
+
}
|
|
34400
|
+
const offenders = [
|
|
34401
|
+
...databases.flatMap((db) => [
|
|
34402
|
+
{ name: `database "${db.database_name}"`, kind: "transaction ID", risk: db.xid },
|
|
34403
|
+
{ name: `database "${db.database_name}"`, kind: "MultiXact", risk: db.multixact }
|
|
34404
|
+
]),
|
|
34405
|
+
...tables.flatMap((table) => [
|
|
34406
|
+
{ name: `table "${table.database_name}"."${table.schema_name}"."${table.table_name}"`, kind: "transaction ID", risk: table.xid },
|
|
34407
|
+
{ name: `table "${table.database_name}"."${table.schema_name}"."${table.table_name}"`, kind: "MultiXact", risk: table.multixact }
|
|
34408
|
+
])
|
|
34409
|
+
].filter((item) => item.risk.severity !== "info").sort((a, b) => severityRank[b.risk.severity] - severityRank[a.risk.severity] || b.risk.age - a.risk.age);
|
|
34410
|
+
const severity = maxSeverity(...offenders.map((item) => item.risk.severity));
|
|
34411
|
+
if (offenders.length === 0) {
|
|
34412
|
+
return {
|
|
34413
|
+
severity,
|
|
34414
|
+
conclusions: ["Transaction ID and MultiXact ages are below their emergency vacuum thresholds."],
|
|
34415
|
+
recommendations: []
|
|
34416
|
+
};
|
|
34417
|
+
}
|
|
34418
|
+
const conclusions = offenders.slice(0, 10).map((item) => `${item.name} has ${item.kind} age ${item.risk.age.toLocaleString("en-US")} ` + `(${item.risk.pct_towards_emergency}% of its emergency vacuum threshold; ${item.risk.severity}).`);
|
|
34419
|
+
const recommendations = [
|
|
34420
|
+
"Check pg_stat_activity for 'autovacuum: %to prevent wraparound%' workers and inspect pg_stat_progress_vacuum for progress.",
|
|
34421
|
+
"Check xmin-horizon holders (long transactions, stale replication slots, and prepared transactions) and the F003 autovacuum queue analysis; high age is usually a symptom of vacuum starvation or a blocked horizon."
|
|
34422
|
+
];
|
|
34423
|
+
if (severity === "high" || severity === "critical") {
|
|
34424
|
+
recommendations.push("Run VACUUM (FREEZE, VERBOSE) on the highest-age offender tables after confirming operational impact. Do not raise freeze_max_age merely to silence the check; that reduces the remaining safety margin.");
|
|
34425
|
+
}
|
|
34426
|
+
return { severity, conclusions, recommendations };
|
|
34427
|
+
}
|
|
32478
34428
|
function buildDeadTuplesConclusions(tables) {
|
|
32479
34429
|
const conclusions = [];
|
|
32480
34430
|
const recommendations = [];
|
|
@@ -32498,6 +34448,7 @@ function buildDeadTuplesConclusions(tables) {
|
|
|
32498
34448
|
function createBaseReport(checkId, checkTitle, nodeName) {
|
|
32499
34449
|
const buildTs = resolveBuildTs();
|
|
32500
34450
|
return {
|
|
34451
|
+
contract_version: CONTRACT_VERSION,
|
|
32501
34452
|
version: version || null,
|
|
32502
34453
|
build_ts: buildTs,
|
|
32503
34454
|
generation_mode: "express",
|
|
@@ -32572,9 +34523,9 @@ async function generateIndexReport(client, nodeName, checkId, checkTitle, indexF
|
|
|
32572
34523
|
[indexFieldName]: indexes,
|
|
32573
34524
|
total_count: totalCount,
|
|
32574
34525
|
total_size_bytes: totalSizeBytes,
|
|
32575
|
-
total_size_pretty:
|
|
34526
|
+
total_size_pretty: formatBytes2(totalSizeBytes),
|
|
32576
34527
|
database_size_bytes: dbSizeBytes,
|
|
32577
|
-
database_size_pretty:
|
|
34528
|
+
database_size_pretty: formatBytes2(dbSizeBytes)
|
|
32578
34529
|
};
|
|
32579
34530
|
if (extraFields) {
|
|
32580
34531
|
Object.assign(dbEntry, await extraFields(client, pgMajorVersion));
|
|
@@ -32758,6 +34709,33 @@ async function generateF001(client, nodeName) {
|
|
|
32758
34709
|
};
|
|
32759
34710
|
return report;
|
|
32760
34711
|
}
|
|
34712
|
+
async function generateF002(client, nodeName) {
|
|
34713
|
+
const report = createBaseReport("F002", "Autovacuum: transaction ID and MultiXact wraparound", nodeName);
|
|
34714
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
34715
|
+
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
|
|
34716
|
+
const { settings, databases, tables, multixact_size, settings_available } = await getWraparoundData(client, pgMajorVersion);
|
|
34717
|
+
const { severity, conclusions, recommendations } = buildWraparoundConclusions(databases, tables, settings_available);
|
|
34718
|
+
report.results[nodeName] = {
|
|
34719
|
+
data: {
|
|
34720
|
+
settings,
|
|
34721
|
+
databases,
|
|
34722
|
+
tables,
|
|
34723
|
+
multixact_size,
|
|
34724
|
+
settings_available,
|
|
34725
|
+
severity,
|
|
34726
|
+
thresholds: {
|
|
34727
|
+
wraparound_limit: F002_WRAPAROUND_LIMIT,
|
|
34728
|
+
critical_age: F002_CRITICAL_AGE,
|
|
34729
|
+
failsafe_high_pct: F002_FAILSAFE_HIGH_PCT,
|
|
34730
|
+
table_limit_per_age: 50
|
|
34731
|
+
},
|
|
34732
|
+
conclusions,
|
|
34733
|
+
recommendations
|
|
34734
|
+
},
|
|
34735
|
+
postgres_version: postgresVersion
|
|
34736
|
+
};
|
|
34737
|
+
return report;
|
|
34738
|
+
}
|
|
32761
34739
|
async function generateF003(client, nodeName) {
|
|
32762
34740
|
const report = createBaseReport("F003", "Autovacuum: dead tuples", nodeName);
|
|
32763
34741
|
const postgresVersion = await getPostgresVersion(client);
|
|
@@ -32784,7 +34762,7 @@ async function generateF003(client, nodeName) {
|
|
|
32784
34762
|
conclusions,
|
|
32785
34763
|
recommendations,
|
|
32786
34764
|
database_size_bytes: dbSizeBytes,
|
|
32787
|
-
database_size_pretty:
|
|
34765
|
+
database_size_pretty: formatBytes2(dbSizeBytes)
|
|
32788
34766
|
};
|
|
32789
34767
|
report.results[nodeName] = {
|
|
32790
34768
|
data: { [dbName]: dbEntry },
|
|
@@ -32792,6 +34770,185 @@ async function generateF003(client, nodeName) {
|
|
|
32792
34770
|
};
|
|
32793
34771
|
return report;
|
|
32794
34772
|
}
|
|
34773
|
+
var F009_NOTICE_FRACTION = 0.1;
|
|
34774
|
+
var F009_WARNING_FRACTION = 0.5;
|
|
34775
|
+
var F009_CRITICAL_FRACTION = 0.8;
|
|
34776
|
+
var F009_ACTIVITY_NOTICE_SECONDS = SECONDS_PER_HOUR;
|
|
34777
|
+
var F009_SEVERITY_RANK = {
|
|
34778
|
+
OK: 0,
|
|
34779
|
+
NOTICE: 1,
|
|
34780
|
+
WARNING: 2,
|
|
34781
|
+
CRITICAL: 3
|
|
34782
|
+
};
|
|
34783
|
+
function numberValue(value) {
|
|
34784
|
+
const parsed = Number(value ?? 0);
|
|
34785
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
34786
|
+
}
|
|
34787
|
+
function parseJsonObject(value) {
|
|
34788
|
+
if (value && typeof value === "object")
|
|
34789
|
+
return value;
|
|
34790
|
+
if (typeof value === "string") {
|
|
34791
|
+
try {
|
|
34792
|
+
const parsed = JSON.parse(value);
|
|
34793
|
+
if (parsed && typeof parsed === "object")
|
|
34794
|
+
return parsed;
|
|
34795
|
+
} catch {}
|
|
34796
|
+
}
|
|
34797
|
+
return {};
|
|
34798
|
+
}
|
|
34799
|
+
function normalizeF009Components(value) {
|
|
34800
|
+
const raw = parseJsonObject(value);
|
|
34801
|
+
const component = (name) => {
|
|
34802
|
+
const item = parseJsonObject(raw[name]);
|
|
34803
|
+
const detail = item.top_blocker;
|
|
34804
|
+
return {
|
|
34805
|
+
age_tx: numberValue(item.age_tx),
|
|
34806
|
+
count: numberValue(item.count),
|
|
34807
|
+
top_blocker: detail && typeof detail === "object" ? detail : null
|
|
34808
|
+
};
|
|
34809
|
+
};
|
|
34810
|
+
return {
|
|
34811
|
+
pg_stat_activity: component("pg_stat_activity"),
|
|
34812
|
+
pg_replication_slots: component("pg_replication_slots"),
|
|
34813
|
+
pg_replication_slots_catalog: component("pg_replication_slots_catalog"),
|
|
34814
|
+
pg_stat_replication: component("pg_stat_replication"),
|
|
34815
|
+
pg_prepared_xacts: component("pg_prepared_xacts")
|
|
34816
|
+
};
|
|
34817
|
+
}
|
|
34818
|
+
function getF009Severity(ageTx, freezeMaxAge) {
|
|
34819
|
+
if (freezeMaxAge <= 0)
|
|
34820
|
+
return "OK";
|
|
34821
|
+
const fraction = ageTx / freezeMaxAge;
|
|
34822
|
+
if (fraction >= F009_CRITICAL_FRACTION)
|
|
34823
|
+
return "CRITICAL";
|
|
34824
|
+
if (fraction >= F009_WARNING_FRACTION)
|
|
34825
|
+
return "WARNING";
|
|
34826
|
+
if (fraction >= F009_NOTICE_FRACTION)
|
|
34827
|
+
return "NOTICE";
|
|
34828
|
+
return "OK";
|
|
34829
|
+
}
|
|
34830
|
+
function selectF009DominantHolder(components) {
|
|
34831
|
+
const candidates = [];
|
|
34832
|
+
const add = (source, component, horizonType, priority) => {
|
|
34833
|
+
const item = components[component];
|
|
34834
|
+
if (item.count > 0 && item.top_blocker) {
|
|
34835
|
+
candidates.push({ source, component, horizon_type: horizonType, age_tx: item.age_tx, detail: item.top_blocker, priority });
|
|
34836
|
+
}
|
|
34837
|
+
};
|
|
34838
|
+
add("slot", "pg_replication_slots", "data", 4);
|
|
34839
|
+
add("slot", "pg_replication_slots_catalog", "catalog", 4);
|
|
34840
|
+
add("prepared", "pg_prepared_xacts", "data", 3);
|
|
34841
|
+
add("activity", "pg_stat_activity", "data", 2);
|
|
34842
|
+
add("replication", "pg_stat_replication", "data", 1);
|
|
34843
|
+
candidates.sort((a, b) => b.age_tx - a.age_tx || b.priority - a.priority || a.component.localeCompare(b.component));
|
|
34844
|
+
if (candidates.length === 0)
|
|
34845
|
+
return null;
|
|
34846
|
+
const { priority: _priority, ...dominant } = candidates[0];
|
|
34847
|
+
return dominant;
|
|
34848
|
+
}
|
|
34849
|
+
function sqlLiteral(value) {
|
|
34850
|
+
return String(value ?? "").replace(/'/g, "''");
|
|
34851
|
+
}
|
|
34852
|
+
function buildF009Analysis(components, dominantHolder, dataSeverity, catalogSeverity, options) {
|
|
34853
|
+
const conclusions = [];
|
|
34854
|
+
const recommendations = [];
|
|
34855
|
+
if (options.skipped) {
|
|
34856
|
+
return { conclusions: ["INFO: xmin horizon analysis is primary-only; this replica was skipped."], recommendations };
|
|
34857
|
+
}
|
|
34858
|
+
if (options.limitedVisibility) {
|
|
34859
|
+
conclusions.push("NOTICE: the check ran with limited visibility; sessions owned by other users may hide query text or xmin details. Grant pg_monitor or pg_read_all_stats for complete RCA context.");
|
|
34860
|
+
}
|
|
34861
|
+
if (options.historyDetected) {
|
|
34862
|
+
conclusions.push("INFO: pg_flight_recorder detected — historical xmin timeline is available via pgfr_analyze.xmin_horizon_history().");
|
|
34863
|
+
}
|
|
34864
|
+
const dataAge = Math.max(components.pg_stat_activity.age_tx, components.pg_replication_slots.age_tx, components.pg_stat_replication.age_tx, components.pg_prepared_xacts.age_tx);
|
|
34865
|
+
const catalogAge = components.pg_replication_slots_catalog.age_tx;
|
|
34866
|
+
if (dataSeverity !== "OK")
|
|
34867
|
+
conclusions.push(`${dataSeverity}: data xmin horizon age is ${dataAge} transactions.`);
|
|
34868
|
+
if (catalogSeverity !== "OK")
|
|
34869
|
+
conclusions.push(`${catalogSeverity}: catalog xmin horizon age is ${catalogAge} transactions.`);
|
|
34870
|
+
const activity = components.pg_stat_activity.top_blocker;
|
|
34871
|
+
const xactAgeSeconds = numberValue(activity?.xact_age_seconds);
|
|
34872
|
+
if (activity && xactAgeSeconds >= F009_ACTIVITY_NOTICE_SECONDS) {
|
|
34873
|
+
conclusions.push(`NOTICE: activity PID ${activity.pid} has held a transaction open for ${xactAgeSeconds} seconds.`);
|
|
34874
|
+
}
|
|
34875
|
+
if (!dominantHolder) {
|
|
34876
|
+
if (conclusions.length === 0)
|
|
34877
|
+
conclusions.push("OK: no xmin horizon blocker is currently visible.");
|
|
34878
|
+
return { conclusions, recommendations };
|
|
34879
|
+
}
|
|
34880
|
+
const d = dominantHolder.detail;
|
|
34881
|
+
conclusions.unshift(`${dataSeverity === "OK" && catalogSeverity === "OK" ? "INFO" : "CAUSE"}: dominant xmin holder is ${dominantHolder.source} (${dominantHolder.age_tx} transactions, ${dominantHolder.horizon_type} horizon).`);
|
|
34882
|
+
if (dominantHolder.source === "activity") {
|
|
34883
|
+
const pid = numberValue(d.pid);
|
|
34884
|
+
const backendType = String(d.backend_type || "").toLowerCase();
|
|
34885
|
+
const state = String(d.state || "").toLowerCase();
|
|
34886
|
+
if (backendType.includes("autovacuum")) {
|
|
34887
|
+
recommendations.push(`Inspect pg_stat_progress_vacuum for autovacuum worker PID ${pid}; do not kill it until its progress and table size are understood.`);
|
|
34888
|
+
} else if (state.includes("idle in transaction")) {
|
|
34889
|
+
recommendations.push(`Terminate the idle-in-transaction holder with SELECT pg_terminate_backend(${pid}); pg_cancel_backend() is a no-op while it is idle. Prevent recurrence with idle_in_transaction_session_timeout.`);
|
|
34890
|
+
} else {
|
|
34891
|
+
recommendations.push(`Cancel the active statement first with SELECT pg_cancel_backend(${pid}); if backend_xmin reappears, the query is inside an explicit BEGIN and SELECT pg_terminate_backend(${pid}) may be required. Prevent recurrence with statement_timeout and, on PostgreSQL 17+, transaction_timeout.`);
|
|
34892
|
+
}
|
|
34893
|
+
} else if (dominantHolder.source === "slot") {
|
|
34894
|
+
const slotName = sqlLiteral(d.slot_name);
|
|
34895
|
+
recommendations.push(`Confirm subscriber state for replication slot '${slotName}' before intervening. Then advance it with pg_replication_slot_advance() or, only if obsolete, SELECT pg_drop_replication_slot('${slotName}'). Slot status=${d.status || "unknown"}, wal_status=${d.wal_status || "unknown"}.`);
|
|
34896
|
+
} else if (dominantHolder.source === "replication") {
|
|
34897
|
+
recommendations.push(`On standby '${d.application_name || "unknown"}', find and cancel the long query responsible for hot_standby_feedback. Do not disable hot_standby_feedback as a first response; that trades xmin holdback for recovery-conflict cancellations.`);
|
|
34898
|
+
} else {
|
|
34899
|
+
const gid = sqlLiteral(d.gid);
|
|
34900
|
+
recommendations.push(`After confirming the transaction outcome, resolve the prepared transaction with ROLLBACK PREPARED '${gid}'. Owner=${d.owner || "unknown"}, database=${d.database || "unknown"}, prepared_at=${d.prepared_at || "unknown"}.`);
|
|
34901
|
+
}
|
|
34902
|
+
return { conclusions, recommendations };
|
|
34903
|
+
}
|
|
34904
|
+
async function generateF009(client, nodeName) {
|
|
34905
|
+
const report = createBaseReport("F009", "Xmin horizon and blockers", nodeName);
|
|
34906
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
34907
|
+
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
|
|
34908
|
+
const result = await client.query(getMetricSql(METRIC_NAMES.F009, pgMajorVersion));
|
|
34909
|
+
const row = result.rows[0] || {};
|
|
34910
|
+
const components = normalizeF009Components(row.components);
|
|
34911
|
+
const skipped = toBool(row.is_in_recovery);
|
|
34912
|
+
const freezeMaxAge = numberValue(row.autovacuum_freeze_max_age);
|
|
34913
|
+
const dataAge = numberValue(row.data_horizon_age_tx);
|
|
34914
|
+
const catalogAge = numberValue(row.catalog_horizon_age_tx);
|
|
34915
|
+
const dataSeverity = skipped ? "OK" : getF009Severity(dataAge, freezeMaxAge);
|
|
34916
|
+
const catalogSeverity = skipped ? "OK" : getF009Severity(catalogAge, freezeMaxAge);
|
|
34917
|
+
const activityAgeSeconds = numberValue(components.pg_stat_activity.top_blocker?.xact_age_seconds);
|
|
34918
|
+
let severity = F009_SEVERITY_RANK[dataSeverity] >= F009_SEVERITY_RANK[catalogSeverity] ? dataSeverity : catalogSeverity;
|
|
34919
|
+
if (severity === "OK" && activityAgeSeconds >= F009_ACTIVITY_NOTICE_SECONDS)
|
|
34920
|
+
severity = "NOTICE";
|
|
34921
|
+
const dominantHolder = skipped ? null : selectF009DominantHolder(components);
|
|
34922
|
+
const limitedVisibility = !toBool(row.has_full_visibility);
|
|
34923
|
+
const historyDetected = toBool(row.pg_flight_recorder_detected);
|
|
34924
|
+
const { conclusions, recommendations } = buildF009Analysis(components, dominantHolder, dataSeverity, catalogSeverity, { limitedVisibility, historyDetected, skipped });
|
|
34925
|
+
const data = {
|
|
34926
|
+
skipped,
|
|
34927
|
+
skip_reason: row.skip_reason || null,
|
|
34928
|
+
snapshot_xmin: row.snapshot_xmin === null || row.snapshot_xmin === undefined ? null : numberValue(row.snapshot_xmin),
|
|
34929
|
+
data_horizon_age_tx: dataAge,
|
|
34930
|
+
catalog_horizon_age_tx: catalogAge,
|
|
34931
|
+
autovacuum_freeze_max_age: freezeMaxAge,
|
|
34932
|
+
severity,
|
|
34933
|
+
data_horizon_severity: dataSeverity,
|
|
34934
|
+
catalog_horizon_severity: catalogSeverity,
|
|
34935
|
+
visibility: limitedVisibility ? "limited" : "full",
|
|
34936
|
+
query_preview_captured: toBool(row.query_preview_enabled) && !limitedVisibility,
|
|
34937
|
+
components,
|
|
34938
|
+
dominant_holder: dominantHolder,
|
|
34939
|
+
conclusions,
|
|
34940
|
+
recommendations
|
|
34941
|
+
};
|
|
34942
|
+
if (historyDetected) {
|
|
34943
|
+
data.history = {
|
|
34944
|
+
available: true,
|
|
34945
|
+
source: "pg_flight_recorder",
|
|
34946
|
+
message: "Historical xmin timeline is available via pgfr_analyze.xmin_horizon_history()."
|
|
34947
|
+
};
|
|
34948
|
+
}
|
|
34949
|
+
report.results[nodeName] = { data, postgres_version: postgresVersion };
|
|
34950
|
+
return report;
|
|
34951
|
+
}
|
|
32795
34952
|
function bloatErrorStatus(err) {
|
|
32796
34953
|
const error2 = err instanceof Error ? err.message : String(err);
|
|
32797
34954
|
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code || "") : "";
|
|
@@ -32893,9 +35050,9 @@ async function generateF004(client, nodeName) {
|
|
|
32893
35050
|
fillfactor: parseInt(String(t.fillfactor || 100), 10),
|
|
32894
35051
|
last_vacuum: vacuumInfo.last_vacuum,
|
|
32895
35052
|
last_vacuum_epoch: vacuumInfo.last_vacuum_epoch,
|
|
32896
|
-
real_size_pretty:
|
|
32897
|
-
extra_size_pretty:
|
|
32898
|
-
bloat_size_pretty:
|
|
35053
|
+
real_size_pretty: formatBytes2(realSizeBytes),
|
|
35054
|
+
extra_size_pretty: formatBytes2(extraSize),
|
|
35055
|
+
bloat_size_pretty: formatBytes2(bloatSize)
|
|
32899
35056
|
};
|
|
32900
35057
|
});
|
|
32901
35058
|
} catch (err) {
|
|
@@ -32914,9 +35071,9 @@ async function generateF004(client, nodeName) {
|
|
|
32914
35071
|
bloated_tables: bloatedTables,
|
|
32915
35072
|
total_count: totalCount,
|
|
32916
35073
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
32917
|
-
total_bloat_size_pretty:
|
|
35074
|
+
total_bloat_size_pretty: formatBytes2(totalBloatSizeBytes),
|
|
32918
35075
|
database_size_bytes: dbSizeBytes,
|
|
32919
|
-
database_size_pretty:
|
|
35076
|
+
database_size_pretty: formatBytes2(dbSizeBytes)
|
|
32920
35077
|
};
|
|
32921
35078
|
report.results[nodeName] = {
|
|
32922
35079
|
data: { [dbName]: dbEntry },
|
|
@@ -32976,10 +35133,10 @@ async function generateF005(client, nodeName) {
|
|
|
32976
35133
|
fillfactor: parseInt(String(t.fillfactor || 90), 10),
|
|
32977
35134
|
last_vacuum: vacuumInfo.last_vacuum,
|
|
32978
35135
|
last_vacuum_epoch: vacuumInfo.last_vacuum_epoch,
|
|
32979
|
-
real_size_pretty:
|
|
32980
|
-
table_size_pretty:
|
|
32981
|
-
extra_size_pretty:
|
|
32982
|
-
bloat_size_pretty:
|
|
35136
|
+
real_size_pretty: formatBytes2(realSizeBytes),
|
|
35137
|
+
table_size_pretty: formatBytes2(tableSizeBytes),
|
|
35138
|
+
extra_size_pretty: formatBytes2(extraSize),
|
|
35139
|
+
bloat_size_pretty: formatBytes2(bloatSize)
|
|
32983
35140
|
};
|
|
32984
35141
|
});
|
|
32985
35142
|
} catch (err) {
|
|
@@ -32998,9 +35155,9 @@ async function generateF005(client, nodeName) {
|
|
|
32998
35155
|
bloated_indexes: bloatedIndexes,
|
|
32999
35156
|
total_count: totalCount,
|
|
33000
35157
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
33001
|
-
total_bloat_size_pretty:
|
|
35158
|
+
total_bloat_size_pretty: formatBytes2(totalBloatSizeBytes),
|
|
33002
35159
|
database_size_bytes: dbSizeBytes,
|
|
33003
|
-
database_size_pretty:
|
|
35160
|
+
database_size_pretty: formatBytes2(dbSizeBytes)
|
|
33004
35161
|
};
|
|
33005
35162
|
report.results[nodeName] = {
|
|
33006
35163
|
data: { [dbName]: dbEntry },
|
|
@@ -33059,19 +35216,19 @@ async function generateG001(client, nodeName) {
|
|
|
33059
35216
|
const maxWorkMemUsage = workMemBytes * maxConnections;
|
|
33060
35217
|
memoryUsage = {
|
|
33061
35218
|
shared_buffers_bytes: sharedBuffersBytes,
|
|
33062
|
-
shared_buffers_pretty:
|
|
35219
|
+
shared_buffers_pretty: formatBytes2(sharedBuffersBytes),
|
|
33063
35220
|
wal_buffers_bytes: walBuffersBytes,
|
|
33064
|
-
wal_buffers_pretty:
|
|
35221
|
+
wal_buffers_pretty: formatBytes2(walBuffersBytes),
|
|
33065
35222
|
shared_memory_total_bytes: sharedMemoryTotal,
|
|
33066
|
-
shared_memory_total_pretty:
|
|
35223
|
+
shared_memory_total_pretty: formatBytes2(sharedMemoryTotal),
|
|
33067
35224
|
work_mem_per_connection_bytes: workMemBytes,
|
|
33068
|
-
work_mem_per_connection_pretty:
|
|
35225
|
+
work_mem_per_connection_pretty: formatBytes2(workMemBytes),
|
|
33069
35226
|
max_work_mem_usage_bytes: maxWorkMemUsage,
|
|
33070
|
-
max_work_mem_usage_pretty:
|
|
35227
|
+
max_work_mem_usage_pretty: formatBytes2(maxWorkMemUsage),
|
|
33071
35228
|
maintenance_work_mem_bytes: maintenanceWorkMemBytes,
|
|
33072
|
-
maintenance_work_mem_pretty:
|
|
35229
|
+
maintenance_work_mem_pretty: formatBytes2(maintenanceWorkMemBytes),
|
|
33073
35230
|
effective_cache_size_bytes: effectiveCacheSizeBytes,
|
|
33074
|
-
effective_cache_size_pretty:
|
|
35231
|
+
effective_cache_size_pretty: formatBytes2(effectiveCacheSizeBytes)
|
|
33075
35232
|
};
|
|
33076
35233
|
}
|
|
33077
35234
|
} catch (err) {
|
|
@@ -33292,9 +35449,11 @@ var REPORT_GENERATORS = {
|
|
|
33292
35449
|
D001: generateD001,
|
|
33293
35450
|
D004: generateD004,
|
|
33294
35451
|
F001: generateF001,
|
|
35452
|
+
F002: generateF002,
|
|
33295
35453
|
F003: generateF003,
|
|
33296
35454
|
F004: generateF004,
|
|
33297
35455
|
F005: generateF005,
|
|
35456
|
+
F009: generateF009,
|
|
33298
35457
|
G001: generateG001,
|
|
33299
35458
|
G003: generateG003,
|
|
33300
35459
|
H001: generateH001,
|
|
@@ -33311,23 +35470,43 @@ var CHECK_INFO = (() => {
|
|
|
33311
35470
|
}
|
|
33312
35471
|
return filtered;
|
|
33313
35472
|
})();
|
|
33314
|
-
async function generateAllReports(client, nodeName = "node-01", onProgress) {
|
|
35473
|
+
async function generateAllReports(client, nodeName = "node-01", onProgress, onCheckError) {
|
|
33315
35474
|
const reports = {};
|
|
35475
|
+
const failures = [];
|
|
33316
35476
|
const entries = Object.entries(REPORT_GENERATORS);
|
|
33317
35477
|
const total = entries.length;
|
|
33318
35478
|
let index = 0;
|
|
33319
35479
|
for (const [checkId, generator] of entries) {
|
|
33320
35480
|
index += 1;
|
|
35481
|
+
const checkTitle = CHECK_INFO[checkId] || checkId;
|
|
33321
35482
|
onProgress?.({
|
|
33322
35483
|
checkId,
|
|
33323
|
-
checkTitle
|
|
35484
|
+
checkTitle,
|
|
33324
35485
|
index,
|
|
33325
35486
|
total
|
|
33326
35487
|
});
|
|
33327
|
-
|
|
35488
|
+
try {
|
|
35489
|
+
reports[checkId] = withCheckSummary(await generator(client, nodeName));
|
|
35490
|
+
} catch (err) {
|
|
35491
|
+
const failure = {
|
|
35492
|
+
checkId,
|
|
35493
|
+
checkTitle,
|
|
35494
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
35495
|
+
};
|
|
35496
|
+
failures.push(failure);
|
|
35497
|
+
onCheckError?.(failure);
|
|
35498
|
+
}
|
|
35499
|
+
}
|
|
35500
|
+
if (total > 0 && Object.keys(reports).length === 0) {
|
|
35501
|
+
const first = failures[0];
|
|
35502
|
+
throw new Error(`All ${total} checks failed — first error (${first.checkId}): ${first.error.message}`);
|
|
33328
35503
|
}
|
|
33329
35504
|
return reports;
|
|
33330
35505
|
}
|
|
35506
|
+
function withCheckSummary(report) {
|
|
35507
|
+
report.summary = generateCheckSummary(report.checkId, report);
|
|
35508
|
+
return report;
|
|
35509
|
+
}
|
|
33331
35510
|
|
|
33332
35511
|
// lib/checkup-dictionary.ts
|
|
33333
35512
|
var dictionaryByCode2 = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
|
|
@@ -33483,6 +35662,7 @@ async function postRpc(params) {
|
|
|
33483
35662
|
clearTimeout(timeoutId);
|
|
33484
35663
|
timeoutId = null;
|
|
33485
35664
|
}
|
|
35665
|
+
res.setEncoding("utf8");
|
|
33486
35666
|
let data = "";
|
|
33487
35667
|
res.on("data", (chunk) => data += chunk);
|
|
33488
35668
|
res.on("end", () => {
|
|
@@ -33559,9 +35739,15 @@ async function verifyApiKey(params) {
|
|
|
33559
35739
|
signal: controller.signal
|
|
33560
35740
|
});
|
|
33561
35741
|
await response.text().catch(() => "");
|
|
33562
|
-
if (response.status === 401
|
|
35742
|
+
if (response.status === 401) {
|
|
33563
35743
|
return { status: "invalid", statusCode: response.status };
|
|
33564
35744
|
}
|
|
35745
|
+
if (response.status === 403) {
|
|
35746
|
+
return {
|
|
35747
|
+
status: "unknown",
|
|
35748
|
+
detail: "HTTP 403 from GET /checkup_reports — the token may still be allowed to upload (different permission surface)"
|
|
35749
|
+
};
|
|
35750
|
+
}
|
|
33565
35751
|
if (response.ok) {
|
|
33566
35752
|
return { status: "valid" };
|
|
33567
35753
|
}
|
|
@@ -33636,7 +35822,7 @@ async function convertCheckupReportJsonToMarkdown(params) {
|
|
|
33636
35822
|
}
|
|
33637
35823
|
|
|
33638
35824
|
// lib/checkup-summary.ts
|
|
33639
|
-
function
|
|
35825
|
+
function generateCheckSummary2(checkId, report) {
|
|
33640
35826
|
const nodeIds = Object.keys(report.results || {});
|
|
33641
35827
|
if (nodeIds.length === 0) {
|
|
33642
35828
|
return { status: "info", message: "No data" };
|
|
@@ -33644,42 +35830,46 @@ function generateCheckSummary(checkId, report) {
|
|
|
33644
35830
|
const nodeData = report.results[nodeIds[0]];
|
|
33645
35831
|
switch (checkId) {
|
|
33646
35832
|
case "H001":
|
|
33647
|
-
return
|
|
35833
|
+
return summarizeH0012(nodeData);
|
|
33648
35834
|
case "H002":
|
|
33649
|
-
return
|
|
35835
|
+
return summarizeH0022(nodeData);
|
|
33650
35836
|
case "H004":
|
|
33651
|
-
return
|
|
35837
|
+
return summarizeH0042(nodeData);
|
|
33652
35838
|
case "A002":
|
|
33653
|
-
return
|
|
35839
|
+
return summarizeA0022(nodeData);
|
|
33654
35840
|
case "A013":
|
|
33655
|
-
return
|
|
35841
|
+
return summarizeA0132(nodeData);
|
|
33656
35842
|
case "A003":
|
|
33657
|
-
return
|
|
35843
|
+
return summarizeA0032(nodeData);
|
|
33658
35844
|
case "A004":
|
|
33659
|
-
return
|
|
35845
|
+
return summarizeA0042(nodeData);
|
|
33660
35846
|
case "A007":
|
|
33661
|
-
return
|
|
35847
|
+
return summarizeA0072(nodeData);
|
|
33662
35848
|
case "D001":
|
|
33663
|
-
return
|
|
35849
|
+
return summarizeD0012(nodeData);
|
|
33664
35850
|
case "D004":
|
|
33665
|
-
return
|
|
35851
|
+
return summarizeD0042(nodeData);
|
|
33666
35852
|
case "F001":
|
|
33667
|
-
return
|
|
35853
|
+
return summarizeF0012(nodeData);
|
|
35854
|
+
case "F002":
|
|
35855
|
+
return summarizeF0022(nodeData);
|
|
33668
35856
|
case "F003":
|
|
33669
|
-
return
|
|
35857
|
+
return summarizeF0032(nodeData);
|
|
33670
35858
|
case "F004":
|
|
33671
|
-
return
|
|
35859
|
+
return summarizeBloat2(nodeData, "table");
|
|
33672
35860
|
case "F005":
|
|
33673
|
-
return
|
|
35861
|
+
return summarizeBloat2(nodeData, "index");
|
|
35862
|
+
case "F009":
|
|
35863
|
+
return summarizeF0092(nodeData);
|
|
33674
35864
|
case "G001":
|
|
33675
|
-
return
|
|
35865
|
+
return summarizeG0012(nodeData);
|
|
33676
35866
|
case "G003":
|
|
33677
|
-
return
|
|
35867
|
+
return summarizeG0032(nodeData);
|
|
33678
35868
|
default:
|
|
33679
35869
|
return { status: "info", message: "Check completed" };
|
|
33680
35870
|
}
|
|
33681
35871
|
}
|
|
33682
|
-
function
|
|
35872
|
+
function summarizeA0032(nodeData) {
|
|
33683
35873
|
const data = nodeData?.data || {};
|
|
33684
35874
|
const settingsCount = Object.keys(data).length;
|
|
33685
35875
|
if (settingsCount === 0) {
|
|
@@ -33690,7 +35880,7 @@ function summarizeA003(nodeData) {
|
|
|
33690
35880
|
message: `${settingsCount} setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33691
35881
|
};
|
|
33692
35882
|
}
|
|
33693
|
-
function
|
|
35883
|
+
function summarizeA0042(nodeData) {
|
|
33694
35884
|
const data = nodeData?.data;
|
|
33695
35885
|
if (!data) {
|
|
33696
35886
|
return { status: "info", message: "Cluster information collected" };
|
|
@@ -33701,7 +35891,7 @@ function summarizeA004(nodeData) {
|
|
|
33701
35891
|
}
|
|
33702
35892
|
return { status: "info", message: "Cluster information collected" };
|
|
33703
35893
|
}
|
|
33704
|
-
function
|
|
35894
|
+
function summarizeA0072(nodeData) {
|
|
33705
35895
|
const data = nodeData?.data || {};
|
|
33706
35896
|
const alteredCount = Object.keys(data).length;
|
|
33707
35897
|
if (alteredCount === 0) {
|
|
@@ -33712,7 +35902,7 @@ function summarizeA007(nodeData) {
|
|
|
33712
35902
|
message: `${alteredCount} setting${alteredCount > 1 ? "s" : ""} altered from defaults`
|
|
33713
35903
|
};
|
|
33714
35904
|
}
|
|
33715
|
-
function
|
|
35905
|
+
function formatBytes3(bytes) {
|
|
33716
35906
|
if (bytes === 0)
|
|
33717
35907
|
return "0 B";
|
|
33718
35908
|
if (bytes < 1024)
|
|
@@ -33723,7 +35913,7 @@ function formatBytes2(bytes) {
|
|
|
33723
35913
|
return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
|
|
33724
35914
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
33725
35915
|
}
|
|
33726
|
-
function
|
|
35916
|
+
function summarizeH0012(nodeData) {
|
|
33727
35917
|
const data = nodeData?.data || {};
|
|
33728
35918
|
let totalCount = 0;
|
|
33729
35919
|
let totalSize = 0;
|
|
@@ -33737,10 +35927,10 @@ function summarizeH001(nodeData) {
|
|
|
33737
35927
|
}
|
|
33738
35928
|
return {
|
|
33739
35929
|
status: "warning",
|
|
33740
|
-
message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${
|
|
35930
|
+
message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
|
|
33741
35931
|
};
|
|
33742
35932
|
}
|
|
33743
|
-
function
|
|
35933
|
+
function summarizeH0022(nodeData) {
|
|
33744
35934
|
const data = nodeData?.data || {};
|
|
33745
35935
|
let totalCount = 0;
|
|
33746
35936
|
let totalSize = 0;
|
|
@@ -33754,10 +35944,10 @@ function summarizeH002(nodeData) {
|
|
|
33754
35944
|
}
|
|
33755
35945
|
return {
|
|
33756
35946
|
status: "warning",
|
|
33757
|
-
message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${
|
|
35947
|
+
message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
|
|
33758
35948
|
};
|
|
33759
35949
|
}
|
|
33760
|
-
function
|
|
35950
|
+
function summarizeH0042(nodeData) {
|
|
33761
35951
|
const data = nodeData?.data || {};
|
|
33762
35952
|
let totalCount = 0;
|
|
33763
35953
|
let totalSize = 0;
|
|
@@ -33771,10 +35961,10 @@ function summarizeH004(nodeData) {
|
|
|
33771
35961
|
}
|
|
33772
35962
|
return {
|
|
33773
35963
|
status: "warning",
|
|
33774
|
-
message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${
|
|
35964
|
+
message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
|
|
33775
35965
|
};
|
|
33776
35966
|
}
|
|
33777
|
-
function
|
|
35967
|
+
function summarizeA0022(nodeData) {
|
|
33778
35968
|
const ver = nodeData?.data?.version;
|
|
33779
35969
|
if (!ver) {
|
|
33780
35970
|
return { status: "info", message: "Version checked" };
|
|
@@ -33791,7 +35981,7 @@ function summarizeA002(nodeData) {
|
|
|
33791
35981
|
message: `PostgreSQL ${major} (consider upgrading)`
|
|
33792
35982
|
};
|
|
33793
35983
|
}
|
|
33794
|
-
function
|
|
35984
|
+
function summarizeA0132(nodeData) {
|
|
33795
35985
|
const ver = nodeData?.data?.version;
|
|
33796
35986
|
if (!ver) {
|
|
33797
35987
|
return { status: "info", message: "Minor version checked" };
|
|
@@ -33802,7 +35992,7 @@ function summarizeA013(nodeData) {
|
|
|
33802
35992
|
message: `Version ${current}`
|
|
33803
35993
|
};
|
|
33804
35994
|
}
|
|
33805
|
-
function
|
|
35995
|
+
function summarizeD0012(nodeData) {
|
|
33806
35996
|
const data = nodeData?.data || {};
|
|
33807
35997
|
const settingsCount = Object.keys(data).length;
|
|
33808
35998
|
if (settingsCount === 0) {
|
|
@@ -33813,7 +36003,7 @@ function summarizeD001(nodeData) {
|
|
|
33813
36003
|
message: `${settingsCount} logging setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33814
36004
|
};
|
|
33815
36005
|
}
|
|
33816
|
-
function
|
|
36006
|
+
function summarizeD0042(nodeData) {
|
|
33817
36007
|
const data = nodeData?.data || {};
|
|
33818
36008
|
const settingsCount = Object.keys(data).length;
|
|
33819
36009
|
if (settingsCount === 0) {
|
|
@@ -33824,7 +36014,7 @@ function summarizeD004(nodeData) {
|
|
|
33824
36014
|
message: `${settingsCount} pg_stat_statements setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33825
36015
|
};
|
|
33826
36016
|
}
|
|
33827
|
-
function
|
|
36017
|
+
function summarizeF0012(nodeData) {
|
|
33828
36018
|
const data = nodeData?.data || {};
|
|
33829
36019
|
const settingsCount = Object.keys(data).length;
|
|
33830
36020
|
if (settingsCount === 0) {
|
|
@@ -33835,7 +36025,25 @@ function summarizeF001(nodeData) {
|
|
|
33835
36025
|
message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33836
36026
|
};
|
|
33837
36027
|
}
|
|
33838
|
-
function
|
|
36028
|
+
function summarizeF0022(nodeData) {
|
|
36029
|
+
if (nodeData?.data?.settings_available === false) {
|
|
36030
|
+
return { status: "info", message: "Wraparound settings unavailable" };
|
|
36031
|
+
}
|
|
36032
|
+
const severity = nodeData?.data?.severity;
|
|
36033
|
+
const databases = nodeData?.data?.databases?.length || 0;
|
|
36034
|
+
const tables = nodeData?.data?.tables || [];
|
|
36035
|
+
const offenders = tables.filter((table) => table?.xid?.severity !== "info" || table?.multixact?.severity !== "info").length;
|
|
36036
|
+
if (!severity)
|
|
36037
|
+
return { status: "info", message: "No wraparound data" };
|
|
36038
|
+
if (severity === "info") {
|
|
36039
|
+
return { status: "ok", message: `${databases} database${databases === 1 ? "" : "s"} below wraparound thresholds` };
|
|
36040
|
+
}
|
|
36041
|
+
return {
|
|
36042
|
+
status: "warning",
|
|
36043
|
+
message: `${severity.toUpperCase()} wraparound risk${offenders ? `; ${offenders} table${offenders === 1 ? "" : "s"} affected` : ""}`
|
|
36044
|
+
};
|
|
36045
|
+
}
|
|
36046
|
+
function summarizeF0032(nodeData) {
|
|
33839
36047
|
const data = nodeData?.data || {};
|
|
33840
36048
|
let flaggedCount = 0;
|
|
33841
36049
|
let disabledCount = 0;
|
|
@@ -33856,7 +36064,30 @@ function summarizeF003(nodeData) {
|
|
|
33856
36064
|
}
|
|
33857
36065
|
return { status: "warning", message: parts.join(", ") };
|
|
33858
36066
|
}
|
|
33859
|
-
function
|
|
36067
|
+
function summarizeF0092(nodeData) {
|
|
36068
|
+
const data = nodeData?.data || {};
|
|
36069
|
+
if (data.skipped) {
|
|
36070
|
+
return { status: "info", message: data.skip_reason || "Primary-only check skipped on replica" };
|
|
36071
|
+
}
|
|
36072
|
+
const severity = String(data.severity || "OK");
|
|
36073
|
+
const age = Math.max(Number(data.data_horizon_age_tx || 0), Number(data.catalog_horizon_age_tx || 0));
|
|
36074
|
+
if (severity === "OK") {
|
|
36075
|
+
return { status: "ok", message: `Xmin horizons healthy (oldest holder: ${age} transactions)` };
|
|
36076
|
+
}
|
|
36077
|
+
const holder = data.dominant_holder?.source ? `; dominant holder: ${data.dominant_holder.source}` : "";
|
|
36078
|
+
const activityAgeSeconds = Number(data.components?.pg_stat_activity?.top_blocker?.xact_age_seconds || 0);
|
|
36079
|
+
if (severity === "NOTICE" && data.data_horizon_severity === "OK" && data.catalog_horizon_severity === "OK" && activityAgeSeconds > 0) {
|
|
36080
|
+
return {
|
|
36081
|
+
status: "info",
|
|
36082
|
+
message: `NOTICE: transaction open for ${activityAgeSeconds} seconds${holder}`
|
|
36083
|
+
};
|
|
36084
|
+
}
|
|
36085
|
+
return {
|
|
36086
|
+
status: severity === "NOTICE" ? "info" : "warning",
|
|
36087
|
+
message: `${severity}: xmin horizon age ${age} transactions${holder}`
|
|
36088
|
+
};
|
|
36089
|
+
}
|
|
36090
|
+
function summarizeBloat2(nodeData, kind) {
|
|
33860
36091
|
const data = nodeData?.data || {};
|
|
33861
36092
|
let totalCount = 0;
|
|
33862
36093
|
for (const dbData of Object.values(data)) {
|
|
@@ -33875,7 +36106,7 @@ function summarizeBloat(nodeData, kind) {
|
|
|
33875
36106
|
message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
|
|
33876
36107
|
};
|
|
33877
36108
|
}
|
|
33878
|
-
function
|
|
36109
|
+
function summarizeG0012(nodeData) {
|
|
33879
36110
|
const data = nodeData?.data || {};
|
|
33880
36111
|
const settingsCount = Object.keys(data).length;
|
|
33881
36112
|
if (settingsCount === 0) {
|
|
@@ -33886,7 +36117,7 @@ function summarizeG001(nodeData) {
|
|
|
33886
36117
|
message: `${settingsCount} memory setting${settingsCount > 1 ? "s" : ""} collected`
|
|
33887
36118
|
};
|
|
33888
36119
|
}
|
|
33889
|
-
function
|
|
36120
|
+
function summarizeG0032(nodeData) {
|
|
33890
36121
|
const data = nodeData?.data || {};
|
|
33891
36122
|
const settings = data.settings || {};
|
|
33892
36123
|
const deadlockStats = data.deadlock_stats;
|
|
@@ -34318,7 +36549,7 @@ Checkup report uploaded`);
|
|
|
34318
36549
|
for (const item of summary.uploaded) {
|
|
34319
36550
|
const report = reports[item.checkId];
|
|
34320
36551
|
if (report) {
|
|
34321
|
-
const { status, message } =
|
|
36552
|
+
const { status, message } = generateCheckSummary2(item.checkId, report);
|
|
34322
36553
|
const title = report.checkTitle || item.checkId;
|
|
34323
36554
|
const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
|
|
34324
36555
|
if (isSignificant) {
|
|
@@ -34354,6 +36585,14 @@ async function downloadText(url) {
|
|
|
34354
36585
|
clearTimeout(timeout);
|
|
34355
36586
|
}
|
|
34356
36587
|
}
|
|
36588
|
+
function composeRefCandidates() {
|
|
36589
|
+
return [
|
|
36590
|
+
process.env.PGAI_PROJECT_REF,
|
|
36591
|
+
package_default.version,
|
|
36592
|
+
`v${package_default.version}`,
|
|
36593
|
+
"main"
|
|
36594
|
+
].filter((v) => Boolean(v && v.trim()));
|
|
36595
|
+
}
|
|
34357
36596
|
async function ensureDefaultMonitoringProject() {
|
|
34358
36597
|
const projectDir = getDefaultMonitoringProjectDir();
|
|
34359
36598
|
const composeFile = path7.resolve(projectDir, "docker-compose.yml");
|
|
@@ -34362,12 +36601,7 @@ async function ensureDefaultMonitoringProject() {
|
|
|
34362
36601
|
fs9.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
34363
36602
|
}
|
|
34364
36603
|
if (!fs9.existsSync(composeFile)) {
|
|
34365
|
-
const refs =
|
|
34366
|
-
process.env.PGAI_PROJECT_REF,
|
|
34367
|
-
package_default.version,
|
|
34368
|
-
`v${package_default.version}`,
|
|
34369
|
-
"main"
|
|
34370
|
-
].filter((v) => Boolean(v && v.trim()));
|
|
36604
|
+
const refs = composeRefCandidates();
|
|
34371
36605
|
let lastErr;
|
|
34372
36606
|
for (const ref of refs) {
|
|
34373
36607
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -34459,11 +36693,7 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
34459
36693
|
const composeFile = path7.resolve(projectDir, "docker-compose.yml");
|
|
34460
36694
|
if (!fs9.existsSync(composeFile))
|
|
34461
36695
|
return false;
|
|
34462
|
-
const refs =
|
|
34463
|
-
process.env.PGAI_PROJECT_REF,
|
|
34464
|
-
package_default.version,
|
|
34465
|
-
`v${package_default.version}`
|
|
34466
|
-
].filter((v) => Boolean(v && v.trim()));
|
|
36696
|
+
const refs = composeRefCandidates();
|
|
34467
36697
|
const fetched = await fetchTargetCompose(refs);
|
|
34468
36698
|
if (!fetched || !isValidComposeYaml(fetched)) {
|
|
34469
36699
|
console.error(`\u26A0 Could not refresh docker-compose.yml to ${package_default.version} (no valid compose was retrieved).`);
|
|
@@ -34522,6 +36752,36 @@ function printResult(result, json3) {
|
|
|
34522
36752
|
}
|
|
34523
36753
|
var program2 = new Command;
|
|
34524
36754
|
program2.name("postgres-ai").description("PostgresAI CLI").version(package_default.version).option("--api-key <key>", "API key (overrides PGAI_API_KEY)").option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)").option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)").option("--storage-base-url <url>", "Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)");
|
|
36755
|
+
program2.showHelpAfterError();
|
|
36756
|
+
program2.addHelpText("after", () => `
|
|
36757
|
+
\uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL}
|
|
36758
|
+
`);
|
|
36759
|
+
program2.hook("postAction", (_thisCommand, actionCommand) => {
|
|
36760
|
+
const name = actionCommand.name();
|
|
36761
|
+
if (name === "feedback" || name === "help")
|
|
36762
|
+
return;
|
|
36763
|
+
if (process.exitCode && process.exitCode !== 0)
|
|
36764
|
+
return;
|
|
36765
|
+
const opts = actionCommand.opts();
|
|
36766
|
+
maybeEmitFeedbackTip({
|
|
36767
|
+
isTTY: !!process.stdout.isTTY,
|
|
36768
|
+
json: !!opts.json,
|
|
36769
|
+
suppress: process.env[FEEDBACK_SUPPRESS_ENV]
|
|
36770
|
+
});
|
|
36771
|
+
});
|
|
36772
|
+
program2.command("feedback").description("share ideas or feedback about the CLI").option("--open", "open the feedback page in your browser", false).option("--json", "output the feedback URL as JSON (machine-readable)", false).action(async (opts) => {
|
|
36773
|
+
if (opts.json) {
|
|
36774
|
+
console.log(feedbackJson());
|
|
36775
|
+
return;
|
|
36776
|
+
}
|
|
36777
|
+
console.log(feedbackMessage());
|
|
36778
|
+
if (opts.open) {
|
|
36779
|
+
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
36780
|
+
try {
|
|
36781
|
+
spawn2(openCommand, [FEEDBACK_URL], { detached: true, stdio: "ignore" }).unref();
|
|
36782
|
+
} catch {}
|
|
36783
|
+
}
|
|
36784
|
+
});
|
|
34525
36785
|
program2.command("set-default-project <project>").description("store default project for checkup uploads").action(async (project) => {
|
|
34526
36786
|
const value = (project || "").trim();
|
|
34527
36787
|
if (!value) {
|
|
@@ -35579,9 +37839,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35579
37839
|
return;
|
|
35580
37840
|
}
|
|
35581
37841
|
let reports;
|
|
37842
|
+
const failedChecks = [];
|
|
35582
37843
|
if (checkId === "ALL") {
|
|
35583
37844
|
reports = await generateAllReports(client, opts.nodeName, (p) => {
|
|
35584
37845
|
spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
|
|
37846
|
+
}, (failure) => {
|
|
37847
|
+
failedChecks.push(failure);
|
|
37848
|
+
console.error(`Warning: check ${failure.checkId} (${failure.checkTitle}) failed: ${failure.error.message} \u2014 continuing with remaining checks`);
|
|
35585
37849
|
});
|
|
35586
37850
|
} else {
|
|
35587
37851
|
const generator = REPORT_GENERATORS[checkId];
|
|
@@ -35599,7 +37863,7 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35599
37863
|
return;
|
|
35600
37864
|
}
|
|
35601
37865
|
spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
|
|
35602
|
-
reports = { [checkId]: await generator(client, opts.nodeName) };
|
|
37866
|
+
reports = { [checkId]: withCheckSummary(await generator(client, opts.nodeName)) };
|
|
35603
37867
|
}
|
|
35604
37868
|
let uploadSummary;
|
|
35605
37869
|
if (uploadCfg) {
|
|
@@ -35686,7 +37950,7 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35686
37950
|
const summaries = [];
|
|
35687
37951
|
let skippedCount = 0;
|
|
35688
37952
|
for (const [checkId2, report] of Object.entries(reports)) {
|
|
35689
|
-
const { status, message } =
|
|
37953
|
+
const { status, message } = generateCheckSummary2(checkId2, report);
|
|
35690
37954
|
const title = report.checkTitle || checkId2;
|
|
35691
37955
|
const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
|
|
35692
37956
|
if (isSignificant) {
|
|
@@ -35709,6 +37973,11 @@ For details:`);
|
|
|
35709
37973
|
}
|
|
35710
37974
|
console.log(" --output <dir> Save to directory");
|
|
35711
37975
|
}
|
|
37976
|
+
if (failedChecks.length > 0) {
|
|
37977
|
+
const ids = failedChecks.map((f) => f.checkId).join(", ");
|
|
37978
|
+
console.error(`Warning: ${failedChecks.length} of ${failedChecks.length + Object.keys(reports).length} checks failed (${ids}); results are partial`);
|
|
37979
|
+
process.exitCode = 1;
|
|
37980
|
+
}
|
|
35712
37981
|
} catch (error2) {
|
|
35713
37982
|
if (error2 instanceof RpcError) {
|
|
35714
37983
|
for (const line of formatRpcErrorForDisplay(error2)) {
|
|
@@ -35971,7 +38240,7 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35971
38240
|
child.on("close", (code) => resolve8(code || 0));
|
|
35972
38241
|
});
|
|
35973
38242
|
}
|
|
35974
|
-
program2.command("help"
|
|
38243
|
+
program2.command("help").description("show help").action(() => {
|
|
35975
38244
|
program2.outputHelp();
|
|
35976
38245
|
});
|
|
35977
38246
|
var mon = program2.command("mon").description("monitoring services management");
|
|
@@ -36372,19 +38641,24 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36372
38641
|
} else {
|
|
36373
38642
|
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
36374
38643
|
}
|
|
36375
|
-
|
|
36376
|
-
|
|
36377
|
-
instancesPath,
|
|
36378
|
-
vcpus: parseVcpus(opts.vcpus ?? process.env.PGAI_VCPUS),
|
|
36379
|
-
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
36380
|
-
debug: !!process.env.DEBUG
|
|
36381
|
-
});
|
|
36382
|
-
if (aas.ok) {
|
|
36383
|
-
console.log(`\u2713 AAS auto-collection registered
|
|
38644
|
+
if (reg === null) {
|
|
38645
|
+
console.error(`\u26A0 Skipping AAS auto-collection arming because instance adoption failed; it can be enabled later by re-running 'postgresai mon local-install'
|
|
36384
38646
|
`);
|
|
36385
38647
|
} else {
|
|
36386
|
-
|
|
38648
|
+
const aas = await registerAasCollection(apiKey, instanceId, {
|
|
38649
|
+
grafanaPassword,
|
|
38650
|
+
instancesPath,
|
|
38651
|
+
vcpus: parseVcpus(opts.vcpus ?? process.env.PGAI_VCPUS),
|
|
38652
|
+
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
38653
|
+
debug: !!process.env.DEBUG
|
|
38654
|
+
});
|
|
38655
|
+
if (aas.ok) {
|
|
38656
|
+
console.log(`\u2713 AAS auto-collection registered
|
|
38657
|
+
`);
|
|
38658
|
+
} else {
|
|
38659
|
+
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
36387
38660
|
`);
|
|
38661
|
+
}
|
|
36388
38662
|
}
|
|
36389
38663
|
} else if (plan.kind === "error-missing-project") {
|
|
36390
38664
|
console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
|
|
@@ -37014,7 +39288,9 @@ Opening browser for authentication...`);
|
|
|
37014
39288
|
${authUrl}
|
|
37015
39289
|
`);
|
|
37016
39290
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
37017
|
-
spawn2(openCommand, [authUrl], { detached: true, stdio: "ignore" })
|
|
39291
|
+
const browserProcess = spawn2(openCommand, [authUrl], { detached: true, stdio: "ignore" });
|
|
39292
|
+
browserProcess.on("error", () => {});
|
|
39293
|
+
browserProcess.unref();
|
|
37018
39294
|
console.log("Waiting for authorization...");
|
|
37019
39295
|
console.log(`(Press Ctrl+C to cancel)
|
|
37020
39296
|
`);
|
|
@@ -37073,10 +39349,22 @@ Please verify the --api-base-url parameter.`);
|
|
|
37073
39349
|
const result = JSON.parse(exchangeBody);
|
|
37074
39350
|
const apiToken = result.api_token || result?.[0]?.result?.api_token;
|
|
37075
39351
|
const orgId = result.org_id || result?.[0]?.result?.org_id;
|
|
39352
|
+
const tokenValid = typeof apiToken === "string" && apiToken.length > 0;
|
|
39353
|
+
const orgIdValid = orgId !== undefined && orgId !== null && orgId !== "";
|
|
39354
|
+
if (!tokenValid || !orgIdValid) {
|
|
39355
|
+
const missingField = !tokenValid ? "an API token" : "an organization ID";
|
|
39356
|
+
console.error(`Authentication failed: server response did not include ${missingField}.`);
|
|
39357
|
+
console.error("Your existing configuration has been left unchanged.");
|
|
39358
|
+
if (opts.debug) {
|
|
39359
|
+
console.error(`Debug: exchange response body: ${exchangeBody}`);
|
|
39360
|
+
}
|
|
39361
|
+
process.exit(1);
|
|
39362
|
+
return;
|
|
39363
|
+
}
|
|
37076
39364
|
const existingConfig = readConfig();
|
|
37077
39365
|
const existingOrgId = existingConfig.orgId;
|
|
37078
39366
|
const existingProject = existingConfig.defaultProject;
|
|
37079
|
-
const orgChanged = existingOrgId && existingOrgId !== orgId;
|
|
39367
|
+
const orgChanged = existingOrgId != null && existingOrgId !== orgId;
|
|
37080
39368
|
writeConfig({
|
|
37081
39369
|
apiKey: apiToken,
|
|
37082
39370
|
baseUrl: apiBaseUrl,
|
|
@@ -38030,6 +40318,369 @@ function tryParseJson(s) {
|
|
|
38030
40318
|
return s;
|
|
38031
40319
|
}
|
|
38032
40320
|
}
|
|
40321
|
+
function printJoeOutcome(outcome, json3, budgetMs) {
|
|
40322
|
+
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
40323
|
+
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
40324
|
+
if (outcome.budgetExpired) {
|
|
40325
|
+
if (json3) {
|
|
40326
|
+
console.log(JSON.stringify({
|
|
40327
|
+
command_id: outcome.commandId,
|
|
40328
|
+
status: outcome.status,
|
|
40329
|
+
budget_expired: true,
|
|
40330
|
+
resume: `pgai joe result ${outcome.commandId}`
|
|
40331
|
+
}, null, 2));
|
|
40332
|
+
} else {
|
|
40333
|
+
console.log(`started ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
|
|
40334
|
+
}
|
|
40335
|
+
return;
|
|
40336
|
+
}
|
|
40337
|
+
const output = outcome.output;
|
|
40338
|
+
if (outcome.status === "ok") {
|
|
40339
|
+
if (!output) {
|
|
40340
|
+
console.error(`command ${outcome.commandId} ok but output is empty`);
|
|
40341
|
+
process.exitCode = 1;
|
|
40342
|
+
return;
|
|
40343
|
+
}
|
|
40344
|
+
if (json3)
|
|
40345
|
+
console.log(JSON.stringify(output, null, 2));
|
|
40346
|
+
else {
|
|
40347
|
+
console.log(`command ${outcome.commandId} \xB7 ok`);
|
|
40348
|
+
const body = formatJoeOutput(output);
|
|
40349
|
+
if (body)
|
|
40350
|
+
console.log(body);
|
|
40351
|
+
}
|
|
40352
|
+
return;
|
|
40353
|
+
}
|
|
40354
|
+
if (json3 && output)
|
|
40355
|
+
console.log(JSON.stringify(output, null, 2));
|
|
40356
|
+
console.error(`command ${outcome.commandId} error: ${output?.error ?? "command failed"}`);
|
|
40357
|
+
process.exitCode = 1;
|
|
40358
|
+
}
|
|
40359
|
+
async function runJoeCli(command, arg, opts) {
|
|
40360
|
+
try {
|
|
40361
|
+
const rootOpts = program2.opts();
|
|
40362
|
+
const cfg = readConfig();
|
|
40363
|
+
const { apiKey } = getConfig(rootOpts);
|
|
40364
|
+
if (!apiKey) {
|
|
40365
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40366
|
+
process.exitCode = 1;
|
|
40367
|
+
return;
|
|
40368
|
+
}
|
|
40369
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
40370
|
+
const instanceRef = (opts.instanceId ?? "").toString().trim();
|
|
40371
|
+
if (!projectRef && !instanceRef) {
|
|
40372
|
+
console.error("Specify --project <id|alias> or --instance-id <id>.");
|
|
40373
|
+
process.exitCode = 1;
|
|
40374
|
+
return;
|
|
40375
|
+
}
|
|
40376
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40377
|
+
if (typeof opts.budget === "number" && (!Number.isFinite(opts.budget) || opts.budget < 0)) {
|
|
40378
|
+
throw new Error("--budget must be a non-negative number of seconds");
|
|
40379
|
+
}
|
|
40380
|
+
const budgetMs = typeof opts.budget === "number" ? opts.budget * 1000 : undefined;
|
|
40381
|
+
const outcome = await executeJoeCommand({
|
|
40382
|
+
apiKey,
|
|
40383
|
+
apiBaseUrl,
|
|
40384
|
+
command,
|
|
40385
|
+
project: projectRef || undefined,
|
|
40386
|
+
instanceId: instanceRef || undefined,
|
|
40387
|
+
input: { arg, variant: opts.variant ?? null },
|
|
40388
|
+
orgId: cfg.orgId ?? undefined,
|
|
40389
|
+
budgetMs,
|
|
40390
|
+
debug: !!opts.debug
|
|
40391
|
+
});
|
|
40392
|
+
printJoeOutcome(outcome, !!opts.json, budgetMs);
|
|
40393
|
+
} catch (err) {
|
|
40394
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40395
|
+
console.error(message);
|
|
40396
|
+
process.exitCode = 1;
|
|
40397
|
+
}
|
|
40398
|
+
}
|
|
40399
|
+
function withJoeOptions(cmd) {
|
|
40400
|
+
return cmd.option("--instance-id <id>", "target the Joe instance id directly (skips --project resolution); required unless --project is given").option("--project <id|alias>", "target project by numeric id OR alias/name; required unless --instance-id is given").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseFloat(v)).option("--debug", "enable debug output").option("--json", "output raw JSON");
|
|
40401
|
+
}
|
|
40402
|
+
var joe = program2.command("joe").description("Joe \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones. " + "All subcommands except 'result' require --project <id|alias> or --instance-id <id>.");
|
|
40403
|
+
withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
|
|
40404
|
+
await runJoeCli("plan", sql, opts);
|
|
40405
|
+
});
|
|
40406
|
+
withJoeOptions(joe.command("explain <sql>").description("EXPLAIN + EXPLAIN ANALYZE a query (EXECUTES on the ephemeral clone)")).action(async (sql, opts) => {
|
|
40407
|
+
await runJoeCli("explain", sql, opts);
|
|
40408
|
+
});
|
|
40409
|
+
withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
|
|
40410
|
+
await runJoeCli("exec", sql, opts);
|
|
40411
|
+
});
|
|
40412
|
+
withJoeOptions(joe.command("hypo <args>").description("HypoPG hypothetical indexes (e.g. `hypo create index on users (email)`, `hypo desc`, `hypo reset`)")).action(async (args, opts) => {
|
|
40413
|
+
await runJoeCli("hypo", args, opts);
|
|
40414
|
+
});
|
|
40415
|
+
withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity) on the clone")).action(async (opts) => {
|
|
40416
|
+
await runJoeCli("activity", null, opts);
|
|
40417
|
+
});
|
|
40418
|
+
withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
40419
|
+
await runJoeCli("terminate", pid, opts);
|
|
40420
|
+
});
|
|
40421
|
+
withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
40422
|
+
await runJoeCli("reset", null, opts);
|
|
40423
|
+
});
|
|
40424
|
+
withJoeOptions(joe.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
|
|
40425
|
+
await runJoeCli("describe", object4, opts);
|
|
40426
|
+
});
|
|
40427
|
+
joe.command("result <commandId>").description("fetch a Joe command's output by id (resume a budget-expired one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
|
|
40428
|
+
try {
|
|
40429
|
+
const rootOpts = program2.opts();
|
|
40430
|
+
const cfg = readConfig();
|
|
40431
|
+
const { apiKey } = getConfig(rootOpts);
|
|
40432
|
+
if (!apiKey) {
|
|
40433
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40434
|
+
process.exitCode = 1;
|
|
40435
|
+
return;
|
|
40436
|
+
}
|
|
40437
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40438
|
+
const output = await getCommandOutput({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
40439
|
+
if (opts.json) {
|
|
40440
|
+
console.log(JSON.stringify(output, null, 2));
|
|
40441
|
+
if (output.status !== "ok") {
|
|
40442
|
+
process.exitCode = 1;
|
|
40443
|
+
}
|
|
40444
|
+
return;
|
|
40445
|
+
}
|
|
40446
|
+
if (output.status === "ok") {
|
|
40447
|
+
console.log(`command ${output.command_id} \xB7 ok`);
|
|
40448
|
+
const body = formatJoeOutput(output);
|
|
40449
|
+
if (body)
|
|
40450
|
+
console.log(body);
|
|
40451
|
+
} else if (output.status === "error") {
|
|
40452
|
+
console.error(`command ${output.command_id} error: ${output.error ?? "command failed"}`);
|
|
40453
|
+
process.exitCode = 1;
|
|
40454
|
+
} else {
|
|
40455
|
+
console.error(`command ${output.command_id} \xB7 ${output.status} \u2014 result is not ready`);
|
|
40456
|
+
process.exitCode = 1;
|
|
40457
|
+
}
|
|
40458
|
+
} catch (err) {
|
|
40459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40460
|
+
console.error(message);
|
|
40461
|
+
process.exitCode = 1;
|
|
40462
|
+
}
|
|
40463
|
+
});
|
|
40464
|
+
program2.command("projects").description("list the org's projects (shows which have Joe ready) \u2014 org-level, not a Joe endpoint").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40465
|
+
try {
|
|
40466
|
+
const rootOpts = program2.opts();
|
|
40467
|
+
const cfg = readConfig();
|
|
40468
|
+
const { apiKey } = getConfig(rootOpts);
|
|
40469
|
+
if (!apiKey) {
|
|
40470
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40471
|
+
process.exitCode = 1;
|
|
40472
|
+
return;
|
|
40473
|
+
}
|
|
40474
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40475
|
+
const projects = await listProjects({
|
|
40476
|
+
apiKey,
|
|
40477
|
+
apiBaseUrl,
|
|
40478
|
+
orgId: cfg.orgId ?? undefined,
|
|
40479
|
+
debug: !!opts.debug
|
|
40480
|
+
});
|
|
40481
|
+
if (opts.json) {
|
|
40482
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
40483
|
+
} else {
|
|
40484
|
+
console.log(formatProjectsTable(projects));
|
|
40485
|
+
}
|
|
40486
|
+
} catch (err) {
|
|
40487
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40488
|
+
console.error(message);
|
|
40489
|
+
process.exitCode = 1;
|
|
40490
|
+
}
|
|
40491
|
+
});
|
|
40492
|
+
async function resolveDblabTarget(project, debug) {
|
|
40493
|
+
const rootOpts = program2.opts();
|
|
40494
|
+
const cfg = readConfig();
|
|
40495
|
+
const { apiKey } = getConfig(rootOpts);
|
|
40496
|
+
if (!apiKey) {
|
|
40497
|
+
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40498
|
+
}
|
|
40499
|
+
const ref = (project ?? "").trim();
|
|
40500
|
+
if (!ref) {
|
|
40501
|
+
throw new Error("--project <id|alias> is required");
|
|
40502
|
+
}
|
|
40503
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40504
|
+
const orgId = cfg.orgId ?? undefined;
|
|
40505
|
+
const instanceId = await resolveDblabInstanceId({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
40506
|
+
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
40507
|
+
}
|
|
40508
|
+
var dblab = program2.command("dblab").description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
|
|
40509
|
+
var clone2 = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
40510
|
+
clone2.command("create").description("create a thin clone of the project's database").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "branch to clone from").option("--snapshot <id>", "snapshot id to clone from").option("--id <id>", "clone id (DBLab generates one when omitted)").option("--db-user <user>", "clone DB user (set password via PGAI_CLONE_DB_PASSWORD)").option("--protected", "protect the clone from auto-deletion").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40511
|
+
try {
|
|
40512
|
+
const dbPassword = process.env.PGAI_CLONE_DB_PASSWORD;
|
|
40513
|
+
if (opts.dbUser && !dbPassword || !opts.dbUser && dbPassword) {
|
|
40514
|
+
throw new Error("--db-user and PGAI_CLONE_DB_PASSWORD must be provided together");
|
|
40515
|
+
}
|
|
40516
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40517
|
+
const result = await createClone({
|
|
40518
|
+
apiKey,
|
|
40519
|
+
apiBaseUrl,
|
|
40520
|
+
instanceId,
|
|
40521
|
+
cloneId: opts.id,
|
|
40522
|
+
branch: opts.branch,
|
|
40523
|
+
snapshotId: opts.snapshot,
|
|
40524
|
+
dbUser: opts.dbUser,
|
|
40525
|
+
dbPassword,
|
|
40526
|
+
isProtected: !!opts.protected,
|
|
40527
|
+
debug: !!opts.debug
|
|
40528
|
+
});
|
|
40529
|
+
printResult(result, opts.json);
|
|
40530
|
+
} catch (err) {
|
|
40531
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40532
|
+
process.exitCode = 1;
|
|
40533
|
+
}
|
|
40534
|
+
});
|
|
40535
|
+
clone2.command("list").description("list the project's thin clones").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40536
|
+
try {
|
|
40537
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40538
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
40539
|
+
printResult(result, opts.json);
|
|
40540
|
+
} catch (err) {
|
|
40541
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40542
|
+
process.exitCode = 1;
|
|
40543
|
+
}
|
|
40544
|
+
});
|
|
40545
|
+
clone2.command("status <cloneId>").description("show a clone's status").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40546
|
+
try {
|
|
40547
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40548
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
40549
|
+
printResult(result, opts.json);
|
|
40550
|
+
} catch (err) {
|
|
40551
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40552
|
+
process.exitCode = 1;
|
|
40553
|
+
}
|
|
40554
|
+
});
|
|
40555
|
+
clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to reset to (default: latest)").option("--latest", "reset to the latest snapshot").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40556
|
+
try {
|
|
40557
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40558
|
+
const result = await resetClone({
|
|
40559
|
+
apiKey,
|
|
40560
|
+
apiBaseUrl,
|
|
40561
|
+
instanceId,
|
|
40562
|
+
cloneId,
|
|
40563
|
+
snapshotId: opts.snapshot,
|
|
40564
|
+
latest: opts.latest,
|
|
40565
|
+
debug: !!opts.debug
|
|
40566
|
+
});
|
|
40567
|
+
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
40568
|
+
} catch (err) {
|
|
40569
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40570
|
+
process.exitCode = 1;
|
|
40571
|
+
}
|
|
40572
|
+
});
|
|
40573
|
+
clone2.command("destroy <cloneId>").description("destroy a clone (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40574
|
+
try {
|
|
40575
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40576
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
40577
|
+
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
40578
|
+
} catch (err) {
|
|
40579
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40580
|
+
process.exitCode = 1;
|
|
40581
|
+
}
|
|
40582
|
+
});
|
|
40583
|
+
var branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
40584
|
+
branch.command("list").description("list the project's branches").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40585
|
+
try {
|
|
40586
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40587
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
40588
|
+
printResult(result, opts.json);
|
|
40589
|
+
} catch (err) {
|
|
40590
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40591
|
+
process.exitCode = 1;
|
|
40592
|
+
}
|
|
40593
|
+
});
|
|
40594
|
+
branch.command("create <name>").description("create a branch").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to base the branch on").option("--base-branch <branch>", "parent branch to fork from").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40595
|
+
try {
|
|
40596
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40597
|
+
const result = await createBranch({
|
|
40598
|
+
apiKey,
|
|
40599
|
+
apiBaseUrl,
|
|
40600
|
+
instanceId,
|
|
40601
|
+
branchName: name,
|
|
40602
|
+
baseBranch: opts.baseBranch,
|
|
40603
|
+
snapshotId: opts.snapshot,
|
|
40604
|
+
debug: !!opts.debug
|
|
40605
|
+
});
|
|
40606
|
+
printResult(result, opts.json);
|
|
40607
|
+
} catch (err) {
|
|
40608
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40609
|
+
process.exitCode = 1;
|
|
40610
|
+
}
|
|
40611
|
+
});
|
|
40612
|
+
branch.command("delete <name>").description("delete a branch (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40613
|
+
try {
|
|
40614
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40615
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40616
|
+
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
40617
|
+
} catch (err) {
|
|
40618
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40619
|
+
process.exitCode = 1;
|
|
40620
|
+
}
|
|
40621
|
+
});
|
|
40622
|
+
branch.command("log <name>").description("show a branch's snapshot log").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40623
|
+
try {
|
|
40624
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40625
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40626
|
+
printResult(result, opts.json);
|
|
40627
|
+
} catch (err) {
|
|
40628
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40629
|
+
process.exitCode = 1;
|
|
40630
|
+
}
|
|
40631
|
+
});
|
|
40632
|
+
var snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
40633
|
+
snapshot.command("list").description("list the project's snapshots").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "filter by branch").option("--dataset <dataset>", "filter by dataset").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40634
|
+
try {
|
|
40635
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40636
|
+
const result = await listSnapshots({
|
|
40637
|
+
apiKey,
|
|
40638
|
+
apiBaseUrl,
|
|
40639
|
+
instanceId,
|
|
40640
|
+
branchName: opts.branch,
|
|
40641
|
+
dataset: opts.dataset,
|
|
40642
|
+
debug: !!opts.debug
|
|
40643
|
+
});
|
|
40644
|
+
printResult(result, opts.json);
|
|
40645
|
+
} catch (err) {
|
|
40646
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40647
|
+
process.exitCode = 1;
|
|
40648
|
+
}
|
|
40649
|
+
});
|
|
40650
|
+
snapshot.command("create").description("create a snapshot from a clone").requiredOption("--project <id|alias>", "project id or alias").requiredOption("--clone <id>", "clone id to snapshot").option("--message <message>", "snapshot message").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40651
|
+
try {
|
|
40652
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40653
|
+
const result = await createSnapshot({
|
|
40654
|
+
apiKey,
|
|
40655
|
+
apiBaseUrl,
|
|
40656
|
+
instanceId,
|
|
40657
|
+
cloneId: opts.clone,
|
|
40658
|
+
message: opts.message,
|
|
40659
|
+
debug: !!opts.debug
|
|
40660
|
+
});
|
|
40661
|
+
printResult(result, opts.json);
|
|
40662
|
+
} catch (err) {
|
|
40663
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40664
|
+
process.exitCode = 1;
|
|
40665
|
+
}
|
|
40666
|
+
});
|
|
40667
|
+
snapshot.command("destroy <snapshotId>").description("destroy a snapshot (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--force", "force-delete even when dependent clones exist").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (snapshotId, opts) => {
|
|
40668
|
+
try {
|
|
40669
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40670
|
+
const result = await destroySnapshot({
|
|
40671
|
+
apiKey,
|
|
40672
|
+
apiBaseUrl,
|
|
40673
|
+
instanceId,
|
|
40674
|
+
snapshotId,
|
|
40675
|
+
force: !!opts.force,
|
|
40676
|
+
debug: !!opts.debug
|
|
40677
|
+
});
|
|
40678
|
+
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
40679
|
+
} catch (err) {
|
|
40680
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40681
|
+
process.exitCode = 1;
|
|
40682
|
+
}
|
|
40683
|
+
});
|
|
38033
40684
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
38034
40685
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
38035
40686
|
const rootOpts = program2.opts();
|
|
@@ -38150,9 +40801,14 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
38150
40801
|
}
|
|
38151
40802
|
});
|
|
38152
40803
|
if (__require.main == __require.module) {
|
|
38153
|
-
|
|
40804
|
+
if (process.argv.length <= 2) {
|
|
40805
|
+
program2.outputHelp();
|
|
38154
40806
|
closeReadline();
|
|
38155
|
-
}
|
|
40807
|
+
} else {
|
|
40808
|
+
program2.parseAsync(process.argv).finally(() => {
|
|
40809
|
+
closeReadline();
|
|
40810
|
+
});
|
|
40811
|
+
}
|
|
38156
40812
|
}
|
|
38157
40813
|
export {
|
|
38158
40814
|
resolveAdoptedProject,
|