@yawlabs/postgres-mcp 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +194 -0
- package/README.md +51 -10
- package/bin/postgres-mcp.mjs +48 -5
- package/dist/index.js +1600 -363
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3114,7 +3114,25 @@ var require_utils = __commonJS({
|
|
|
3114
3114
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3115
3115
|
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3116
3116
|
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3117
|
-
var isPathCharacter = RegExp.prototype.test.bind(/^[
|
|
3117
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
|
|
3118
|
+
var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u);
|
|
3119
|
+
var isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u);
|
|
3120
|
+
var BYTE_HEX = new Array(256);
|
|
3121
|
+
{
|
|
3122
|
+
const HEX_DIGITS = "0123456789ABCDEF";
|
|
3123
|
+
for (let i = 0; i < 256; i++) {
|
|
3124
|
+
BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function percentEncodeNonAscii(cp) {
|
|
3128
|
+
if (cp < 2048) {
|
|
3129
|
+
return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63];
|
|
3130
|
+
}
|
|
3131
|
+
if (cp < 65536) {
|
|
3132
|
+
return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
|
|
3133
|
+
}
|
|
3134
|
+
return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
|
|
3135
|
+
}
|
|
3118
3136
|
function stringArrayToHexStripped(input) {
|
|
3119
3137
|
let acc = "";
|
|
3120
3138
|
let code = 0;
|
|
@@ -3139,91 +3157,105 @@ var require_utils = __commonJS({
|
|
|
3139
3157
|
}
|
|
3140
3158
|
return acc;
|
|
3141
3159
|
}
|
|
3160
|
+
var isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/);
|
|
3161
|
+
var isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/);
|
|
3162
|
+
var isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/);
|
|
3142
3163
|
var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
|
|
3143
|
-
function
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
if (hex3 !== "") {
|
|
3151
|
-
address.push(hex3);
|
|
3152
|
-
} else {
|
|
3153
|
-
output.error = true;
|
|
3154
|
-
return false;
|
|
3164
|
+
function isZoneIdentifier(zone) {
|
|
3165
|
+
if (zone.length === 0) return false;
|
|
3166
|
+
for (let i = 0; i < zone.length; i++) {
|
|
3167
|
+
if (isZoneCharacter(zone[i])) continue;
|
|
3168
|
+
if (zone[i] === "%" && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
|
|
3169
|
+
i += 2;
|
|
3170
|
+
continue;
|
|
3155
3171
|
}
|
|
3156
|
-
|
|
3172
|
+
return false;
|
|
3157
3173
|
}
|
|
3158
3174
|
return true;
|
|
3159
3175
|
}
|
|
3160
|
-
function
|
|
3161
|
-
let
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
let
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
}
|
|
3173
|
-
if (cursor === ":") {
|
|
3174
|
-
if (endipv6Encountered === true) {
|
|
3175
|
-
endIpv6 = true;
|
|
3176
|
-
}
|
|
3177
|
-
if (!consume(buffer, address, output)) {
|
|
3178
|
-
break;
|
|
3176
|
+
function compressIPv6ZeroRun(hextets) {
|
|
3177
|
+
let bestStart = -1;
|
|
3178
|
+
let bestLength = 0;
|
|
3179
|
+
let runStart = -1;
|
|
3180
|
+
let runLength = 0;
|
|
3181
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
3182
|
+
if (hextets[i] === "0") {
|
|
3183
|
+
if (runStart === -1) runStart = i;
|
|
3184
|
+
runLength++;
|
|
3185
|
+
if (runLength > bestLength) {
|
|
3186
|
+
bestLength = runLength;
|
|
3187
|
+
bestStart = runStart;
|
|
3179
3188
|
}
|
|
3180
|
-
if (++tokenCount > 7) {
|
|
3181
|
-
output.error = true;
|
|
3182
|
-
break;
|
|
3183
|
-
}
|
|
3184
|
-
if (i > 0 && input[i - 1] === ":") {
|
|
3185
|
-
endipv6Encountered = true;
|
|
3186
|
-
}
|
|
3187
|
-
address.push(":");
|
|
3188
|
-
continue;
|
|
3189
|
-
} else if (cursor === "%") {
|
|
3190
|
-
if (!consume(buffer, address, output)) {
|
|
3191
|
-
break;
|
|
3192
|
-
}
|
|
3193
|
-
consume = consumeIsZone;
|
|
3194
3189
|
} else {
|
|
3195
|
-
|
|
3190
|
+
runStart = -1;
|
|
3191
|
+
runLength = 0;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
if (bestLength < 2) return hextets.join(":");
|
|
3195
|
+
const head = hextets.slice(0, bestStart).join(":");
|
|
3196
|
+
const tail = hextets.slice(bestStart + bestLength).join(":");
|
|
3197
|
+
return head + "::" + tail;
|
|
3198
|
+
}
|
|
3199
|
+
function normalizeIPv6Address(input) {
|
|
3200
|
+
const compression = input.indexOf("::");
|
|
3201
|
+
if (compression !== -1 && input.indexOf("::", compression + 1) !== -1) return void 0;
|
|
3202
|
+
const left = compression === -1 ? input.split(":") : input.slice(0, compression).split(":");
|
|
3203
|
+
const right = compression === -1 ? [] : input.slice(compression + 2).split(":");
|
|
3204
|
+
if (compression !== -1) {
|
|
3205
|
+
if (left.length === 1 && left[0] === "") left.length = 0;
|
|
3206
|
+
if (right.length === 1 && right[0] === "") right.length = 0;
|
|
3207
|
+
}
|
|
3208
|
+
const parts = left.concat(right);
|
|
3209
|
+
let hextetCount = 0;
|
|
3210
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3211
|
+
const part = parts[i];
|
|
3212
|
+
if (part === "") return void 0;
|
|
3213
|
+
if (part.indexOf(".") !== -1) {
|
|
3214
|
+
if (i !== parts.length - 1 || compression !== -1 && right.length === 0 || !isIPv4(part)) return void 0;
|
|
3215
|
+
hextetCount += 2;
|
|
3196
3216
|
continue;
|
|
3197
3217
|
}
|
|
3218
|
+
if (!isHextet(part)) return void 0;
|
|
3219
|
+
parts[i] = parseInt(part, 16).toString(16);
|
|
3220
|
+
hextetCount++;
|
|
3198
3221
|
}
|
|
3199
|
-
if (
|
|
3200
|
-
if (
|
|
3201
|
-
|
|
3202
|
-
} else if (endIpv6) {
|
|
3203
|
-
address.push(buffer.join(""));
|
|
3204
|
-
} else {
|
|
3205
|
-
address.push(stringArrayToHexStripped(buffer));
|
|
3206
|
-
}
|
|
3222
|
+
if (compression === -1) {
|
|
3223
|
+
if (hextetCount !== 8) return void 0;
|
|
3224
|
+
return compressIPv6ZeroRun(parts);
|
|
3207
3225
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3226
|
+
if (hextetCount >= 8) return void 0;
|
|
3227
|
+
const expanded = parts.slice(0, left.length);
|
|
3228
|
+
for (let i = hextetCount; i < 8; i++) expanded.push("0");
|
|
3229
|
+
for (let i = left.length; i < parts.length; i++) expanded.push(parts[i]);
|
|
3230
|
+
return compressIPv6ZeroRun(expanded);
|
|
3210
3231
|
}
|
|
3211
3232
|
function normalizeIPv6(host) {
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
}
|
|
3215
|
-
|
|
3216
|
-
if (
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3233
|
+
const bracketed = host[0] === "[" && host[host.length - 1] === "]";
|
|
3234
|
+
const hasBracket = host[0] === "[" || host[host.length - 1] === "]";
|
|
3235
|
+
if (hasBracket && !bracketed) return { host, isIPV6: false, error: true };
|
|
3236
|
+
let input = bracketed ? host.slice(1, -1) : host;
|
|
3237
|
+
if (bracketed && isIPvFuture(input)) {
|
|
3238
|
+
input = input.toLowerCase();
|
|
3239
|
+
return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true };
|
|
3240
|
+
}
|
|
3241
|
+
if (findToken(input, ":") < 2) {
|
|
3242
|
+
return { host, isIPV6: false, error: bracketed };
|
|
3243
|
+
}
|
|
3244
|
+
let zoneIdentifier = "";
|
|
3245
|
+
const zoneSeparator = input.indexOf("%");
|
|
3246
|
+
if (zoneSeparator !== -1) {
|
|
3247
|
+
const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === "%25" ? 3 : 1;
|
|
3248
|
+
zoneIdentifier = input.slice(zoneSeparator + separatorLength);
|
|
3249
|
+
if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true };
|
|
3250
|
+
input = input.slice(0, zoneSeparator);
|
|
3251
|
+
}
|
|
3252
|
+
const address = normalizeIPv6Address(input);
|
|
3253
|
+
if (address === void 0) return { host, isIPV6: false, error: true };
|
|
3254
|
+
return {
|
|
3255
|
+
host: address + (zoneIdentifier ? "%" + zoneIdentifier : ""),
|
|
3256
|
+
escapedHost: address + (zoneIdentifier ? "%25" + zoneIdentifier : ""),
|
|
3257
|
+
isIPV6: true
|
|
3258
|
+
};
|
|
3227
3259
|
}
|
|
3228
3260
|
function findToken(str, token) {
|
|
3229
3261
|
let ind = 0;
|
|
@@ -3342,7 +3374,8 @@ var require_utils = __commonJS({
|
|
|
3342
3374
|
function normalizePathEncoding(input) {
|
|
3343
3375
|
let output = "";
|
|
3344
3376
|
for (let i = 0; i < input.length; i++) {
|
|
3345
|
-
|
|
3377
|
+
const ch = input[i];
|
|
3378
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3346
3379
|
const hex3 = input.slice(i + 1, i + 3);
|
|
3347
3380
|
if (isHexPair(hex3)) {
|
|
3348
3381
|
const normalizedHex = hex3.toUpperCase();
|
|
@@ -3356,10 +3389,152 @@ var require_utils = __commonJS({
|
|
|
3356
3389
|
continue;
|
|
3357
3390
|
}
|
|
3358
3391
|
}
|
|
3359
|
-
if (isPathCharacter(
|
|
3360
|
-
output +=
|
|
3392
|
+
if (isPathCharacter(ch)) {
|
|
3393
|
+
output += ch;
|
|
3361
3394
|
} else {
|
|
3362
|
-
|
|
3395
|
+
const code = input.charCodeAt(i);
|
|
3396
|
+
if (code < 128) {
|
|
3397
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
|
|
3398
|
+
} else if (code < 55296 || code > 57343) {
|
|
3399
|
+
output += percentEncodeNonAscii(code);
|
|
3400
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3401
|
+
const low = input.charCodeAt(i + 1);
|
|
3402
|
+
if (low >= 56320 && low <= 57343) {
|
|
3403
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3404
|
+
i++;
|
|
3405
|
+
} else {
|
|
3406
|
+
output += percentEncodeNonAscii(65533);
|
|
3407
|
+
}
|
|
3408
|
+
} else {
|
|
3409
|
+
output += percentEncodeNonAscii(65533);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return output;
|
|
3414
|
+
}
|
|
3415
|
+
function serializePathEncoding(input, pathNoScheme = false) {
|
|
3416
|
+
let output = "";
|
|
3417
|
+
let firstSegment = pathNoScheme && input[0] !== "/";
|
|
3418
|
+
for (let i = 0; i < input.length; i++) {
|
|
3419
|
+
const ch = input[i];
|
|
3420
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3421
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3422
|
+
if (isHexPair(hex3)) {
|
|
3423
|
+
output += "%" + hex3.toUpperCase();
|
|
3424
|
+
i += 2;
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (ch === "/") {
|
|
3429
|
+
firstSegment = false;
|
|
3430
|
+
}
|
|
3431
|
+
if (isPathCharacter(ch) && (ch !== ":" || !firstSegment)) {
|
|
3432
|
+
output += ch;
|
|
3433
|
+
} else {
|
|
3434
|
+
const code = input.charCodeAt(i);
|
|
3435
|
+
if (code < 128) {
|
|
3436
|
+
output += BYTE_HEX[code];
|
|
3437
|
+
} else if (code < 55296 || code > 57343) {
|
|
3438
|
+
output += percentEncodeNonAscii(code);
|
|
3439
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3440
|
+
const low = input.charCodeAt(i + 1);
|
|
3441
|
+
if (low >= 56320 && low <= 57343) {
|
|
3442
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3443
|
+
i++;
|
|
3444
|
+
} else {
|
|
3445
|
+
output += percentEncodeNonAscii(65533);
|
|
3446
|
+
}
|
|
3447
|
+
} else {
|
|
3448
|
+
output += percentEncodeNonAscii(65533);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
return output;
|
|
3453
|
+
}
|
|
3454
|
+
function encodeComponent(input, isAllowed) {
|
|
3455
|
+
let output = "";
|
|
3456
|
+
for (let i = 0; i < input.length; i++) {
|
|
3457
|
+
const ch = input[i];
|
|
3458
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3459
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3460
|
+
if (isHexPair(hex3)) {
|
|
3461
|
+
output += "%" + hex3.toUpperCase();
|
|
3462
|
+
i += 2;
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
if (isAllowed(ch)) {
|
|
3467
|
+
output += ch;
|
|
3468
|
+
} else {
|
|
3469
|
+
const code = input.charCodeAt(i);
|
|
3470
|
+
if (code < 128) {
|
|
3471
|
+
output += BYTE_HEX[code];
|
|
3472
|
+
} else if (code < 55296 || code > 57343) {
|
|
3473
|
+
output += percentEncodeNonAscii(code);
|
|
3474
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3475
|
+
const low = input.charCodeAt(i + 1);
|
|
3476
|
+
if (low >= 56320 && low <= 57343) {
|
|
3477
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3478
|
+
i++;
|
|
3479
|
+
} else {
|
|
3480
|
+
output += percentEncodeNonAscii(65533);
|
|
3481
|
+
}
|
|
3482
|
+
} else {
|
|
3483
|
+
output += percentEncodeNonAscii(65533);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return output;
|
|
3488
|
+
}
|
|
3489
|
+
function encodeUserinfo(input) {
|
|
3490
|
+
return encodeComponent(input, isUserinfoCharacter);
|
|
3491
|
+
}
|
|
3492
|
+
function encodeQuery(input) {
|
|
3493
|
+
return encodeComponent(input, isQueryFragmentCharacter);
|
|
3494
|
+
}
|
|
3495
|
+
function encodeFragment(input) {
|
|
3496
|
+
return encodeComponent(input, isQueryFragmentCharacter);
|
|
3497
|
+
}
|
|
3498
|
+
function isEscapeSafe(cp) {
|
|
3499
|
+
return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95;
|
|
3500
|
+
}
|
|
3501
|
+
function normalizeQueryFragmentEncoding(input) {
|
|
3502
|
+
let output = "";
|
|
3503
|
+
for (let i = 0; i < input.length; i++) {
|
|
3504
|
+
const ch = input[i];
|
|
3505
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3506
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3507
|
+
if (isHexPair(hex3)) {
|
|
3508
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3509
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3510
|
+
if (isUnreserved(decoded)) {
|
|
3511
|
+
output += decoded;
|
|
3512
|
+
} else {
|
|
3513
|
+
output += "%" + normalizedHex;
|
|
3514
|
+
}
|
|
3515
|
+
i += 2;
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
if (isQueryFragmentCharacter(ch)) {
|
|
3520
|
+
output += ch;
|
|
3521
|
+
} else {
|
|
3522
|
+
const code = input.charCodeAt(i);
|
|
3523
|
+
if (code < 128) {
|
|
3524
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
|
|
3525
|
+
} else if (code < 55296 || code > 57343) {
|
|
3526
|
+
output += percentEncodeNonAscii(code);
|
|
3527
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3528
|
+
const low = input.charCodeAt(i + 1);
|
|
3529
|
+
if (low >= 56320 && low <= 57343) {
|
|
3530
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3531
|
+
i++;
|
|
3532
|
+
} else {
|
|
3533
|
+
output += percentEncodeNonAscii(65533);
|
|
3534
|
+
}
|
|
3535
|
+
} else {
|
|
3536
|
+
output += percentEncodeNonAscii(65533);
|
|
3537
|
+
}
|
|
3363
3538
|
}
|
|
3364
3539
|
}
|
|
3365
3540
|
return output;
|
|
@@ -3382,14 +3557,18 @@ var require_utils = __commonJS({
|
|
|
3382
3557
|
function recomposeAuthority(component) {
|
|
3383
3558
|
const uriTokens = [];
|
|
3384
3559
|
if (component.userinfo !== void 0) {
|
|
3385
|
-
uriTokens.push(component.userinfo);
|
|
3560
|
+
uriTokens.push(encodeUserinfo(component.userinfo));
|
|
3386
3561
|
uriTokens.push("@");
|
|
3387
3562
|
}
|
|
3388
3563
|
if (component.host !== void 0) {
|
|
3389
|
-
let host =
|
|
3564
|
+
let host = component.host;
|
|
3390
3565
|
if (!isIPv4(host)) {
|
|
3391
|
-
|
|
3392
|
-
if (ipV6res.isIPV6
|
|
3566
|
+
let ipV6res = normalizeIPv6(host);
|
|
3567
|
+
if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
|
|
3568
|
+
host = normalizePercentEncoding(host, true);
|
|
3569
|
+
ipV6res = normalizeIPv6(host);
|
|
3570
|
+
}
|
|
3571
|
+
if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
|
|
3393
3572
|
host = `[${ipV6res.escapedHost}]`;
|
|
3394
3573
|
} else {
|
|
3395
3574
|
host = reescapeHostDelimiters(host, false);
|
|
@@ -3409,6 +3588,11 @@ var require_utils = __commonJS({
|
|
|
3409
3588
|
reescapeHostDelimiters,
|
|
3410
3589
|
normalizePercentEncoding,
|
|
3411
3590
|
normalizePathEncoding,
|
|
3591
|
+
serializePathEncoding,
|
|
3592
|
+
normalizeQueryFragmentEncoding,
|
|
3593
|
+
encodeUserinfo,
|
|
3594
|
+
encodeQuery,
|
|
3595
|
+
encodeFragment,
|
|
3412
3596
|
escapePreservingEscapes,
|
|
3413
3597
|
removeDotSegments,
|
|
3414
3598
|
isIPv4,
|
|
@@ -3424,7 +3608,7 @@ var require_schemes = __commonJS({
|
|
|
3424
3608
|
"node_modules/fast-uri/lib/schemes.js"(exports, module) {
|
|
3425
3609
|
"use strict";
|
|
3426
3610
|
var { isUUID } = require_utils();
|
|
3427
|
-
var URN_REG =
|
|
3611
|
+
var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
|
|
3428
3612
|
var supportedSchemeNames = (
|
|
3429
3613
|
/** @type {const} */
|
|
3430
3614
|
[
|
|
@@ -3485,9 +3669,10 @@ var require_schemes = __commonJS({
|
|
|
3485
3669
|
wsComponent.secure = void 0;
|
|
3486
3670
|
}
|
|
3487
3671
|
if (wsComponent.resourceName) {
|
|
3488
|
-
const
|
|
3672
|
+
const queryIndex = wsComponent.resourceName.indexOf("?");
|
|
3673
|
+
const path = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
|
|
3489
3674
|
wsComponent.path = path && path !== "/" ? path : void 0;
|
|
3490
|
-
wsComponent.query =
|
|
3675
|
+
wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
|
|
3491
3676
|
wsComponent.resourceName = void 0;
|
|
3492
3677
|
}
|
|
3493
3678
|
wsComponent.fragment = void 0;
|
|
@@ -3499,7 +3684,7 @@ var require_schemes = __commonJS({
|
|
|
3499
3684
|
return urnComponent;
|
|
3500
3685
|
}
|
|
3501
3686
|
const matches = urnComponent.path.match(URN_REG);
|
|
3502
|
-
if (matches) {
|
|
3687
|
+
if (matches && matches[0] === urnComponent.path) {
|
|
3503
3688
|
const scheme = options.scheme || urnComponent.scheme || "urn";
|
|
3504
3689
|
urnComponent.nid = matches[1].toLowerCase();
|
|
3505
3690
|
urnComponent.nss = matches[2];
|
|
@@ -3633,8 +3818,17 @@ var require_schemes = __commonJS({
|
|
|
3633
3818
|
var require_fast_uri = __commonJS({
|
|
3634
3819
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3635
3820
|
"use strict";
|
|
3636
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding,
|
|
3821
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3637
3822
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3823
|
+
var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
|
|
3824
|
+
var MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
|
|
3825
|
+
function decodeValidScheme(scheme) {
|
|
3826
|
+
const decodedScheme = unescape(String(scheme));
|
|
3827
|
+
if (!VALID_SCHEME.test(decodedScheme)) {
|
|
3828
|
+
throw new TypeError(MALFORMED_SCHEME_ERROR);
|
|
3829
|
+
}
|
|
3830
|
+
return decodedScheme;
|
|
3831
|
+
}
|
|
3638
3832
|
function normalize(uri, options) {
|
|
3639
3833
|
if (typeof uri === "string") {
|
|
3640
3834
|
uri = /** @type {T} */
|
|
@@ -3647,7 +3841,34 @@ var require_fast_uri = __commonJS({
|
|
|
3647
3841
|
}
|
|
3648
3842
|
function resolve(baseURI, relativeURI, options) {
|
|
3649
3843
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3650
|
-
const
|
|
3844
|
+
const {
|
|
3845
|
+
parsed: baseParsed,
|
|
3846
|
+
malformedAuthorityOrPort: baseMalformed,
|
|
3847
|
+
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
3848
|
+
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
3849
|
+
malformedHost: baseMalformedHost,
|
|
3850
|
+
malformedScheme: baseMalformedScheme
|
|
3851
|
+
} = parseWithStatus(baseURI, schemelessOptions);
|
|
3852
|
+
const {
|
|
3853
|
+
parsed: relativeParsed,
|
|
3854
|
+
malformedAuthorityOrPort: relativeMalformed,
|
|
3855
|
+
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
3856
|
+
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
3857
|
+
malformedHost: relativeMalformedHost,
|
|
3858
|
+
malformedScheme: relativeMalformedScheme
|
|
3859
|
+
} = parseWithStatus(relativeURI, schemelessOptions);
|
|
3860
|
+
if (baseMalformed || relativeMalformed || baseMalformedPercentEncoding || relativeMalformedPercentEncoding || baseMalformedSchemeSpecific || relativeMalformedSchemeSpecific || baseMalformedHost || relativeMalformedHost || baseMalformedScheme || relativeMalformedScheme) {
|
|
3861
|
+
throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
|
|
3862
|
+
}
|
|
3863
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3864
|
+
const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
|
|
3865
|
+
const resolvedHost = resolved.host;
|
|
3866
|
+
const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
|
|
3867
|
+
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
|
|
3868
|
+
const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost);
|
|
3869
|
+
if (resolved.error && !encodedASCIIHost) {
|
|
3870
|
+
throw new Error(resolved.error);
|
|
3871
|
+
}
|
|
3651
3872
|
schemelessOptions.skipEscape = true;
|
|
3652
3873
|
return serialize(resolved, schemelessOptions);
|
|
3653
3874
|
}
|
|
@@ -3707,7 +3928,7 @@ var require_fast_uri = __commonJS({
|
|
|
3707
3928
|
function equal(uriA, uriB, options) {
|
|
3708
3929
|
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3709
3930
|
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3710
|
-
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA
|
|
3931
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA === normalizedB;
|
|
3711
3932
|
}
|
|
3712
3933
|
function serialize(cmpts, opts) {
|
|
3713
3934
|
const component = {
|
|
@@ -3728,19 +3949,22 @@ var require_fast_uri = __commonJS({
|
|
|
3728
3949
|
};
|
|
3729
3950
|
const options = Object.assign({}, opts);
|
|
3730
3951
|
const uriTokens = [];
|
|
3952
|
+
if (component.scheme) {
|
|
3953
|
+
component.scheme = decodeValidScheme(component.scheme);
|
|
3954
|
+
}
|
|
3731
3955
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
|
|
3732
3956
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3957
|
+
const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
|
|
3958
|
+
const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
|
|
3733
3959
|
if (component.path !== void 0) {
|
|
3734
3960
|
if (!options.skipEscape) {
|
|
3735
|
-
component.path =
|
|
3736
|
-
if (component.scheme !== void 0) {
|
|
3737
|
-
component.path = component.path.split("%3A").join(":");
|
|
3738
|
-
}
|
|
3961
|
+
component.path = serializePathEncoding(component.path, pathNoScheme);
|
|
3739
3962
|
} else {
|
|
3740
3963
|
component.path = normalizePercentEncoding(component.path);
|
|
3741
3964
|
}
|
|
3742
3965
|
}
|
|
3743
3966
|
if (options.reference !== "suffix" && component.scheme) {
|
|
3967
|
+
component.scheme = decodeValidScheme(component.scheme);
|
|
3744
3968
|
uriTokens.push(component.scheme, ":");
|
|
3745
3969
|
}
|
|
3746
3970
|
const authority = recomposeAuthority(component);
|
|
@@ -3758,20 +3982,25 @@ var require_fast_uri = __commonJS({
|
|
|
3758
3982
|
if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
|
|
3759
3983
|
s = removeDotSegments(s);
|
|
3760
3984
|
}
|
|
3985
|
+
if (pathNoScheme) {
|
|
3986
|
+
s = serializePathEncoding(s, true);
|
|
3987
|
+
}
|
|
3761
3988
|
if (authority === void 0 && s[0] === "/" && s[1] === "/") {
|
|
3762
3989
|
s = "/%2F" + s.slice(2);
|
|
3763
3990
|
}
|
|
3764
3991
|
uriTokens.push(s);
|
|
3765
3992
|
}
|
|
3766
3993
|
if (component.query !== void 0) {
|
|
3767
|
-
uriTokens.push("?", component.query);
|
|
3994
|
+
uriTokens.push("?", encodeQuery(component.query));
|
|
3768
3995
|
}
|
|
3769
3996
|
if (component.fragment !== void 0) {
|
|
3770
|
-
uriTokens.push("#", component.fragment);
|
|
3997
|
+
uriTokens.push("#", encodeFragment(component.fragment));
|
|
3771
3998
|
}
|
|
3772
3999
|
return uriTokens.join("");
|
|
3773
4000
|
}
|
|
3774
4001
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
4002
|
+
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
4003
|
+
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
3775
4004
|
function getParseError(parsed, matches) {
|
|
3776
4005
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3777
4006
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3781,6 +4010,32 @@ var require_fast_uri = __commonJS({
|
|
|
3781
4010
|
}
|
|
3782
4011
|
return void 0;
|
|
3783
4012
|
}
|
|
4013
|
+
function hasMalformedPercentEncoding(component) {
|
|
4014
|
+
if (component === void 0) return false;
|
|
4015
|
+
let percent = component.indexOf("%");
|
|
4016
|
+
while (percent !== -1) {
|
|
4017
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
4018
|
+
return true;
|
|
4019
|
+
}
|
|
4020
|
+
percent = component.indexOf("%", percent + 3);
|
|
4021
|
+
}
|
|
4022
|
+
return false;
|
|
4023
|
+
}
|
|
4024
|
+
function hasMalformedComponentPercentEncoding(matches) {
|
|
4025
|
+
const host = matches[4];
|
|
4026
|
+
return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !(host[0] === "[" && host[host.length - 1] === "]") && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
|
|
4027
|
+
}
|
|
4028
|
+
function canonicalizeHost(parsed, options, schemeHandler, isIP) {
|
|
4029
|
+
if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && parsed.host[0] !== "[" && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
4030
|
+
try {
|
|
4031
|
+
parsed.host = new URL("http://" + parsed.host).hostname;
|
|
4032
|
+
} catch (e) {
|
|
4033
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
|
|
4034
|
+
return true;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return false;
|
|
4038
|
+
}
|
|
3784
4039
|
function parseWithStatus(uri, opts) {
|
|
3785
4040
|
const options = Object.assign({}, opts);
|
|
3786
4041
|
const parsed = {
|
|
@@ -3793,6 +4048,11 @@ var require_fast_uri = __commonJS({
|
|
|
3793
4048
|
fragment: void 0
|
|
3794
4049
|
};
|
|
3795
4050
|
let malformedAuthorityOrPort = false;
|
|
4051
|
+
let malformedPercentEncoding = false;
|
|
4052
|
+
let malformedSchemeSpecific = false;
|
|
4053
|
+
let malformedHost = false;
|
|
4054
|
+
let malformedIPLiteral = false;
|
|
4055
|
+
let malformedScheme = false;
|
|
3796
4056
|
let isIP = false;
|
|
3797
4057
|
if (options.reference === "suffix") {
|
|
3798
4058
|
if (options.scheme) {
|
|
@@ -3801,6 +4061,25 @@ var require_fast_uri = __commonJS({
|
|
|
3801
4061
|
uri = "//" + uri;
|
|
3802
4062
|
}
|
|
3803
4063
|
}
|
|
4064
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX);
|
|
4065
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
|
|
4066
|
+
parsed.error = "URI authority must not contain a literal backslash.";
|
|
4067
|
+
malformedAuthorityOrPort = true;
|
|
4068
|
+
}
|
|
4069
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
|
|
4070
|
+
if (introducerMatch !== null) {
|
|
4071
|
+
const region = introducerMatch[1];
|
|
4072
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, "");
|
|
4073
|
+
if (normalizedRegion.length >= 2) {
|
|
4074
|
+
if (normalizedRegion.slice(0, 2) !== "//") {
|
|
4075
|
+
parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
|
|
4076
|
+
malformedAuthorityOrPort = true;
|
|
4077
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
4078
|
+
parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
|
|
4079
|
+
malformedAuthorityOrPort = true;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
3804
4083
|
const matches = uri.match(URI_PARSE);
|
|
3805
4084
|
if (matches) {
|
|
3806
4085
|
parsed.scheme = matches[1];
|
|
@@ -3810,6 +4089,19 @@ var require_fast_uri = __commonJS({
|
|
|
3810
4089
|
parsed.path = matches[6] || "";
|
|
3811
4090
|
parsed.query = matches[7];
|
|
3812
4091
|
parsed.fragment = matches[8];
|
|
4092
|
+
if (parsed.scheme !== void 0) {
|
|
4093
|
+
const decodedScheme = unescape(parsed.scheme);
|
|
4094
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
4095
|
+
parsed.scheme = decodedScheme.toLowerCase();
|
|
4096
|
+
} else {
|
|
4097
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
|
|
4098
|
+
malformedScheme = true;
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches);
|
|
4102
|
+
if (malformedPercentEncoding) {
|
|
4103
|
+
parsed.error = parsed.error || "URI contains malformed percent-encoding.";
|
|
4104
|
+
}
|
|
3813
4105
|
if (isNaN(parsed.port)) {
|
|
3814
4106
|
parsed.port = matches[5];
|
|
3815
4107
|
}
|
|
@@ -3821,9 +4113,15 @@ var require_fast_uri = __commonJS({
|
|
|
3821
4113
|
if (parsed.host) {
|
|
3822
4114
|
const ipv4result = isIPv4(parsed.host);
|
|
3823
4115
|
if (ipv4result === false) {
|
|
4116
|
+
const bracketedIPLiteral = parsed.host[0] === "[" && parsed.host[parsed.host.length - 1] === "]";
|
|
3824
4117
|
const ipv6result = normalizeIPv6(parsed.host);
|
|
3825
|
-
|
|
3826
|
-
|
|
4118
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
|
|
4119
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true;
|
|
4120
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
|
|
4121
|
+
if (malformedIPLiteral) {
|
|
4122
|
+
parsed.error = parsed.error || "URI host is malformed.";
|
|
4123
|
+
malformedAuthorityOrPort = true;
|
|
4124
|
+
}
|
|
3827
4125
|
} else {
|
|
3828
4126
|
isIP = true;
|
|
3829
4127
|
}
|
|
@@ -3841,42 +4139,34 @@ var require_fast_uri = __commonJS({
|
|
|
3841
4139
|
parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
|
|
3842
4140
|
}
|
|
3843
4141
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
|
|
3844
|
-
|
|
3845
|
-
if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
3846
|
-
try {
|
|
3847
|
-
parsed.host = URL.domainToASCII(parsed.host.toLowerCase());
|
|
3848
|
-
} catch (e) {
|
|
3849
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
4142
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
|
|
3853
4143
|
if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
|
|
3854
4144
|
if (uri.indexOf("%") !== -1) {
|
|
3855
|
-
if (parsed.
|
|
3856
|
-
parsed.
|
|
3857
|
-
|
|
3858
|
-
if (parsed.host !== void 0) {
|
|
3859
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
4145
|
+
if (parsed.host !== void 0 && !malformedIPLiteral) {
|
|
4146
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
|
|
4147
|
+
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
3860
4148
|
}
|
|
3861
4149
|
}
|
|
3862
4150
|
if (parsed.path) {
|
|
3863
4151
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
3864
4152
|
}
|
|
4153
|
+
if (parsed.query) {
|
|
4154
|
+
parsed.query = normalizeQueryFragmentEncoding(parsed.query);
|
|
4155
|
+
}
|
|
3865
4156
|
if (parsed.fragment) {
|
|
3866
|
-
|
|
3867
|
-
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3868
|
-
} catch {
|
|
3869
|
-
parsed.error = parsed.error || "URI malformed";
|
|
3870
|
-
}
|
|
4157
|
+
parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
|
|
3871
4158
|
}
|
|
3872
4159
|
}
|
|
3873
4160
|
if (schemeHandler && schemeHandler.parse) {
|
|
3874
4161
|
schemeHandler.parse(parsed, options);
|
|
4162
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === void 0) {
|
|
4163
|
+
malformedSchemeSpecific = true;
|
|
4164
|
+
}
|
|
3875
4165
|
}
|
|
3876
4166
|
} else {
|
|
3877
4167
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3878
4168
|
}
|
|
3879
|
-
return { parsed, malformedAuthorityOrPort };
|
|
4169
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
|
|
3880
4170
|
}
|
|
3881
4171
|
function parse3(uri, opts) {
|
|
3882
4172
|
return parseWithStatus(uri, opts).parsed;
|
|
@@ -3885,20 +4175,28 @@ var require_fast_uri = __commonJS({
|
|
|
3885
4175
|
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3886
4176
|
}
|
|
3887
4177
|
function normalizeStringWithStatus(uri, opts) {
|
|
3888
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
4178
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts);
|
|
3889
4179
|
return {
|
|
3890
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3891
|
-
malformedAuthorityOrPort
|
|
4180
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
4181
|
+
malformedAuthorityOrPort,
|
|
4182
|
+
malformedPercentEncoding,
|
|
4183
|
+
malformedSchemeSpecific,
|
|
4184
|
+
malformedHost,
|
|
4185
|
+
malformedScheme
|
|
3892
4186
|
};
|
|
3893
4187
|
}
|
|
3894
4188
|
function normalizeComparableURI(uri, opts) {
|
|
3895
|
-
if (typeof uri
|
|
3896
|
-
|
|
3897
|
-
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
4189
|
+
if (typeof uri !== "string" && typeof uri !== "object") {
|
|
4190
|
+
return void 0;
|
|
3898
4191
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
4192
|
+
let value;
|
|
4193
|
+
try {
|
|
4194
|
+
value = typeof uri === "string" ? uri : serialize(uri, opts);
|
|
4195
|
+
} catch {
|
|
4196
|
+
return void 0;
|
|
3901
4197
|
}
|
|
4198
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
|
|
4199
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized;
|
|
3902
4200
|
}
|
|
3903
4201
|
var fastUri = {
|
|
3904
4202
|
SCHEMES,
|
|
@@ -7862,6 +8160,8 @@ var require_defaults2 = __commonJS({
|
|
|
7862
8160
|
idleTimeoutMillis: 3e4,
|
|
7863
8161
|
client_encoding: "",
|
|
7864
8162
|
ssl: false,
|
|
8163
|
+
// SSL negotiation style: 'postgres' (traditional SSLRequest) or 'direct'
|
|
8164
|
+
sslnegotiation: void 0,
|
|
7865
8165
|
application_name: void 0,
|
|
7866
8166
|
fallback_application_name: void 0,
|
|
7867
8167
|
options: void 0,
|
|
@@ -7896,8 +8196,7 @@ var require_utils2 = __commonJS({
|
|
|
7896
8196
|
"node_modules/pg/lib/utils.js"(exports, module) {
|
|
7897
8197
|
"use strict";
|
|
7898
8198
|
var defaults2 = require_defaults2();
|
|
7899
|
-
var
|
|
7900
|
-
var { isDate } = util2.types || util2;
|
|
8199
|
+
var { isDate } = __require("util/types");
|
|
7901
8200
|
function escapeElement(elementRepresentation) {
|
|
7902
8201
|
const escaped = elementRepresentation.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
7903
8202
|
return '"' + escaped + '"';
|
|
@@ -7906,28 +8205,23 @@ var require_utils2 = __commonJS({
|
|
|
7906
8205
|
let result = "{";
|
|
7907
8206
|
for (let i = 0; i < val.length; i++) {
|
|
7908
8207
|
if (i > 0) {
|
|
7909
|
-
result
|
|
7910
|
-
}
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
8208
|
+
result += ",";
|
|
8209
|
+
}
|
|
8210
|
+
let item = val[i];
|
|
8211
|
+
if (item == null) {
|
|
8212
|
+
result += "NULL";
|
|
8213
|
+
} else if (Array.isArray(item)) {
|
|
8214
|
+
result += arrayString(item);
|
|
8215
|
+
} else if (ArrayBuffer.isView(item)) {
|
|
7917
8216
|
if (!(item instanceof Buffer)) {
|
|
7918
|
-
|
|
7919
|
-
if (buf.length === item.byteLength) {
|
|
7920
|
-
item = buf;
|
|
7921
|
-
} else {
|
|
7922
|
-
item = buf.slice(item.byteOffset, item.byteOffset + item.byteLength);
|
|
7923
|
-
}
|
|
8217
|
+
item = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
|
|
7924
8218
|
}
|
|
7925
8219
|
result += "\\\\x" + item.toString("hex");
|
|
7926
8220
|
} else {
|
|
7927
|
-
result += escapeElement(prepareValue(
|
|
8221
|
+
result += escapeElement(prepareValue(item));
|
|
7928
8222
|
}
|
|
7929
8223
|
}
|
|
7930
|
-
result
|
|
8224
|
+
result += "}";
|
|
7931
8225
|
return result;
|
|
7932
8226
|
}
|
|
7933
8227
|
var prepareValue = function(val, seen) {
|
|
@@ -7939,11 +8233,7 @@ var require_utils2 = __commonJS({
|
|
|
7939
8233
|
return val;
|
|
7940
8234
|
}
|
|
7941
8235
|
if (ArrayBuffer.isView(val)) {
|
|
7942
|
-
|
|
7943
|
-
if (buf.length === val.byteLength) {
|
|
7944
|
-
return buf;
|
|
7945
|
-
}
|
|
7946
|
-
return buf.slice(val.byteOffset, val.byteOffset + val.byteLength);
|
|
8236
|
+
return Buffer.from(val.buffer, val.byteOffset, val.byteLength);
|
|
7947
8237
|
}
|
|
7948
8238
|
if (isDate(val)) {
|
|
7949
8239
|
if (defaults2.parseInputDatesAsUTC) {
|
|
@@ -8049,47 +8339,9 @@ var require_utils2 = __commonJS({
|
|
|
8049
8339
|
}
|
|
8050
8340
|
});
|
|
8051
8341
|
|
|
8052
|
-
// node_modules/pg/lib/crypto/utils
|
|
8053
|
-
var
|
|
8054
|
-
"node_modules/pg/lib/crypto/utils
|
|
8055
|
-
"use strict";
|
|
8056
|
-
var nodeCrypto = __require("crypto");
|
|
8057
|
-
function md5(string4) {
|
|
8058
|
-
return nodeCrypto.createHash("md5").update(string4, "utf-8").digest("hex");
|
|
8059
|
-
}
|
|
8060
|
-
function postgresMd5PasswordHash(user, password, salt) {
|
|
8061
|
-
const inner = md5(password + user);
|
|
8062
|
-
const outer = md5(Buffer.concat([Buffer.from(inner), salt]));
|
|
8063
|
-
return "md5" + outer;
|
|
8064
|
-
}
|
|
8065
|
-
function sha256(text) {
|
|
8066
|
-
return nodeCrypto.createHash("sha256").update(text).digest();
|
|
8067
|
-
}
|
|
8068
|
-
function hashByName(hashName, text) {
|
|
8069
|
-
hashName = hashName.replace(/(\D)-/, "$1");
|
|
8070
|
-
return nodeCrypto.createHash(hashName).update(text).digest();
|
|
8071
|
-
}
|
|
8072
|
-
function hmacSha256(key, msg) {
|
|
8073
|
-
return nodeCrypto.createHmac("sha256", key).update(msg).digest();
|
|
8074
|
-
}
|
|
8075
|
-
async function deriveKey(password, salt, iterations) {
|
|
8076
|
-
return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, "sha256");
|
|
8077
|
-
}
|
|
8078
|
-
module.exports = {
|
|
8079
|
-
postgresMd5PasswordHash,
|
|
8080
|
-
randomBytes: nodeCrypto.randomBytes,
|
|
8081
|
-
deriveKey,
|
|
8082
|
-
sha256,
|
|
8083
|
-
hashByName,
|
|
8084
|
-
hmacSha256,
|
|
8085
|
-
md5
|
|
8086
|
-
};
|
|
8087
|
-
}
|
|
8088
|
-
});
|
|
8089
|
-
|
|
8090
|
-
// node_modules/pg/lib/crypto/utils-webcrypto.js
|
|
8091
|
-
var require_utils_webcrypto = __commonJS({
|
|
8092
|
-
"node_modules/pg/lib/crypto/utils-webcrypto.js"(exports, module) {
|
|
8342
|
+
// node_modules/pg/lib/crypto/utils.js
|
|
8343
|
+
var require_utils3 = __commonJS({
|
|
8344
|
+
"node_modules/pg/lib/crypto/utils.js"(exports, module) {
|
|
8093
8345
|
var nodeCrypto = __require("crypto");
|
|
8094
8346
|
module.exports = {
|
|
8095
8347
|
postgresMd5PasswordHash,
|
|
@@ -8138,19 +8390,6 @@ var require_utils_webcrypto = __commonJS({
|
|
|
8138
8390
|
}
|
|
8139
8391
|
});
|
|
8140
8392
|
|
|
8141
|
-
// node_modules/pg/lib/crypto/utils.js
|
|
8142
|
-
var require_utils3 = __commonJS({
|
|
8143
|
-
"node_modules/pg/lib/crypto/utils.js"(exports, module) {
|
|
8144
|
-
"use strict";
|
|
8145
|
-
var useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split(".")[0]) < 15;
|
|
8146
|
-
if (useLegacyCrypto) {
|
|
8147
|
-
module.exports = require_utils_legacy();
|
|
8148
|
-
} else {
|
|
8149
|
-
module.exports = require_utils_webcrypto();
|
|
8150
|
-
}
|
|
8151
|
-
}
|
|
8152
|
-
});
|
|
8153
|
-
|
|
8154
8393
|
// node_modules/pg/lib/crypto/cert-signatures.js
|
|
8155
8394
|
var require_cert_signatures = __commonJS({
|
|
8156
8395
|
"node_modules/pg/lib/crypto/cert-signatures.js"(exports, module) {
|
|
@@ -8270,7 +8509,13 @@ var require_sasl = __commonJS({
|
|
|
8270
8509
|
"use strict";
|
|
8271
8510
|
var crypto = require_utils3();
|
|
8272
8511
|
var { signatureAlgorithmHashFromCertificate } = require_cert_signatures();
|
|
8273
|
-
function
|
|
8512
|
+
function saslprep(password) {
|
|
8513
|
+
const nonAsciiSpace = /[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/g;
|
|
8514
|
+
const mappedToNothing = /[\u00AD\u034F\u1806\u180B\u180C\u180D\u200C\u200D\u2060\uFE00-\uFE0F\uFEFF]/g;
|
|
8515
|
+
return password.replace(nonAsciiSpace, " ").replace(mappedToNothing, "").normalize("NFKC");
|
|
8516
|
+
}
|
|
8517
|
+
var DEFAULT_MAX_SCRAM_ITERATIONS = 1e5;
|
|
8518
|
+
function startSession(mechanisms, stream, scramMaxIterations = DEFAULT_MAX_SCRAM_ITERATIONS) {
|
|
8274
8519
|
const candidates = ["SCRAM-SHA-256"];
|
|
8275
8520
|
if (stream) candidates.unshift("SCRAM-SHA-256-PLUS");
|
|
8276
8521
|
const mechanism = candidates.find((candidate) => mechanisms.includes(candidate));
|
|
@@ -8286,7 +8531,8 @@ var require_sasl = __commonJS({
|
|
|
8286
8531
|
mechanism,
|
|
8287
8532
|
clientNonce,
|
|
8288
8533
|
response: gs2Header + ",,n=*,r=" + clientNonce,
|
|
8289
|
-
message: "SASLInitialResponse"
|
|
8534
|
+
message: "SASLInitialResponse",
|
|
8535
|
+
scramMaxIterations
|
|
8290
8536
|
};
|
|
8291
8537
|
}
|
|
8292
8538
|
async function continueSession(session, password, serverData, stream) {
|
|
@@ -8308,6 +8554,12 @@ var require_sasl = __commonJS({
|
|
|
8308
8554
|
} else if (sv.nonce.length === session.clientNonce.length) {
|
|
8309
8555
|
throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short");
|
|
8310
8556
|
}
|
|
8557
|
+
const scramMaxIterations = typeof session.scramMaxIterations === "number" ? session.scramMaxIterations : DEFAULT_MAX_SCRAM_ITERATIONS;
|
|
8558
|
+
if (scramMaxIterations !== 0 && sv.iteration > scramMaxIterations) {
|
|
8559
|
+
throw new Error(
|
|
8560
|
+
"SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count " + sv.iteration + " exceeds scramMaxIterations of " + scramMaxIterations
|
|
8561
|
+
);
|
|
8562
|
+
}
|
|
8311
8563
|
const clientFirstMessageBare = "n=*,r=" + session.clientNonce;
|
|
8312
8564
|
const serverFirstMessage = "r=" + sv.nonce + ",s=" + sv.salt + ",i=" + sv.iteration;
|
|
8313
8565
|
let channelBinding = stream ? "eSws" : "biws";
|
|
@@ -8322,7 +8574,7 @@ var require_sasl = __commonJS({
|
|
|
8322
8574
|
const clientFinalMessageWithoutProof = "c=" + channelBinding + ",r=" + sv.nonce;
|
|
8323
8575
|
const authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
|
|
8324
8576
|
const saltBytes = Buffer.from(sv.salt, "base64");
|
|
8325
|
-
const saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration);
|
|
8577
|
+
const saltedPassword = await crypto.deriveKey(saslprep(password), saltBytes, sv.iteration);
|
|
8326
8578
|
const clientKey = await crypto.hmacSha256(saltedPassword, "Client Key");
|
|
8327
8579
|
const storedKey = await crypto.sha256(clientKey);
|
|
8328
8580
|
const clientSignature = await crypto.hmacSha256(storedKey, authMessage);
|
|
@@ -8398,7 +8650,11 @@ var require_sasl = __commonJS({
|
|
|
8398
8650
|
}
|
|
8399
8651
|
function parseServerFinalMessage(serverData) {
|
|
8400
8652
|
const attrPairs = parseAttributePairs(serverData);
|
|
8653
|
+
const error51 = attrPairs.get("e");
|
|
8401
8654
|
const serverSignature = attrPairs.get("v");
|
|
8655
|
+
if (error51) {
|
|
8656
|
+
throw new Error(`SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${error51}"`);
|
|
8657
|
+
}
|
|
8402
8658
|
if (!serverSignature) {
|
|
8403
8659
|
throw new Error("SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing");
|
|
8404
8660
|
} else if (!isBase64(serverSignature)) {
|
|
@@ -8426,7 +8682,8 @@ var require_sasl = __commonJS({
|
|
|
8426
8682
|
module.exports = {
|
|
8427
8683
|
startSession,
|
|
8428
8684
|
continueSession,
|
|
8429
|
-
finalizeSession
|
|
8685
|
+
finalizeSession,
|
|
8686
|
+
DEFAULT_MAX_SCRAM_ITERATIONS
|
|
8430
8687
|
};
|
|
8431
8688
|
}
|
|
8432
8689
|
});
|
|
@@ -8728,6 +8985,15 @@ var require_connection_parameters = __commonJS({
|
|
|
8728
8985
|
enumerable: false
|
|
8729
8986
|
});
|
|
8730
8987
|
}
|
|
8988
|
+
this.sslnegotiation = val("sslnegotiation", config2, "PGSSLNEGOTIATION");
|
|
8989
|
+
if (this.sslnegotiation !== void 0 && this.sslnegotiation !== "postgres" && this.sslnegotiation !== "direct") {
|
|
8990
|
+
throw new Error(
|
|
8991
|
+
`Invalid sslnegotiation value: "${this.sslnegotiation}". Valid values are "postgres" and "direct".`
|
|
8992
|
+
);
|
|
8993
|
+
}
|
|
8994
|
+
if (this.sslnegotiation === "direct" && !this.ssl) {
|
|
8995
|
+
throw new Error("sslnegotiation=direct requires SSL to be enabled");
|
|
8996
|
+
}
|
|
8731
8997
|
this.client_encoding = val("client_encoding", config2);
|
|
8732
8998
|
this.replication = val("replication", config2);
|
|
8733
8999
|
this.isDomainSocket = !(this.host || "").indexOf("/");
|
|
@@ -8766,6 +9032,7 @@ var require_connection_parameters = __commonJS({
|
|
|
8766
9032
|
add(params, ssl, "sslkey");
|
|
8767
9033
|
add(params, ssl, "sslcert");
|
|
8768
9034
|
add(params, ssl, "sslrootcert");
|
|
9035
|
+
add(params, this, "sslnegotiation");
|
|
8769
9036
|
if (this.database) {
|
|
8770
9037
|
params.push("dbname=" + quoteParamValue(this.database));
|
|
8771
9038
|
}
|
|
@@ -8866,7 +9133,7 @@ var require_result = __commonJS({
|
|
|
8866
9133
|
if (this.fields.length) {
|
|
8867
9134
|
this._parsers = new Array(fieldDescriptions.length);
|
|
8868
9135
|
}
|
|
8869
|
-
const row =
|
|
9136
|
+
const row = /* @__PURE__ */ Object.create(null);
|
|
8870
9137
|
for (let i = 0; i < fieldDescriptions.length; i++) {
|
|
8871
9138
|
const desc = fieldDescriptions[i];
|
|
8872
9139
|
row[desc.name] = null;
|
|
@@ -9007,7 +9274,7 @@ var require_query = __commonJS({
|
|
|
9007
9274
|
if (typeof this.text !== "string" && typeof this.name !== "string") {
|
|
9008
9275
|
return new Error("A query must have either text or a name. Supplying neither is unsupported.");
|
|
9009
9276
|
}
|
|
9010
|
-
const previous = connection.parsedStatements[this.name];
|
|
9277
|
+
const previous = connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name];
|
|
9011
9278
|
if (this.text && previous && this.text !== previous) {
|
|
9012
9279
|
return new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`);
|
|
9013
9280
|
}
|
|
@@ -9027,7 +9294,7 @@ var require_query = __commonJS({
|
|
|
9027
9294
|
return null;
|
|
9028
9295
|
}
|
|
9029
9296
|
hasBeenParsed(connection) {
|
|
9030
|
-
return this.name && connection.parsedStatements[this.name];
|
|
9297
|
+
return this.name && (connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name]);
|
|
9031
9298
|
}
|
|
9032
9299
|
handlePortalSuspended(connection) {
|
|
9033
9300
|
this._getRows(connection, this.rows);
|
|
@@ -9051,6 +9318,9 @@ var require_query = __commonJS({
|
|
|
9051
9318
|
name: this.name,
|
|
9052
9319
|
types: this.types
|
|
9053
9320
|
});
|
|
9321
|
+
if (this.name) {
|
|
9322
|
+
connection.submittedNamedStatements[this.name] = this.text;
|
|
9323
|
+
}
|
|
9054
9324
|
}
|
|
9055
9325
|
try {
|
|
9056
9326
|
connection.bind({
|
|
@@ -9061,6 +9331,8 @@ var require_query = __commonJS({
|
|
|
9061
9331
|
valueMapper: utils.prepareValue
|
|
9062
9332
|
});
|
|
9063
9333
|
} catch (err) {
|
|
9334
|
+
connection.close({ type: "S", name: this.name });
|
|
9335
|
+
connection.sync();
|
|
9064
9336
|
this.handleError(err, connection);
|
|
9065
9337
|
return;
|
|
9066
9338
|
}
|
|
@@ -9300,6 +9572,25 @@ var require_buffer_writer = __commonJS({
|
|
|
9300
9572
|
this.offset += len;
|
|
9301
9573
|
return this;
|
|
9302
9574
|
}
|
|
9575
|
+
// Write an Int32 byte-length prefix immediately followed by the string's UTF-8
|
|
9576
|
+
// bytes. Postgres' Bind wire format prefixes every parameter with its length,
|
|
9577
|
+
// and doing it in one method computes Buffer.byteLength ONCE — the previous
|
|
9578
|
+
// `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned the string
|
|
9579
|
+
// three times (byteLength for the prefix, byteLength again inside addString,
|
|
9580
|
+
// then the encode), which is costly for large text parameters.
|
|
9581
|
+
addInt32PrefixedString(string4) {
|
|
9582
|
+
const len = Buffer.byteLength(string4);
|
|
9583
|
+
this.ensure(4 + len);
|
|
9584
|
+
const buffer = this.buffer;
|
|
9585
|
+
let offset = this.offset;
|
|
9586
|
+
buffer[offset++] = len >>> 24 & 255;
|
|
9587
|
+
buffer[offset++] = len >>> 16 & 255;
|
|
9588
|
+
buffer[offset++] = len >>> 8 & 255;
|
|
9589
|
+
buffer[offset++] = len >>> 0 & 255;
|
|
9590
|
+
buffer.write(string4, offset, "utf-8");
|
|
9591
|
+
this.offset = offset + len;
|
|
9592
|
+
return this;
|
|
9593
|
+
}
|
|
9303
9594
|
add(otherBuffer) {
|
|
9304
9595
|
this.ensure(otherBuffer.length);
|
|
9305
9596
|
otherBuffer.copy(this.buffer, this.offset);
|
|
@@ -9321,6 +9612,10 @@ var require_buffer_writer = __commonJS({
|
|
|
9321
9612
|
this.buffer = Buffer.allocUnsafe(this.size);
|
|
9322
9613
|
return result;
|
|
9323
9614
|
}
|
|
9615
|
+
clear() {
|
|
9616
|
+
this.offset = 5;
|
|
9617
|
+
this.headerPosition = 0;
|
|
9618
|
+
}
|
|
9324
9619
|
};
|
|
9325
9620
|
exports.Writer = Writer;
|
|
9326
9621
|
}
|
|
@@ -9357,7 +9652,7 @@ var require_serializer = __commonJS({
|
|
|
9357
9652
|
);
|
|
9358
9653
|
};
|
|
9359
9654
|
var sendSASLInitialResponseMessage = function(mechanism, initialResponse) {
|
|
9360
|
-
writer.addCString(mechanism).
|
|
9655
|
+
writer.addCString(mechanism).addInt32PrefixedString(initialResponse);
|
|
9361
9656
|
return writer.flush(
|
|
9362
9657
|
112
|
|
9363
9658
|
/* code.startup */
|
|
@@ -9416,8 +9711,7 @@ var require_serializer = __commonJS({
|
|
|
9416
9711
|
0
|
|
9417
9712
|
/* ParamType.STRING */
|
|
9418
9713
|
);
|
|
9419
|
-
paramWriter.
|
|
9420
|
-
paramWriter.addString(mappedVal);
|
|
9714
|
+
paramWriter.addInt32PrefixedString(mappedVal);
|
|
9421
9715
|
}
|
|
9422
9716
|
}
|
|
9423
9717
|
};
|
|
@@ -9429,7 +9723,13 @@ var require_serializer = __commonJS({
|
|
|
9429
9723
|
const len = values.length;
|
|
9430
9724
|
writer.addCString(portal).addCString(statement);
|
|
9431
9725
|
writer.addInt16(len);
|
|
9432
|
-
|
|
9726
|
+
try {
|
|
9727
|
+
writeValues(values, config2.valueMapper);
|
|
9728
|
+
} catch (err) {
|
|
9729
|
+
writer.clear();
|
|
9730
|
+
paramWriter.clear();
|
|
9731
|
+
throw err;
|
|
9732
|
+
}
|
|
9433
9733
|
writer.addInt16(len);
|
|
9434
9734
|
writer.add(paramWriter.flush());
|
|
9435
9735
|
writer.addInt16(1);
|
|
@@ -9587,7 +9887,7 @@ var require_buffer_reader = __commonJS({
|
|
|
9587
9887
|
cstring() {
|
|
9588
9888
|
const start = this.offset;
|
|
9589
9889
|
let end = start;
|
|
9590
|
-
while (this.buffer[end++]
|
|
9890
|
+
while (this.buffer[end++]) {
|
|
9591
9891
|
}
|
|
9592
9892
|
this.offset = end;
|
|
9593
9893
|
return this.buffer.toString(this.encoding, start, end - 1);
|
|
@@ -9809,7 +10109,7 @@ var require_parser = __commonJS({
|
|
|
9809
10109
|
const parameterCount = reader.int16();
|
|
9810
10110
|
const message = new messages_1.ParameterDescriptionMessage(LATEINIT_LENGTH, parameterCount);
|
|
9811
10111
|
for (let i = 0; i < parameterCount; i++) {
|
|
9812
|
-
message.dataTypeIDs[i] = reader.
|
|
10112
|
+
message.dataTypeIDs[i] = reader.uint32();
|
|
9813
10113
|
}
|
|
9814
10114
|
return message;
|
|
9815
10115
|
};
|
|
@@ -9914,7 +10214,8 @@ var require_dist2 = __commonJS({
|
|
|
9914
10214
|
"node_modules/pg-protocol/dist/index.js"(exports) {
|
|
9915
10215
|
"use strict";
|
|
9916
10216
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9917
|
-
exports.DatabaseError = exports.serialize =
|
|
10217
|
+
exports.DatabaseError = exports.serialize = void 0;
|
|
10218
|
+
exports.parse = parse3;
|
|
9918
10219
|
var messages_1 = require_messages();
|
|
9919
10220
|
Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function() {
|
|
9920
10221
|
return messages_1.DatabaseError;
|
|
@@ -9929,7 +10230,6 @@ var require_dist2 = __commonJS({
|
|
|
9929
10230
|
stream.on("data", (buffer) => parser.parse(buffer, callback));
|
|
9930
10231
|
return new Promise((resolve) => stream.on("end", () => resolve()));
|
|
9931
10232
|
}
|
|
9932
|
-
exports.parse = parse3;
|
|
9933
10233
|
}
|
|
9934
10234
|
});
|
|
9935
10235
|
|
|
@@ -10014,7 +10314,8 @@ var require_connection = __commonJS({
|
|
|
10014
10314
|
"use strict";
|
|
10015
10315
|
var EventEmitter = __require("events").EventEmitter;
|
|
10016
10316
|
var { parse: parse3, serialize } = require_dist2();
|
|
10017
|
-
var
|
|
10317
|
+
var stream = require_stream();
|
|
10318
|
+
var { getStream } = stream;
|
|
10018
10319
|
var flushBuffer = serialize.flush();
|
|
10019
10320
|
var syncBuffer = serialize.sync();
|
|
10020
10321
|
var endBuffer = serialize.end();
|
|
@@ -10029,7 +10330,9 @@ var require_connection = __commonJS({
|
|
|
10029
10330
|
this._keepAlive = config2.keepAlive;
|
|
10030
10331
|
this._keepAliveInitialDelayMillis = config2.keepAliveInitialDelayMillis;
|
|
10031
10332
|
this.parsedStatements = {};
|
|
10333
|
+
this.submittedNamedStatements = {};
|
|
10032
10334
|
this.ssl = config2.ssl || false;
|
|
10335
|
+
this.sslNegotiation = config2.sslNegotiation || "postgres";
|
|
10033
10336
|
this._ending = false;
|
|
10034
10337
|
this._emitMessage = false;
|
|
10035
10338
|
const self = this;
|
|
@@ -10063,6 +10366,11 @@ var require_connection = __commonJS({
|
|
|
10063
10366
|
if (!this.ssl) {
|
|
10064
10367
|
return this.attachListeners(this.stream);
|
|
10065
10368
|
}
|
|
10369
|
+
if (this.sslNegotiation === "direct") {
|
|
10370
|
+
return this.stream.once("connect", function() {
|
|
10371
|
+
self.upgradeToSSL(host, reportStreamError);
|
|
10372
|
+
});
|
|
10373
|
+
}
|
|
10066
10374
|
this.stream.once("data", function(buffer) {
|
|
10067
10375
|
const responseCode = buffer.toString("utf8");
|
|
10068
10376
|
switch (responseCode) {
|
|
@@ -10075,31 +10383,38 @@ var require_connection = __commonJS({
|
|
|
10075
10383
|
self.stream.end();
|
|
10076
10384
|
return self.emit("error", new Error("There was an error establishing an SSL connection"));
|
|
10077
10385
|
}
|
|
10078
|
-
|
|
10079
|
-
socket: self.stream
|
|
10080
|
-
};
|
|
10081
|
-
if (self.ssl !== true) {
|
|
10082
|
-
Object.assign(options, self.ssl);
|
|
10083
|
-
if ("key" in self.ssl) {
|
|
10084
|
-
options.key = self.ssl.key;
|
|
10085
|
-
}
|
|
10086
|
-
}
|
|
10087
|
-
const net = __require("net");
|
|
10088
|
-
if (net.isIP && net.isIP(host) === 0) {
|
|
10089
|
-
options.servername = host;
|
|
10090
|
-
}
|
|
10091
|
-
try {
|
|
10092
|
-
self.stream = getSecureStream(options);
|
|
10093
|
-
} catch (err) {
|
|
10094
|
-
return self.emit("error", err);
|
|
10095
|
-
}
|
|
10096
|
-
self.attachListeners(self.stream);
|
|
10097
|
-
self.stream.on("error", reportStreamError);
|
|
10098
|
-
self.emit("sslconnect");
|
|
10386
|
+
self.upgradeToSSL(host, reportStreamError);
|
|
10099
10387
|
});
|
|
10100
10388
|
}
|
|
10101
|
-
|
|
10102
|
-
|
|
10389
|
+
upgradeToSSL(host, reportStreamError) {
|
|
10390
|
+
const self = this;
|
|
10391
|
+
const options = {
|
|
10392
|
+
socket: self.stream
|
|
10393
|
+
};
|
|
10394
|
+
if (self.ssl !== true) {
|
|
10395
|
+
Object.assign(options, self.ssl);
|
|
10396
|
+
if ("key" in self.ssl) {
|
|
10397
|
+
options.key = self.ssl.key;
|
|
10398
|
+
}
|
|
10399
|
+
}
|
|
10400
|
+
if (self.sslNegotiation === "direct") {
|
|
10401
|
+
options.ALPNProtocols = ["postgresql"];
|
|
10402
|
+
}
|
|
10403
|
+
const net = __require("net");
|
|
10404
|
+
if (net.isIP && net.isIP(host) === 0) {
|
|
10405
|
+
options.servername = host;
|
|
10406
|
+
}
|
|
10407
|
+
try {
|
|
10408
|
+
self.stream = stream.getSecureStream(options);
|
|
10409
|
+
} catch (err) {
|
|
10410
|
+
return self.emit("error", err);
|
|
10411
|
+
}
|
|
10412
|
+
self.attachListeners(self.stream);
|
|
10413
|
+
self.stream.on("error", reportStreamError);
|
|
10414
|
+
self.emit("sslconnect");
|
|
10415
|
+
}
|
|
10416
|
+
attachListeners(stream2) {
|
|
10417
|
+
parse3(stream2, (msg) => {
|
|
10103
10418
|
const eventName = msg.name === "error" ? "errorMessage" : msg.name;
|
|
10104
10419
|
if (this._emitMessage) {
|
|
10105
10420
|
this.emit("message", msg);
|
|
@@ -10522,6 +10837,16 @@ var require_client = __commonJS({
|
|
|
10522
10837
|
},
|
|
10523
10838
|
"Calling client.query() when the client is already executing a query is deprecated and will be removed in pg@9.0. Use async/await or an external async flow control mechanism instead."
|
|
10524
10839
|
);
|
|
10840
|
+
function coerceNumberOrDefault(value, defaultValue) {
|
|
10841
|
+
if (typeof value === "number") {
|
|
10842
|
+
return Number.isFinite(value) ? value : defaultValue;
|
|
10843
|
+
}
|
|
10844
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
10845
|
+
const n = Number(value);
|
|
10846
|
+
return Number.isFinite(n) ? n : defaultValue;
|
|
10847
|
+
}
|
|
10848
|
+
return defaultValue;
|
|
10849
|
+
}
|
|
10525
10850
|
var Client2 = class extends EventEmitter {
|
|
10526
10851
|
constructor(config2) {
|
|
10527
10852
|
super();
|
|
@@ -10550,19 +10875,25 @@ var require_client = __commonJS({
|
|
|
10550
10875
|
this._connectionError = false;
|
|
10551
10876
|
this._queryable = true;
|
|
10552
10877
|
this._activeQuery = null;
|
|
10878
|
+
this._txStatus = null;
|
|
10553
10879
|
this.enableChannelBinding = Boolean(c.enableChannelBinding);
|
|
10880
|
+
this.scramMaxIterations = coerceNumberOrDefault(c.scramMaxIterations, sasl.DEFAULT_MAX_SCRAM_ITERATIONS);
|
|
10554
10881
|
this.connection = c.connection || new Connection2({
|
|
10555
10882
|
stream: c.stream,
|
|
10556
10883
|
ssl: this.connectionParameters.ssl,
|
|
10884
|
+
sslNegotiation: this.connectionParameters.sslnegotiation,
|
|
10557
10885
|
keepAlive: c.keepAlive || false,
|
|
10558
10886
|
keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
|
|
10559
10887
|
encoding: this.connectionParameters.client_encoding || "utf8"
|
|
10560
10888
|
});
|
|
10561
10889
|
this._queryQueue = [];
|
|
10890
|
+
this._sentQueryQueue = [];
|
|
10891
|
+
this.pipeline = Boolean(c.pipeline);
|
|
10562
10892
|
this.binary = c.binary || defaults2.binary;
|
|
10563
10893
|
this.processID = null;
|
|
10564
10894
|
this.secretKey = null;
|
|
10565
10895
|
this.ssl = this.connectionParameters.ssl || false;
|
|
10896
|
+
this.sslNegotiation = this.connectionParameters.sslnegotiation || "postgres";
|
|
10566
10897
|
if (this.ssl && this.ssl.key) {
|
|
10567
10898
|
Object.defineProperty(this.ssl, "key", {
|
|
10568
10899
|
enumerable: false
|
|
@@ -10592,6 +10923,8 @@ var require_client = __commonJS({
|
|
|
10592
10923
|
enqueueError(activeQuery);
|
|
10593
10924
|
this._activeQuery = null;
|
|
10594
10925
|
}
|
|
10926
|
+
this._sentQueryQueue.forEach(enqueueError);
|
|
10927
|
+
this._sentQueryQueue.length = 0;
|
|
10595
10928
|
this._queryQueue.forEach(enqueueError);
|
|
10596
10929
|
this._queryQueue.length = 0;
|
|
10597
10930
|
}
|
|
@@ -10623,7 +10956,9 @@ var require_client = __commonJS({
|
|
|
10623
10956
|
}
|
|
10624
10957
|
con.on("connect", function() {
|
|
10625
10958
|
if (self.ssl) {
|
|
10626
|
-
|
|
10959
|
+
if (self.sslNegotiation !== "direct") {
|
|
10960
|
+
con.requestSsl();
|
|
10961
|
+
}
|
|
10627
10962
|
} else {
|
|
10628
10963
|
con.startup(self.getStartupConf());
|
|
10629
10964
|
}
|
|
@@ -10741,7 +11076,11 @@ var require_client = __commonJS({
|
|
|
10741
11076
|
_handleAuthSASL(msg) {
|
|
10742
11077
|
this._getPassword(() => {
|
|
10743
11078
|
try {
|
|
10744
|
-
this.saslSession = sasl.startSession(
|
|
11079
|
+
this.saslSession = sasl.startSession(
|
|
11080
|
+
msg.mechanisms,
|
|
11081
|
+
this.enableChannelBinding && this.connection.stream,
|
|
11082
|
+
this.scramMaxIterations
|
|
11083
|
+
);
|
|
10745
11084
|
this.connection.sendSASLInitialResponseMessage(this.saslSession.mechanism, this.saslSession.response);
|
|
10746
11085
|
} catch (err) {
|
|
10747
11086
|
this.connection.emit("error", err);
|
|
@@ -10786,6 +11125,7 @@ var require_client = __commonJS({
|
|
|
10786
11125
|
}
|
|
10787
11126
|
const activeQuery = this._getActiveQuery();
|
|
10788
11127
|
this._activeQuery = null;
|
|
11128
|
+
this._txStatus = msg?.status ?? null;
|
|
10789
11129
|
this.readyForQuery = true;
|
|
10790
11130
|
if (activeQuery) {
|
|
10791
11131
|
activeQuery.handleReadyForQuery(this.connection);
|
|
@@ -10827,6 +11167,9 @@ var require_client = __commonJS({
|
|
|
10827
11167
|
return;
|
|
10828
11168
|
}
|
|
10829
11169
|
this._activeQuery = null;
|
|
11170
|
+
if (activeQuery.name) {
|
|
11171
|
+
delete this.connection.submittedNamedStatements[activeQuery.name];
|
|
11172
|
+
}
|
|
10830
11173
|
activeQuery.handleError(msg, this.connection);
|
|
10831
11174
|
}
|
|
10832
11175
|
_handleRowDescription(msg) {
|
|
@@ -10883,6 +11226,7 @@ var require_client = __commonJS({
|
|
|
10883
11226
|
}
|
|
10884
11227
|
if (activeQuery.name) {
|
|
10885
11228
|
this.connection.parsedStatements[activeQuery.name] = activeQuery.text;
|
|
11229
|
+
delete this.connection.submittedNamedStatements[activeQuery.name];
|
|
10886
11230
|
}
|
|
10887
11231
|
}
|
|
10888
11232
|
_handleCopyInResponse(msg) {
|
|
@@ -10949,6 +11293,9 @@ var require_client = __commonJS({
|
|
|
10949
11293
|
});
|
|
10950
11294
|
} else if (client._queryQueue.indexOf(query) !== -1) {
|
|
10951
11295
|
client._queryQueue.splice(client._queryQueue.indexOf(query), 1);
|
|
11296
|
+
} else if (client._sentQueryQueue.indexOf(query) !== -1) {
|
|
11297
|
+
query.callback = () => {
|
|
11298
|
+
};
|
|
10952
11299
|
}
|
|
10953
11300
|
}
|
|
10954
11301
|
setTypeParser(oid, format, parseFn) {
|
|
@@ -10967,6 +11314,10 @@ var require_client = __commonJS({
|
|
|
10967
11314
|
return utils.escapeLiteral(str);
|
|
10968
11315
|
}
|
|
10969
11316
|
_pulseQueryQueue() {
|
|
11317
|
+
if (this.pipeline) {
|
|
11318
|
+
this._pulsePipelinedQueryQueue();
|
|
11319
|
+
return;
|
|
11320
|
+
}
|
|
10970
11321
|
if (this.readyForQuery === true) {
|
|
10971
11322
|
this._activeQuery = this._queryQueue.shift();
|
|
10972
11323
|
const activeQuery = this._getActiveQuery();
|
|
@@ -10987,16 +11338,37 @@ var require_client = __commonJS({
|
|
|
10987
11338
|
}
|
|
10988
11339
|
}
|
|
10989
11340
|
}
|
|
11341
|
+
_pulsePipelinedQueryQueue() {
|
|
11342
|
+
if (!this._connected || !this._queryable) {
|
|
11343
|
+
return;
|
|
11344
|
+
}
|
|
11345
|
+
while (this._queryQueue.length > 0) {
|
|
11346
|
+
const query = this._queryQueue.shift();
|
|
11347
|
+
this.hasExecuted = true;
|
|
11348
|
+
const queryError = query.submit(this.connection);
|
|
11349
|
+
if (queryError) {
|
|
11350
|
+
process.nextTick(() => {
|
|
11351
|
+
query.handleError(queryError, this.connection);
|
|
11352
|
+
});
|
|
11353
|
+
continue;
|
|
11354
|
+
}
|
|
11355
|
+
this._sentQueryQueue.push(query);
|
|
11356
|
+
}
|
|
11357
|
+
if (this.readyForQuery && !this._activeQuery && this._sentQueryQueue.length > 0) {
|
|
11358
|
+
this._activeQuery = this._sentQueryQueue.shift();
|
|
11359
|
+
this.readyForQuery = false;
|
|
11360
|
+
}
|
|
11361
|
+
if (!this._activeQuery && this._sentQueryQueue.length === 0 && this._queryQueue.length === 0 && this.hasExecuted) {
|
|
11362
|
+
this.emit("drain");
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
10990
11365
|
query(config2, values, callback) {
|
|
10991
11366
|
let query;
|
|
10992
11367
|
let result;
|
|
10993
|
-
|
|
10994
|
-
let readTimeoutTimer;
|
|
10995
|
-
let queryCallback;
|
|
10996
|
-
if (config2 === null || config2 === void 0) {
|
|
11368
|
+
if (config2 == null) {
|
|
10997
11369
|
throw new TypeError("Client was passed a null or undefined query");
|
|
10998
|
-
}
|
|
10999
|
-
|
|
11370
|
+
}
|
|
11371
|
+
if (typeof config2.submit === "function") {
|
|
11000
11372
|
result = query = config2;
|
|
11001
11373
|
if (!query.callback) {
|
|
11002
11374
|
if (typeof values === "function") {
|
|
@@ -11006,7 +11378,6 @@ var require_client = __commonJS({
|
|
|
11006
11378
|
}
|
|
11007
11379
|
}
|
|
11008
11380
|
} else {
|
|
11009
|
-
readTimeout = config2.query_timeout || this.connectionParameters.query_timeout;
|
|
11010
11381
|
query = new Query2(config2, values, callback);
|
|
11011
11382
|
if (!query.callback) {
|
|
11012
11383
|
result = new this._Promise((resolve, reject) => {
|
|
@@ -11015,12 +11386,15 @@ var require_client = __commonJS({
|
|
|
11015
11386
|
Error.captureStackTrace(err);
|
|
11016
11387
|
throw err;
|
|
11017
11388
|
});
|
|
11389
|
+
} else if (typeof query.callback !== "function") {
|
|
11390
|
+
throw new TypeError("callback is not a function");
|
|
11018
11391
|
}
|
|
11019
11392
|
}
|
|
11393
|
+
const readTimeout = config2.query_timeout || this.connectionParameters.query_timeout;
|
|
11020
11394
|
if (readTimeout) {
|
|
11021
|
-
queryCallback = query.callback || (() => {
|
|
11395
|
+
const queryCallback = query.callback || (() => {
|
|
11022
11396
|
});
|
|
11023
|
-
readTimeoutTimer = setTimeout(() => {
|
|
11397
|
+
const readTimeoutTimer = setTimeout(() => {
|
|
11024
11398
|
const error51 = new Error("Query read timeout");
|
|
11025
11399
|
process.nextTick(() => {
|
|
11026
11400
|
query.handleError(error51, this.connection);
|
|
@@ -11031,6 +11405,9 @@ var require_client = __commonJS({
|
|
|
11031
11405
|
const index = this._queryQueue.indexOf(query);
|
|
11032
11406
|
if (index > -1) {
|
|
11033
11407
|
this._queryQueue.splice(index, 1);
|
|
11408
|
+
} else if (this.pipeline) {
|
|
11409
|
+
this.connection.stream.destroy();
|
|
11410
|
+
return;
|
|
11034
11411
|
}
|
|
11035
11412
|
this._pulseQueryQueue();
|
|
11036
11413
|
}, readTimeout);
|
|
@@ -11057,7 +11434,7 @@ var require_client = __commonJS({
|
|
|
11057
11434
|
});
|
|
11058
11435
|
return result;
|
|
11059
11436
|
}
|
|
11060
|
-
if (this._queryQueue.length > 0) {
|
|
11437
|
+
if (this._queryQueue.length > 0 && !this.pipeline) {
|
|
11061
11438
|
queryQueueLengthDeprecationNotice();
|
|
11062
11439
|
}
|
|
11063
11440
|
this._queryQueue.push(query);
|
|
@@ -11070,16 +11447,24 @@ var require_client = __commonJS({
|
|
|
11070
11447
|
unref() {
|
|
11071
11448
|
this.connection.unref();
|
|
11072
11449
|
}
|
|
11450
|
+
getTransactionStatus() {
|
|
11451
|
+
return this._txStatus;
|
|
11452
|
+
}
|
|
11073
11453
|
end(cb) {
|
|
11074
11454
|
this._ending = true;
|
|
11075
11455
|
if (!this.connection._connecting || this._ended) {
|
|
11076
11456
|
if (cb) {
|
|
11077
11457
|
cb();
|
|
11458
|
+
return;
|
|
11078
11459
|
} else {
|
|
11079
11460
|
return this._Promise.resolve();
|
|
11080
11461
|
}
|
|
11081
11462
|
}
|
|
11082
|
-
if (
|
|
11463
|
+
if (!this._queryable) {
|
|
11464
|
+
this.connection.stream.destroy();
|
|
11465
|
+
} else if (this.pipeline && (this._getActiveQuery() || this._sentQueryQueue.length > 0 || this._queryQueue.length > 0)) {
|
|
11466
|
+
this.once("drain", () => this.connection.end());
|
|
11467
|
+
} else if (this._getActiveQuery()) {
|
|
11083
11468
|
this.connection.stream.destroy();
|
|
11084
11469
|
} else {
|
|
11085
11470
|
this.connection.end();
|
|
@@ -11569,7 +11954,7 @@ var require_query2 = __commonJS({
|
|
|
11569
11954
|
sourceFunction: "routine"
|
|
11570
11955
|
};
|
|
11571
11956
|
NativeQuery.prototype.handleError = function(err) {
|
|
11572
|
-
const fields = this.native.pq.resultErrorFields();
|
|
11957
|
+
const fields = this.native && this.native.pq.resultErrorFields();
|
|
11573
11958
|
if (fields) {
|
|
11574
11959
|
for (const key in fields) {
|
|
11575
11960
|
const normalizedFieldName = errorFieldMap[key] || key;
|
|
@@ -11702,6 +12087,8 @@ var require_client2 = __commonJS({
|
|
|
11702
12087
|
this._connecting = false;
|
|
11703
12088
|
this._connected = false;
|
|
11704
12089
|
this._queryable = true;
|
|
12090
|
+
this.pipeline = Boolean(config2.pipeline);
|
|
12091
|
+
this._pipelineInFlight = false;
|
|
11705
12092
|
const cp = this.connectionParameters = new ConnectionParameters(config2);
|
|
11706
12093
|
if (config2.nativeConnectionString) cp.nativeConnectionString = config2.nativeConnectionString;
|
|
11707
12094
|
this.user = cp.user;
|
|
@@ -11845,7 +12232,7 @@ var require_client2 = __commonJS({
|
|
|
11845
12232
|
});
|
|
11846
12233
|
return result;
|
|
11847
12234
|
}
|
|
11848
|
-
if (this._queryQueue.length > 0) {
|
|
12235
|
+
if (this._queryQueue.length > 0 && !this.pipeline) {
|
|
11849
12236
|
queryQueueLengthDeprecationNotice();
|
|
11850
12237
|
}
|
|
11851
12238
|
this._queryQueue.push(query);
|
|
@@ -11855,8 +12242,11 @@ var require_client2 = __commonJS({
|
|
|
11855
12242
|
Client2.prototype.end = function(cb) {
|
|
11856
12243
|
const self = this;
|
|
11857
12244
|
this._ending = true;
|
|
11858
|
-
if (!this._connected) {
|
|
11859
|
-
this.once("connect",
|
|
12245
|
+
if (this._connecting && !this._connected) {
|
|
12246
|
+
this.once("connect", () => {
|
|
12247
|
+
this.end(() => {
|
|
12248
|
+
});
|
|
12249
|
+
});
|
|
11860
12250
|
}
|
|
11861
12251
|
let result;
|
|
11862
12252
|
if (!cb) {
|
|
@@ -11864,14 +12254,21 @@ var require_client2 = __commonJS({
|
|
|
11864
12254
|
cb = (err) => err ? reject(err) : resolve();
|
|
11865
12255
|
});
|
|
11866
12256
|
}
|
|
11867
|
-
|
|
11868
|
-
self.
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
12257
|
+
const doEnd = function() {
|
|
12258
|
+
self.native.end(function() {
|
|
12259
|
+
self._connected = false;
|
|
12260
|
+
self._errorAllQueries(new Error("Connection terminated"));
|
|
12261
|
+
process.nextTick(() => {
|
|
12262
|
+
self.emit("end");
|
|
12263
|
+
if (cb) cb();
|
|
12264
|
+
});
|
|
11873
12265
|
});
|
|
11874
|
-
}
|
|
12266
|
+
};
|
|
12267
|
+
if (this.pipeline && (this._pipelineInFlight || this._queryQueue.length > 0)) {
|
|
12268
|
+
this.once("drain", doEnd);
|
|
12269
|
+
} else {
|
|
12270
|
+
doEnd();
|
|
12271
|
+
}
|
|
11875
12272
|
return result;
|
|
11876
12273
|
};
|
|
11877
12274
|
Client2.prototype._hasActiveQuery = function() {
|
|
@@ -11881,6 +12278,9 @@ var require_client2 = __commonJS({
|
|
|
11881
12278
|
if (!this._connected) {
|
|
11882
12279
|
return;
|
|
11883
12280
|
}
|
|
12281
|
+
if (this.pipeline && !initialConnection) {
|
|
12282
|
+
return this._pulsePipelinedQueryQueue();
|
|
12283
|
+
}
|
|
11884
12284
|
if (this._hasActiveQuery()) {
|
|
11885
12285
|
return;
|
|
11886
12286
|
}
|
|
@@ -11898,6 +12298,69 @@ var require_client2 = __commonJS({
|
|
|
11898
12298
|
self._pulseQueryQueue();
|
|
11899
12299
|
});
|
|
11900
12300
|
};
|
|
12301
|
+
Client2.prototype._pulsePipelinedQueryQueue = function() {
|
|
12302
|
+
if (!this._connected || this._pipelineInFlight) {
|
|
12303
|
+
return;
|
|
12304
|
+
}
|
|
12305
|
+
if (this._queryQueue.length === 0) {
|
|
12306
|
+
if (this.hasExecuted) {
|
|
12307
|
+
this.emit("drain");
|
|
12308
|
+
}
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
this._pipelineInFlight = true;
|
|
12312
|
+
const self = this;
|
|
12313
|
+
const queries = [];
|
|
12314
|
+
const nativeQueries = [];
|
|
12315
|
+
const utils = require_utils2();
|
|
12316
|
+
while (this._queryQueue.length > 0) {
|
|
12317
|
+
const query = this._queryQueue.shift();
|
|
12318
|
+
this.hasExecuted = true;
|
|
12319
|
+
nativeQueries.push(query);
|
|
12320
|
+
const values = query.values ? query.values.map(utils.prepareValue) : null;
|
|
12321
|
+
const pipelineEntry = { text: query.text, name: query.name };
|
|
12322
|
+
if (values) {
|
|
12323
|
+
pipelineEntry.values = values;
|
|
12324
|
+
}
|
|
12325
|
+
if (query.name && this.namedQueries[query.name]) {
|
|
12326
|
+
pipelineEntry._alreadyPrepared = true;
|
|
12327
|
+
}
|
|
12328
|
+
queries.push(pipelineEntry);
|
|
12329
|
+
}
|
|
12330
|
+
this.native.pipeline(queries, function(err, results) {
|
|
12331
|
+
self._pipelineInFlight = false;
|
|
12332
|
+
if (err) {
|
|
12333
|
+
for (let i = 0; i < nativeQueries.length; i++) {
|
|
12334
|
+
const q = nativeQueries[i];
|
|
12335
|
+
q.native = self.native;
|
|
12336
|
+
q.handleError(err);
|
|
12337
|
+
}
|
|
12338
|
+
self._pulsePipelinedQueryQueue();
|
|
12339
|
+
return;
|
|
12340
|
+
}
|
|
12341
|
+
for (let i = 0; i < nativeQueries.length; i++) {
|
|
12342
|
+
const q = nativeQueries[i];
|
|
12343
|
+
const r = results[i];
|
|
12344
|
+
q.native = self.native;
|
|
12345
|
+
if (r.err) {
|
|
12346
|
+
q.handleError(r.err);
|
|
12347
|
+
} else {
|
|
12348
|
+
if (q.name) {
|
|
12349
|
+
self.namedQueries[q.name] = q.text;
|
|
12350
|
+
}
|
|
12351
|
+
q.state = "end";
|
|
12352
|
+
q.emit("end", r.result);
|
|
12353
|
+
if (q.callback) {
|
|
12354
|
+
q.callback(null, r.result);
|
|
12355
|
+
}
|
|
12356
|
+
}
|
|
12357
|
+
setImmediate(function() {
|
|
12358
|
+
q.emit("_done");
|
|
12359
|
+
});
|
|
12360
|
+
}
|
|
12361
|
+
self._pulsePipelinedQueryQueue();
|
|
12362
|
+
});
|
|
12363
|
+
};
|
|
11901
12364
|
Client2.prototype.cancel = function(query) {
|
|
11902
12365
|
if (this._activeQuery === query) {
|
|
11903
12366
|
this.native.cancel(function() {
|
|
@@ -11919,6 +12382,9 @@ var require_client2 = __commonJS({
|
|
|
11919
12382
|
Client2.prototype.isConnected = function() {
|
|
11920
12383
|
return this._connected;
|
|
11921
12384
|
};
|
|
12385
|
+
Client2.prototype.getTransactionStatus = function() {
|
|
12386
|
+
return this.native.getTransactionStatus();
|
|
12387
|
+
};
|
|
11922
12388
|
}
|
|
11923
12389
|
});
|
|
11924
12390
|
|
|
@@ -28152,17 +28618,33 @@ function normalizeObjectSchema(schema) {
|
|
|
28152
28618
|
}
|
|
28153
28619
|
return void 0;
|
|
28154
28620
|
}
|
|
28621
|
+
function getDotPath(path) {
|
|
28622
|
+
if (path.length === 0) {
|
|
28623
|
+
return "object root";
|
|
28624
|
+
}
|
|
28625
|
+
return path.reduce((acc, seg, index) => {
|
|
28626
|
+
if (index === 0) {
|
|
28627
|
+
return String(seg);
|
|
28628
|
+
}
|
|
28629
|
+
if (typeof seg === "number") {
|
|
28630
|
+
return `${acc}[${seg}]`;
|
|
28631
|
+
}
|
|
28632
|
+
return `${acc}.${seg}`;
|
|
28633
|
+
}, "");
|
|
28634
|
+
}
|
|
28155
28635
|
function getParseErrorMessage(error51) {
|
|
28156
28636
|
if (error51 && typeof error51 === "object") {
|
|
28637
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
28638
|
+
return error51.issues.map((i) => {
|
|
28639
|
+
if (!i.path?.length) {
|
|
28640
|
+
return i.message;
|
|
28641
|
+
}
|
|
28642
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
28643
|
+
}).join("\n");
|
|
28644
|
+
}
|
|
28157
28645
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
28158
28646
|
return error51.message;
|
|
28159
28647
|
}
|
|
28160
|
-
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
28161
|
-
const firstIssue = error51.issues[0];
|
|
28162
|
-
if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
|
|
28163
|
-
return String(firstIssue.message);
|
|
28164
|
-
}
|
|
28165
|
-
}
|
|
28166
28648
|
try {
|
|
28167
28649
|
return JSON.stringify(error51);
|
|
28168
28650
|
} catch {
|
|
@@ -34777,16 +35259,7 @@ var Server = class extends Protocol {
|
|
|
34777
35259
|
if (!methodSchema) {
|
|
34778
35260
|
throw new Error("Schema is missing a method literal");
|
|
34779
35261
|
}
|
|
34780
|
-
|
|
34781
|
-
if (isZ4Schema(methodSchema)) {
|
|
34782
|
-
const v4Schema = methodSchema;
|
|
34783
|
-
const v4Def = v4Schema._zod?.def;
|
|
34784
|
-
methodValue = v4Def?.value ?? v4Schema.value;
|
|
34785
|
-
} else {
|
|
34786
|
-
const v3Schema = methodSchema;
|
|
34787
|
-
const legacyDef = v3Schema._def;
|
|
34788
|
-
methodValue = legacyDef?.value ?? v3Schema.value;
|
|
34789
|
-
}
|
|
35262
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
34790
35263
|
if (typeof methodValue !== "string") {
|
|
34791
35264
|
throw new Error("Schema method literal must be a string");
|
|
34792
35265
|
}
|
|
@@ -35974,8 +36447,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
35974
36447
|
import process3 from "node:process";
|
|
35975
36448
|
|
|
35976
36449
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
36450
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
35977
36451
|
var ReadBuffer = class {
|
|
36452
|
+
constructor(options) {
|
|
36453
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
36454
|
+
}
|
|
35978
36455
|
append(chunk) {
|
|
36456
|
+
const newSize = (this._buffer?.length ?? 0) + chunk.length;
|
|
36457
|
+
if (newSize > this._maxBufferSize) {
|
|
36458
|
+
this.clear();
|
|
36459
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
36460
|
+
}
|
|
35979
36461
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
35980
36462
|
}
|
|
35981
36463
|
readMessage() {
|
|
@@ -36003,18 +36485,24 @@ function serializeMessage(message) {
|
|
|
36003
36485
|
|
|
36004
36486
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
36005
36487
|
var StdioServerTransport = class {
|
|
36006
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
36488
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
36007
36489
|
this._stdin = _stdin;
|
|
36008
36490
|
this._stdout = _stdout;
|
|
36009
|
-
this._readBuffer = new ReadBuffer();
|
|
36010
36491
|
this._started = false;
|
|
36011
36492
|
this._ondata = (chunk) => {
|
|
36012
|
-
|
|
36013
|
-
|
|
36493
|
+
try {
|
|
36494
|
+
this._readBuffer.append(chunk);
|
|
36495
|
+
this.processReadBuffer();
|
|
36496
|
+
} catch (error51) {
|
|
36497
|
+
this.onerror?.(error51);
|
|
36498
|
+
this.close().catch(() => {
|
|
36499
|
+
});
|
|
36500
|
+
}
|
|
36014
36501
|
};
|
|
36015
36502
|
this._onerror = (error51) => {
|
|
36016
36503
|
this.onerror?.(error51);
|
|
36017
36504
|
};
|
|
36505
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
36018
36506
|
}
|
|
36019
36507
|
/**
|
|
36020
36508
|
* Starts listening for messages on stdin.
|
|
@@ -36115,6 +36603,31 @@ function isWritesAllowed() {
|
|
|
36115
36603
|
const v = process.env.ALLOW_WRITES;
|
|
36116
36604
|
return v === "1" || v === "true";
|
|
36117
36605
|
}
|
|
36606
|
+
function getApplicationName() {
|
|
36607
|
+
const raw = process.env.POSTGRES_APPLICATION_NAME;
|
|
36608
|
+
return raw && raw.trim() !== "" ? raw : "postgres-mcp";
|
|
36609
|
+
}
|
|
36610
|
+
var PG16 = 16e4;
|
|
36611
|
+
var PG17 = 17e4;
|
|
36612
|
+
var PG18 = 18e4;
|
|
36613
|
+
var serverVersionNum = null;
|
|
36614
|
+
var poolGeneration = 0;
|
|
36615
|
+
async function getServerVersionNum(client) {
|
|
36616
|
+
if (serverVersionNum !== null) return serverVersionNum;
|
|
36617
|
+
const generation = poolGeneration;
|
|
36618
|
+
try {
|
|
36619
|
+
const runner = client ?? getPool();
|
|
36620
|
+
const res = await runner.query("SELECT current_setting('server_version_num') AS v");
|
|
36621
|
+
const parsed = Number.parseInt(res.rows[0]?.v ?? "", 10);
|
|
36622
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
36623
|
+
if (generation === poolGeneration) serverVersionNum = parsed;
|
|
36624
|
+
return parsed;
|
|
36625
|
+
}
|
|
36626
|
+
return 0;
|
|
36627
|
+
} catch {
|
|
36628
|
+
return 0;
|
|
36629
|
+
}
|
|
36630
|
+
}
|
|
36118
36631
|
function getSslConfig() {
|
|
36119
36632
|
const raw = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED;
|
|
36120
36633
|
if (raw === void 0) return void 0;
|
|
@@ -36130,6 +36643,11 @@ function getPool() {
|
|
|
36130
36643
|
const ssl = getSslConfig();
|
|
36131
36644
|
pool = new esm_default.Pool({
|
|
36132
36645
|
connectionString: getDatabaseUrl(),
|
|
36646
|
+
// Identify this server in pg_stat_activity. Without it, agent traffic is
|
|
36647
|
+
// anonymous to whoever is watching the database -- while pg_health itself
|
|
36648
|
+
// reports application_name for every OTHER session. See getApplicationName
|
|
36649
|
+
// for why a DSN-supplied application_name still wins.
|
|
36650
|
+
application_name: getApplicationName(),
|
|
36133
36651
|
statement_timeout: getStatementTimeoutMs(),
|
|
36134
36652
|
connectionTimeoutMillis: getConnectionTimeoutMs(),
|
|
36135
36653
|
max: getPoolMax(),
|
|
@@ -36355,6 +36873,8 @@ async function withSharedClient(fn) {
|
|
|
36355
36873
|
}
|
|
36356
36874
|
async function shutdown() {
|
|
36357
36875
|
typeNameCache = null;
|
|
36876
|
+
serverVersionNum = null;
|
|
36877
|
+
poolGeneration++;
|
|
36358
36878
|
if (!pool) return;
|
|
36359
36879
|
const ending = pool;
|
|
36360
36880
|
pool = null;
|
|
@@ -36709,7 +37229,7 @@ var adminTools = [
|
|
|
36709
37229
|
},
|
|
36710
37230
|
{
|
|
36711
37231
|
name: "pg_advisor",
|
|
36712
|
-
description: "Rolled-up DBA lint pass. One call returns
|
|
37232
|
+
description: "Rolled-up DBA lint pass. One call returns four categories of findings:\n- sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class.\n- wraparound_risk: transaction-ID AND multixact wraparound pressure, the classic pageable incident. `{autovacuum_freeze_max_age, autovacuum_multixact_freeze_max_age, databases[], tables[]}`. Those two cluster GUCs are the divisors both lists are measured against (null if unreadable). Multixact IDs are a SEPARATE 32-bit counter, consumed by row-level locking (`SELECT ... FOR SHARE/UPDATE`, FK checks), so a lock-heavy workload can exhaust them while relfrozenxid stays perfectly healthy -- both counters are checked here. `databases` rows: `{database, xid_age (age(datfrozenxid)), mxid_age (mxid_age(datminmxid)), pct_of_freeze_max_age, pct_of_multixact_freeze_max_age, triggered_by}` -- template databases included, since template0 ages like any other and the cluster horizon is the minimum across all of them. `tables` rows: `{schema, table, relkind, xid_age (age(relfrozenxid)), freeze_max_age, pct_of_freeze_max_age, mxid_age (mxid_age(relminmxid)), multixact_freeze_max_age, pct_of_multixact_freeze_max_age, triggered_by}`, where `freeze_max_age` / `multixact_freeze_max_age` are the EFFECTIVE limits -- a per-table `autovacuum_freeze_max_age` / `autovacuum_multixact_freeze_max_age` storage parameter wins over the GUC. A row is returned when EITHER ratio is at or above `wraparoundThreshold`, and `triggered_by` ('xid' | 'multixact' | 'both') says which one did it: 'xid' means chase freezing/autovacuum, 'multixact' means chase the lock-heavy workload burning members. `mxid_age` and `pct_of_multixact_freeze_max_age` are null on rows whose minmxid is InvalidMultiXactId (no multixact ever recorded); such rows can only be xid-triggered. At pct_of_freeze_max_age 1.0 autovacuum forces an anti-wraparound VACUUM, and near 2.1 billion xids (or 4.2 billion multixacts) the server stops accepting writes. `tables` deliberately includes pg_catalog and pg_toast relations -- the culprit is more often a TOAST table or a system catalog than a user table. On PG18+ table rows also carry `pages` / `all_frozen_pages` / `frozen_page_fraction` from `pg_class.relallfrozen` (visibility-map freeze coverage); those three keys are ABSENT on older servers rather than null.\n- tables_without_primary_key: user tables (plain and partitioned) with no PK defined. Bloat candidates and a sign of design drift; some replication setups also need PKs. Foreign tables are excluded -- PostgreSQL forbids declaring PKs on foreign tables.\n- public_tables_without_rls: tables in `public` (or any schema in `rlsSchemas`) with row-level security disabled. Useful as a security baseline check.\nAny category whose query fails (permission-gated catalogs on managed providers) appends to `_warnings` and returns empty; the other categories still return.\nUse this as the 'what should I be looking at?' starting point, then drill into `pg_unused_indexes`, `pg_table_bloat`, `pg_seq_scan_tables` for the perf side.",
|
|
36713
37233
|
annotations: {
|
|
36714
37234
|
title: "Database advisor (DBA lints)",
|
|
36715
37235
|
readOnlyHint: true,
|
|
@@ -36719,17 +37239,26 @@ var adminTools = [
|
|
|
36719
37239
|
},
|
|
36720
37240
|
inputSchema: external_exports.object({
|
|
36721
37241
|
seqExhaustionThreshold: external_exports.number().min(0).max(1).default(0.5).describe("Minimum used-fraction (last_value / max_value) to flag a sequence (default 0.5 = 50%)."),
|
|
37242
|
+
wraparoundThreshold: external_exports.number().min(0).max(1).default(0.5).describe(
|
|
37243
|
+
"Minimum used-fraction to flag a database or table for wraparound risk (default 0.5 = 50%). Applied to BOTH ratios -- age(frozenxid) / autovacuum_freeze_max_age and mxid_age(minmxid) / autovacuum_multixact_freeze_max_age -- and a row is flagged if either one clears it. 1.0 is where autovacuum starts forcing anti-wraparound VACUUMs."
|
|
37244
|
+
),
|
|
36722
37245
|
rlsSchemas: external_exports.array(identSchema).default(["public"]).describe("Schemas where RLS-missing should be flagged. Defaults to ['public']."),
|
|
36723
37246
|
limit: external_exports.number().int().min(1).max(500).default(50).describe("Max rows per category (default 50).")
|
|
36724
37247
|
}),
|
|
36725
37248
|
handler: async (input) => {
|
|
36726
37249
|
const {
|
|
36727
37250
|
seqExhaustionThreshold = 0.5,
|
|
37251
|
+
wraparoundThreshold = 0.5,
|
|
36728
37252
|
rlsSchemas = ["public"],
|
|
36729
37253
|
limit = 50
|
|
36730
37254
|
} = input;
|
|
37255
|
+
const versionNum = await getServerVersionNum();
|
|
37256
|
+
const frozenCoverageCols = versionNum >= PG18 ? `,
|
|
37257
|
+
c.relpages AS pages,
|
|
37258
|
+
c.relallfrozen AS all_frozen_pages,
|
|
37259
|
+
(c.relallfrozen::numeric / NULLIF(c.relpages, 0))::numeric(6, 4)::float8 AS frozen_page_fraction` : "";
|
|
36731
37260
|
return withSharedClient(async (run) => {
|
|
36732
|
-
const [seqRes, noPkRes, rlsRes] = await Promise.all([
|
|
37261
|
+
const [seqRes, wrapGucRes, wrapDbRes, wrapTblRes, noPkRes, rlsRes] = await Promise.all([
|
|
36733
37262
|
run(
|
|
36734
37263
|
// pg_sequences was added in PG10. last_value can be NULL on a never-
|
|
36735
37264
|
// touched sequence; we filter those out (nothing to report yet).
|
|
@@ -36755,6 +37284,193 @@ var adminTools = [
|
|
|
36755
37284
|
LIMIT $2`,
|
|
36756
37285
|
[seqExhaustionThreshold, limit]
|
|
36757
37286
|
),
|
|
37287
|
+
run(
|
|
37288
|
+
// Fetched as its own row rather than repeated on every finding:
|
|
37289
|
+
// both wraparound lists are threshold-filtered, so on a healthy
|
|
37290
|
+
// cluster they are empty and a per-row copy of the GUC would leave
|
|
37291
|
+
// the category with no scale at all. The caller needs the divisor
|
|
37292
|
+
// to interpret an empty result as "nothing above the threshold"
|
|
37293
|
+
// rather than "could not measure".
|
|
37294
|
+
//
|
|
37295
|
+
// Both GUCs come back in ONE row, not two sub-queries: they are
|
|
37296
|
+
// the two divisors for the same category, and splitting them would
|
|
37297
|
+
// add a seventh round trip to the fanout for a second scalar that
|
|
37298
|
+
// fails or succeeds under exactly the same conditions as the first.
|
|
37299
|
+
// autovacuum_multixact_freeze_max_age exists as far back as PG9.3,
|
|
37300
|
+
// so unlike relallfrozen below it needs no version gate.
|
|
37301
|
+
`SELECT
|
|
37302
|
+
current_setting('autovacuum_freeze_max_age')::int AS autovacuum_freeze_max_age,
|
|
37303
|
+
current_setting('autovacuum_multixact_freeze_max_age')::int AS autovacuum_multixact_freeze_max_age`
|
|
37304
|
+
),
|
|
37305
|
+
run(
|
|
37306
|
+
// Template databases are deliberately NOT excluded. template0 has
|
|
37307
|
+
// datallowconn = false and so is never autovacuumed through the
|
|
37308
|
+
// normal path, but its datfrozenxid ages exactly like any other
|
|
37309
|
+
// database and the cluster-wide wraparound horizon is the MINIMUM
|
|
37310
|
+
// across all of pg_database. Filtering templates out is how a
|
|
37311
|
+
// wraparound incident hides from the check that was meant to catch
|
|
37312
|
+
// it.
|
|
37313
|
+
//
|
|
37314
|
+
// numeric(10, 4), not the numeric(6, 4) used for sequence
|
|
37315
|
+
// exhaustion: that ratio is bounded by 1 (a sequence cannot pass
|
|
37316
|
+
// its max_value), this one is not. age() tops out near 2.1e9 and
|
|
37317
|
+
// autovacuum_freeze_max_age can legally be set as low as 1e5, so a
|
|
37318
|
+
// ratio of ~21000 is reachable and would overflow a 6-digit
|
|
37319
|
+
// numeric -- turning a wraparound alarm into a numeric field
|
|
37320
|
+
// overflow error at the exact moment it matters. The multixact
|
|
37321
|
+
// ratio fits the same width: mxid_age tops out near 4.3e9 and the
|
|
37322
|
+
// GUC's floor is 10000, so ~430000 is the worst case.
|
|
37323
|
+
//
|
|
37324
|
+
// datminmxid is tracked SEPARATELY from datfrozenxid, and a
|
|
37325
|
+
// database can hit autovacuum_multixact_freeze_max_age with a
|
|
37326
|
+
// perfectly healthy relfrozenxid -- multixacts are burned by
|
|
37327
|
+
// row-level locking (SELECT ... FOR SHARE/UPDATE, FK checks), not
|
|
37328
|
+
// by transaction volume. Checking only the xid side reports a
|
|
37329
|
+
// lock-heavy cluster as clean right up to the multixact shutdown.
|
|
37330
|
+
//
|
|
37331
|
+
// The InvalidMultiXactId guard is `<> '0'::xid` because
|
|
37332
|
+
// pg_database.datminmxid is catalog-typed `xid` even though it
|
|
37333
|
+
// holds a MultiXactId -- there is no `mxid` type to cast to, and
|
|
37334
|
+
// mxid_age('0'::xid) would report a ~4.2-billion age for a
|
|
37335
|
+
// database that has simply never recorded a multixact. It yields
|
|
37336
|
+
// NULL rather than dropping the row: a NULL ratio can never
|
|
37337
|
+
// satisfy `>= $1`, so the row is still returned (and correctly
|
|
37338
|
+
// labelled 'xid') when its xid age is what is dangerous.
|
|
37339
|
+
//
|
|
37340
|
+
// ORDER BY is the GREATER of the two ratios, not age(datfrozenxid):
|
|
37341
|
+
// with the filter now an OR, ordering by the xid age alone would
|
|
37342
|
+
// sort a multixact-critical database near the bottom and let LIMIT
|
|
37343
|
+
// cut the only row that mattered. triggered_by is computed from
|
|
37344
|
+
// the same full-precision ratios the WHERE uses (not the rounded
|
|
37345
|
+
// pct_* columns), so the label can never disagree with the reason
|
|
37346
|
+
// the row came back.
|
|
37347
|
+
`SELECT
|
|
37348
|
+
d.datname AS database,
|
|
37349
|
+
age(d.datfrozenxid) AS xid_age,
|
|
37350
|
+
mx.mxid_age AS mxid_age,
|
|
37351
|
+
r.xid_ratio::numeric(10, 4)::float8 AS pct_of_freeze_max_age,
|
|
37352
|
+
r.mxid_ratio::numeric(10, 4)::float8 AS pct_of_multixact_freeze_max_age,
|
|
37353
|
+
CASE
|
|
37354
|
+
WHEN r.xid_ratio >= $1 AND r.mxid_ratio >= $1 THEN 'both'
|
|
37355
|
+
WHEN r.mxid_ratio >= $1 THEN 'multixact'
|
|
37356
|
+
ELSE 'xid'
|
|
37357
|
+
END AS triggered_by
|
|
37358
|
+
FROM pg_catalog.pg_database d
|
|
37359
|
+
CROSS JOIN LATERAL (
|
|
37360
|
+
SELECT CASE WHEN d.datminmxid <> '0'::xid THEN mxid_age(d.datminmxid) END AS mxid_age
|
|
37361
|
+
) mx
|
|
37362
|
+
CROSS JOIN LATERAL (
|
|
37363
|
+
SELECT
|
|
37364
|
+
(age(d.datfrozenxid)::numeric
|
|
37365
|
+
/ NULLIF(current_setting('autovacuum_freeze_max_age')::numeric, 0)) AS xid_ratio,
|
|
37366
|
+
(mx.mxid_age::numeric
|
|
37367
|
+
/ NULLIF(current_setting('autovacuum_multixact_freeze_max_age')::numeric, 0)) AS mxid_ratio
|
|
37368
|
+
) r
|
|
37369
|
+
WHERE r.xid_ratio >= $1 OR r.mxid_ratio >= $1
|
|
37370
|
+
ORDER BY GREATEST(r.xid_ratio, r.mxid_ratio) DESC
|
|
37371
|
+
LIMIT $2`,
|
|
37372
|
+
[wraparoundThreshold, limit]
|
|
37373
|
+
),
|
|
37374
|
+
run(
|
|
37375
|
+
// Deliberately unfiltered by schema, unlike every other category
|
|
37376
|
+
// in this tool: an anti-wraparound emergency is usually driven by
|
|
37377
|
+
// a TOAST table (pg_toast.*) or a system catalog, and a check that
|
|
37378
|
+
// only looked at user tables would report "nothing wrong" while
|
|
37379
|
+
// the cluster approached a forced shutdown.
|
|
37380
|
+
//
|
|
37381
|
+
// relkind is restricted to relations that actually carry a
|
|
37382
|
+
// frozen-xid horizon: heap ('r'), materialized view ('m'), TOAST
|
|
37383
|
+
// ('t'). Everything else -- views, indexes, partitioned parents
|
|
37384
|
+
// ('p'), sequences -- stores InvalidTransactionId (0) in
|
|
37385
|
+
// relfrozenxid, and age('0'::xid) is a meaningless ~2.1-billion
|
|
37386
|
+
// number that would flood this list with false positives. The
|
|
37387
|
+
// explicit `relfrozenxid <> '0'::xid` guard catches the same case
|
|
37388
|
+
// for any relkind that stops carrying a horizon in a future major.
|
|
37389
|
+
//
|
|
37390
|
+
// freeze_max_age resolves the per-table storage parameter
|
|
37391
|
+
// (`ALTER TABLE ... SET (autovacuum_freeze_max_age = ...)`) and
|
|
37392
|
+
// falls back to the cluster GUC. Without this, a table with a
|
|
37393
|
+
// deliberately raised override reads as critical against the
|
|
37394
|
+
// cluster default, and -- worse -- a table with a LOWERED override
|
|
37395
|
+
// reads as safe when autovacuum is already forcing freezes on it.
|
|
37396
|
+
// pg_options_to_table is strict, so a NULL reloptions yields zero
|
|
37397
|
+
// rows and the scalar subquery returns NULL for COALESCE to
|
|
37398
|
+
// absorb. multixact_freeze_max_age resolves the same way from the
|
|
37399
|
+
// separate `autovacuum_multixact_freeze_max_age` storage parameter
|
|
37400
|
+
// -- a table with a lowered multixact override is already being
|
|
37401
|
+
// force-vacuumed while it still reads as safe against the cluster
|
|
37402
|
+
// default.
|
|
37403
|
+
//
|
|
37404
|
+
// relminmxid is the multixact twin of relfrozenxid and moves
|
|
37405
|
+
// independently of it: row-level locks (SELECT ... FOR
|
|
37406
|
+
// SHARE/UPDATE, FK checks) burn multixact IDs without consuming
|
|
37407
|
+
// xids, so a lock-heavy table can be at 90% of
|
|
37408
|
+
// autovacuum_multixact_freeze_max_age with an age(relfrozenxid) of
|
|
37409
|
+
// nearly zero. Filtering on the xid ratio alone reports that table
|
|
37410
|
+
// as clean.
|
|
37411
|
+
//
|
|
37412
|
+
// The InvalidMultiXactId guard mirrors the relfrozenxid one as
|
|
37413
|
+
// `<> '0'::xid` -- relminmxid is catalog-typed `xid` even though
|
|
37414
|
+
// it holds a MultiXactId, so there is no separate type to cast to,
|
|
37415
|
+
// and mxid_age('0'::xid) would return a meaningless ~4.2-billion
|
|
37416
|
+
// age for any relation that has never recorded a multixact. It is
|
|
37417
|
+
// expressed as a CASE yielding NULL, NOT as another AND in the
|
|
37418
|
+
// WHERE: dropping those rows outright would also drop rows whose
|
|
37419
|
+
// relfrozenxid IS dangerous. A NULL ratio cannot satisfy `>= $1`,
|
|
37420
|
+
// so such a row can only ever be xid-triggered.
|
|
37421
|
+
//
|
|
37422
|
+
// ORDER BY is the GREATER of the two ratios rather than
|
|
37423
|
+
// age(relfrozenxid): now that the filter is an OR, ordering on the
|
|
37424
|
+
// xid age would push a multixact-critical TOAST table below the
|
|
37425
|
+
// LIMIT cut and hide the exact row the operator was paged for.
|
|
37426
|
+
// triggered_by is computed from the same full-precision ratios as
|
|
37427
|
+
// the WHERE (not the rounded pct_* columns), so it can never
|
|
37428
|
+
// disagree with why the row was returned.
|
|
37429
|
+
`SELECT
|
|
37430
|
+
n.nspname AS schema,
|
|
37431
|
+
c.relname AS "table",
|
|
37432
|
+
c.relkind AS relkind,
|
|
37433
|
+
age(c.relfrozenxid) AS xid_age,
|
|
37434
|
+
fma.freeze_max_age AS freeze_max_age,
|
|
37435
|
+
r.xid_ratio::numeric(10, 4)::float8 AS pct_of_freeze_max_age,
|
|
37436
|
+
fma.mxid_age AS mxid_age,
|
|
37437
|
+
fma.multixact_freeze_max_age AS multixact_freeze_max_age,
|
|
37438
|
+
r.mxid_ratio::numeric(10, 4)::float8 AS pct_of_multixact_freeze_max_age,
|
|
37439
|
+
CASE
|
|
37440
|
+
WHEN r.xid_ratio >= $1 AND r.mxid_ratio >= $1 THEN 'both'
|
|
37441
|
+
WHEN r.mxid_ratio >= $1 THEN 'multixact'
|
|
37442
|
+
ELSE 'xid'
|
|
37443
|
+
END AS triggered_by${frozenCoverageCols}
|
|
37444
|
+
FROM pg_catalog.pg_class c
|
|
37445
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
37446
|
+
CROSS JOIN LATERAL (
|
|
37447
|
+
SELECT
|
|
37448
|
+
COALESCE(
|
|
37449
|
+
(SELECT o.option_value::int
|
|
37450
|
+
FROM pg_catalog.pg_options_to_table(c.reloptions) o
|
|
37451
|
+
WHERE o.option_name = 'autovacuum_freeze_max_age'),
|
|
37452
|
+
current_setting('autovacuum_freeze_max_age')::int
|
|
37453
|
+
) AS freeze_max_age,
|
|
37454
|
+
COALESCE(
|
|
37455
|
+
(SELECT o.option_value::int
|
|
37456
|
+
FROM pg_catalog.pg_options_to_table(c.reloptions) o
|
|
37457
|
+
WHERE o.option_name = 'autovacuum_multixact_freeze_max_age'),
|
|
37458
|
+
current_setting('autovacuum_multixact_freeze_max_age')::int
|
|
37459
|
+
) AS multixact_freeze_max_age,
|
|
37460
|
+
CASE WHEN c.relminmxid <> '0'::xid THEN mxid_age(c.relminmxid) END AS mxid_age
|
|
37461
|
+
) fma
|
|
37462
|
+
CROSS JOIN LATERAL (
|
|
37463
|
+
SELECT
|
|
37464
|
+
(age(c.relfrozenxid)::numeric / NULLIF(fma.freeze_max_age, 0)::numeric) AS xid_ratio,
|
|
37465
|
+
(fma.mxid_age::numeric / NULLIF(fma.multixact_freeze_max_age, 0)::numeric) AS mxid_ratio
|
|
37466
|
+
) r
|
|
37467
|
+
WHERE c.relkind IN ('r', 'm', 't')
|
|
37468
|
+
AND c.relfrozenxid <> '0'::xid
|
|
37469
|
+
AND (r.xid_ratio >= $1 OR r.mxid_ratio >= $1)
|
|
37470
|
+
ORDER BY GREATEST(r.xid_ratio, r.mxid_ratio) DESC
|
|
37471
|
+
LIMIT $2`,
|
|
37472
|
+
[wraparoundThreshold, limit]
|
|
37473
|
+
),
|
|
36758
37474
|
run(
|
|
36759
37475
|
// Includes partitioned parents (relkind='p') alongside plain heap
|
|
36760
37476
|
// tables ('r'). A partitioned table with no PK is a real design-
|
|
@@ -36802,12 +37518,32 @@ var adminTools = [
|
|
|
36802
37518
|
]);
|
|
36803
37519
|
const warnings = [];
|
|
36804
37520
|
if (!seqRes.ok) warnings.push(`sequence_exhaustion fetch failed: ${seqRes.error}`);
|
|
37521
|
+
if (!wrapGucRes.ok) {
|
|
37522
|
+
warnings.push(`wraparound_risk.autovacuum_freeze_max_age fetch failed: ${wrapGucRes.error}`);
|
|
37523
|
+
}
|
|
37524
|
+
if (!wrapDbRes.ok) warnings.push(`wraparound_risk.databases fetch failed: ${wrapDbRes.error}`);
|
|
37525
|
+
if (!wrapTblRes.ok) warnings.push(`wraparound_risk.tables fetch failed: ${wrapTblRes.error}`);
|
|
36805
37526
|
if (!noPkRes.ok) warnings.push(`tables_without_primary_key fetch failed: ${noPkRes.error}`);
|
|
36806
37527
|
if (!rlsRes.ok) warnings.push(`public_tables_without_rls fetch failed: ${rlsRes.error}`);
|
|
36807
37528
|
return {
|
|
36808
37529
|
ok: true,
|
|
36809
37530
|
data: {
|
|
36810
37531
|
sequence_exhaustion: seqRes.ok ? seqRes.data : [],
|
|
37532
|
+
wraparound_risk: {
|
|
37533
|
+
// `?? null` rather than leaving it undefined: an absent key
|
|
37534
|
+
// serializes away entirely, so a caller reading an empty
|
|
37535
|
+
// `databases` list would have no way to tell "nothing above the
|
|
37536
|
+
// threshold" from "the divisor was never readable".
|
|
37537
|
+
autovacuum_freeze_max_age: wrapGucRes.ok ? wrapGucRes.data?.[0]?.autovacuum_freeze_max_age ?? null : null,
|
|
37538
|
+
// Same `?? null` reasoning, and it is exposed even though the
|
|
37539
|
+
// two GUCs share a sub-query: a caller that only saw the xid
|
|
37540
|
+
// divisor would have no scale for pct_of_multixact_freeze_max_age
|
|
37541
|
+
// and no way to judge an empty `databases` list on the multixact
|
|
37542
|
+
// axis.
|
|
37543
|
+
autovacuum_multixact_freeze_max_age: wrapGucRes.ok ? wrapGucRes.data?.[0]?.autovacuum_multixact_freeze_max_age ?? null : null,
|
|
37544
|
+
databases: wrapDbRes.ok ? wrapDbRes.data : [],
|
|
37545
|
+
tables: wrapTblRes.ok ? wrapTblRes.data : []
|
|
37546
|
+
},
|
|
36811
37547
|
tables_without_primary_key: noPkRes.ok ? noPkRes.data : [],
|
|
36812
37548
|
public_tables_without_rls: rlsRes.ok ? rlsRes.data : [],
|
|
36813
37549
|
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
@@ -36910,7 +37646,12 @@ var adminTools = [
|
|
|
36910
37646
|
];
|
|
36911
37647
|
|
|
36912
37648
|
// src/tools/explain.ts
|
|
36913
|
-
var
|
|
37649
|
+
var PG12 = 12e4;
|
|
37650
|
+
var PG13 = 13e4;
|
|
37651
|
+
var SERIALIZE_MODES = ["none", "text", "binary"];
|
|
37652
|
+
var serializeMode = external_exports.enum(SERIALIZE_MODES);
|
|
37653
|
+
var INDEX_ACCESS_METHODS = ["btree", "hash", "gin", "gist", "brin", "spgist"];
|
|
37654
|
+
var indexAccessMethod = external_exports.enum(INDEX_ACCESS_METHODS);
|
|
36914
37655
|
var hypotheticalIndex = external_exports.object({
|
|
36915
37656
|
// `table` is `schema.table` or `table`. The 127-char ceiling is a generous
|
|
36916
37657
|
// upper bound on the combined form -- the actual NAMEDATALEN (63-byte)
|
|
@@ -36927,6 +37668,9 @@ function quoteQualifiedTable(name) {
|
|
|
36927
37668
|
return name.split(".").map((p) => quoteIdent(p)).join(".");
|
|
36928
37669
|
}
|
|
36929
37670
|
function validateHypoIndex(idx) {
|
|
37671
|
+
if (idx.using !== void 0 && !INDEX_ACCESS_METHODS.includes(idx.using)) {
|
|
37672
|
+
return `Unsupported \`using\` value ${JSON.stringify(idx.using)}; expected one of: ${INDEX_ACCESS_METHODS.join(", ")}.`;
|
|
37673
|
+
}
|
|
36930
37674
|
if (idx.table.includes('"')) {
|
|
36931
37675
|
return `Hypothetical index table ${JSON.stringify(idx.table)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
36932
37676
|
}
|
|
@@ -36974,7 +37718,7 @@ function buildHypopgHooks(indexes) {
|
|
|
36974
37718
|
var explainTools = [
|
|
36975
37719
|
{
|
|
36976
37720
|
name: "pg_explain",
|
|
36977
|
-
description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE - for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are always rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the mutation. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement). Set `hypothetical_indexes` to a list of `{table, columns, using?}` to ask the planner 'what would the plan be if these indexes existed?' -- requires the HypoPG extension (`CREATE EXTENSION hypopg`). The hypothetical indexes are torn down at the end of the call, never touching real disk.",
|
|
37721
|
+
description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE - for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are always rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the mutation. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement). Planner options (all optional): `buffers` reports shared/local/temp block hits and is the fastest way to tell a bad plan from a cold cache - it defaults to TRUE whenever `analyze` is true (matching PostgreSQL 18, which turns it on for you), pass `buffers: false` to suppress it; requesting it WITHOUT `analyze` needs PostgreSQL 13+. `verbose` adds output columns and schema-qualified names. `settings` (PostgreSQL 12+) lists planner GUCs set away from their defaults - the usual explanation for a plan that looks impossible. `wal` (PostgreSQL 13+) reports WAL generated and `serialize` (`none`|`text`|`binary`, PostgreSQL 17+) charges the cost of building the result rows; both require `analyze`. `memory` (PostgreSQL 17+) reports memory used by the PLANNER, so it works with or without `analyze` - use it alone to ask why planning a statement is expensive. `generic_plan` (PostgreSQL 16+) plans a parameterized statement WITHOUT values for its $1/$2 placeholders and cannot be combined with `analyze` or `params`. `costs` and `timing` default to true (as in postgres); set either to false to drop those columns, and note `timing` only applies with `analyze`. Options that need a newer server than the one connected are rejected with an explicit error naming the required version instead of a confusing parse failure. Set `hypothetical_indexes` to a list of `{table, columns, using?}` to ask the planner 'what would the plan be if these indexes existed?' -- requires the HypoPG extension (`CREATE EXTENSION hypopg`). The hypothetical indexes are torn down at the end of the call, never touching real disk.",
|
|
36978
37722
|
annotations: {
|
|
36979
37723
|
title: "Explain query plan",
|
|
36980
37724
|
readOnlyHint: false,
|
|
@@ -36986,6 +37730,28 @@ var explainTools = [
|
|
|
36986
37730
|
sql: external_exports.string().min(1).max(1e6).describe("The SQL statement to explain. Do NOT prefix with EXPLAIN."),
|
|
36987
37731
|
analyze: external_exports.boolean().default(false).describe("Run EXPLAIN ANALYZE (actually executes the query)."),
|
|
36988
37732
|
format: external_exports.enum(["text", "json"]).default("text").describe("Output format."),
|
|
37733
|
+
// Deliberately NOT `.default(false)`: the effective default is "on when
|
|
37734
|
+
// analyzing", which Zod cannot express in terms of a sibling field. An
|
|
37735
|
+
// absent value means "follow analyze"; an explicit false still wins.
|
|
37736
|
+
buffers: external_exports.boolean().optional().describe(
|
|
37737
|
+
"Report buffer hits/reads/dirtied. Defaults to TRUE when `analyze` is true (PostgreSQL 18 does the same); pass false to suppress. Requesting it without `analyze` requires PostgreSQL 13+."
|
|
37738
|
+
),
|
|
37739
|
+
verbose: external_exports.boolean().default(false).describe("Include output columns, schema-qualified names, and triggers."),
|
|
37740
|
+
settings: external_exports.boolean().default(false).describe("Report planner GUCs set away from their defaults - explains a weird plan (PostgreSQL 12+)."),
|
|
37741
|
+
wal: external_exports.boolean().default(false).describe("Report WAL generated by the statement. Requires `analyze` (PostgreSQL 13+)."),
|
|
37742
|
+
memory: external_exports.boolean().default(false).describe(
|
|
37743
|
+
"Report memory used by the planner (PostgreSQL 17+). Works with or without `analyze`, since planning happens either way."
|
|
37744
|
+
),
|
|
37745
|
+
serialize: serializeMode.optional().describe(
|
|
37746
|
+
"Charge the cost of serializing result rows (network-bound queries hide it otherwise). Requires `analyze` (PostgreSQL 17+)."
|
|
37747
|
+
),
|
|
37748
|
+
generic_plan: external_exports.boolean().default(false).describe(
|
|
37749
|
+
"Plan the statement with UNKNOWN values for its $1/$2 placeholders - the plan a prepared statement would get. Cannot be combined with `analyze` or `params` (PostgreSQL 16+)."
|
|
37750
|
+
),
|
|
37751
|
+
costs: external_exports.boolean().default(true).describe("Include estimated cost/rows/width. Set false for a terser plan."),
|
|
37752
|
+
timing: external_exports.boolean().default(true).describe(
|
|
37753
|
+
"Include per-node actual timing. Setting it to false REQUIRES `analyze: true` (it is rejected otherwise, not silently ignored); false lowers measurement overhead."
|
|
37754
|
+
),
|
|
36989
37755
|
params: paramsArray.optional().describe("Positional parameters referenced as $1, $2, ... in the SQL."),
|
|
36990
37756
|
hypothetical_indexes: external_exports.array(hypotheticalIndex).optional().describe(
|
|
36991
37757
|
"List of indexes the planner should pretend exist for this EXPLAIN. Requires the HypoPG extension. Indexes are session-scoped and reset at the end of the call."
|
|
@@ -36996,6 +37762,15 @@ var explainTools = [
|
|
|
36996
37762
|
sql,
|
|
36997
37763
|
analyze = false,
|
|
36998
37764
|
format = "text",
|
|
37765
|
+
buffers,
|
|
37766
|
+
verbose = false,
|
|
37767
|
+
settings = false,
|
|
37768
|
+
wal = false,
|
|
37769
|
+
memory = false,
|
|
37770
|
+
serialize,
|
|
37771
|
+
generic_plan = false,
|
|
37772
|
+
costs = true,
|
|
37773
|
+
timing = true,
|
|
36999
37774
|
params,
|
|
37000
37775
|
hypothetical_indexes
|
|
37001
37776
|
} = input;
|
|
@@ -37005,15 +37780,74 @@ var explainTools = [
|
|
|
37005
37780
|
error: "The `sql` parameter should be the query to explain, not an EXPLAIN statement. Use the `analyze` and `format` parameters on this tool instead of prefixing the SQL."
|
|
37006
37781
|
};
|
|
37007
37782
|
}
|
|
37008
|
-
|
|
37009
|
-
|
|
37010
|
-
|
|
37011
|
-
|
|
37783
|
+
if (serialize !== void 0 && !SERIALIZE_MODES.includes(serialize)) {
|
|
37784
|
+
return {
|
|
37785
|
+
ok: false,
|
|
37786
|
+
error: `Unsupported \`serialize\` value ${JSON.stringify(serialize)}; expected one of: ${SERIALIZE_MODES.join(", ")}.`
|
|
37787
|
+
};
|
|
37788
|
+
}
|
|
37789
|
+
const wantBuffers = buffers ?? analyze;
|
|
37790
|
+
const analyzeOnly = [];
|
|
37791
|
+
if (!analyze) {
|
|
37792
|
+
if (wal) analyzeOnly.push("wal");
|
|
37793
|
+
if (serialize !== void 0) analyzeOnly.push("serialize");
|
|
37794
|
+
if (!timing) analyzeOnly.push("timing");
|
|
37795
|
+
}
|
|
37796
|
+
if (analyzeOnly.length > 0) {
|
|
37797
|
+
const named = analyzeOnly.map((o) => `\`${o}\``).join(", ");
|
|
37798
|
+
return {
|
|
37799
|
+
ok: false,
|
|
37800
|
+
error: `These EXPLAIN options only apply with \`analyze: true\`: ${named}. Set \`analyze: true\` (the statement is executed, and any write is rolled back) or drop them.`
|
|
37801
|
+
};
|
|
37802
|
+
}
|
|
37803
|
+
if (generic_plan && analyze) {
|
|
37804
|
+
return {
|
|
37805
|
+
ok: false,
|
|
37806
|
+
error: "`generic_plan` and `analyze` cannot be combined: GENERIC_PLAN plans the statement without parameter values, while ANALYZE has to execute it with real ones. Pick one."
|
|
37807
|
+
};
|
|
37808
|
+
}
|
|
37809
|
+
if (generic_plan && (params?.length ?? 0) > 0) {
|
|
37810
|
+
return {
|
|
37811
|
+
ok: false,
|
|
37812
|
+
error: "`generic_plan` plans the statement WITHOUT parameter values -- drop `params` (leave the $1/$2 placeholders in the SQL), or drop `generic_plan` and pass the values."
|
|
37813
|
+
};
|
|
37814
|
+
}
|
|
37012
37815
|
const hypoIndexes = hypothetical_indexes ?? [];
|
|
37013
37816
|
for (const idx of hypoIndexes) {
|
|
37014
37817
|
const err = validateHypoIndex(idx);
|
|
37015
37818
|
if (err) return { ok: false, error: err };
|
|
37016
37819
|
}
|
|
37820
|
+
const gated = [];
|
|
37821
|
+
if (settings) gated.push({ option: "settings", min: PG12, since: "12" });
|
|
37822
|
+
if (wal) gated.push({ option: "wal", min: PG13, since: "13" });
|
|
37823
|
+
if (generic_plan) gated.push({ option: "generic_plan", min: PG16, since: "16" });
|
|
37824
|
+
if (memory) gated.push({ option: "memory", min: PG17, since: "17" });
|
|
37825
|
+
if (serialize !== void 0) gated.push({ option: "serialize", min: PG17, since: "17" });
|
|
37826
|
+
if (wantBuffers && !analyze) gated.push({ option: "buffers without analyze", min: PG13, since: "13" });
|
|
37827
|
+
if (gated.length > 0) {
|
|
37828
|
+
const serverVersion = await getServerVersionNum();
|
|
37829
|
+
const unsupported = gated.filter((g) => serverVersion < g.min);
|
|
37830
|
+
if (unsupported.length > 0) {
|
|
37831
|
+
const server2 = serverVersion === 0 ? "the server version could not be determined, so the oldest supported behavior is assumed" : `this server reports PostgreSQL ${Math.floor(serverVersion / 1e4)}`;
|
|
37832
|
+
return {
|
|
37833
|
+
ok: false,
|
|
37834
|
+
error: `Unsupported EXPLAIN option for this server: ${unsupported.map((g) => `\`${g.option}\` requires PostgreSQL ${g.since}+`).join("; ")} (${server2}). Drop the option and re-run.`
|
|
37835
|
+
};
|
|
37836
|
+
}
|
|
37837
|
+
}
|
|
37838
|
+
const flags = [];
|
|
37839
|
+
if (analyze) flags.push("ANALYZE");
|
|
37840
|
+
if (wantBuffers) flags.push("BUFFERS");
|
|
37841
|
+
if (verbose) flags.push("VERBOSE");
|
|
37842
|
+
if (settings) flags.push("SETTINGS");
|
|
37843
|
+
if (wal) flags.push("WAL");
|
|
37844
|
+
if (memory) flags.push("MEMORY");
|
|
37845
|
+
if (generic_plan) flags.push("GENERIC_PLAN");
|
|
37846
|
+
if (!costs) flags.push("COSTS OFF");
|
|
37847
|
+
if (!timing) flags.push("TIMING OFF");
|
|
37848
|
+
if (serialize !== void 0) flags.push(`SERIALIZE ${serialize.toUpperCase()}`);
|
|
37849
|
+
if (format === "json") flags.push("FORMAT JSON");
|
|
37850
|
+
const explainSql = flags.length > 0 ? `EXPLAIN (${flags.join(", ")}) ${sql}` : `EXPLAIN ${sql}`;
|
|
37017
37851
|
const hooks = hypoIndexes.length > 0 ? buildHypopgHooks(hypoIndexes) : {};
|
|
37018
37852
|
if (hypoIndexes.length > 0) {
|
|
37019
37853
|
const check2 = await runInternal(
|
|
@@ -37053,7 +37887,7 @@ var explainTools = [
|
|
|
37053
37887
|
var healthTools = [
|
|
37054
37888
|
{
|
|
37055
37889
|
name: "pg_health",
|
|
37056
|
-
description: "Quick health snapshot: server version, database size, connection
|
|
37890
|
+
description: "Quick health snapshot: server version, database size, connection counts measured against `max_connections`, active queries with their wait events, a pg_stat_database rollup, and table count. Useful as a connection sanity check and to spot runaway queries, connection-cap pressure, and lock/IO waits.\n- connections: `total` for the CURRENT database, broken down into `active` / `idle` / `idle_in_transaction` / `idle_in_transaction_aborted` / `other` (starting, fastpath function call, disabled) / `state_unavailable` -- those six sum to `total`. `idle_in_transaction_aborted` is called out separately because it holds locks and blocks vacuum while doing no work and will never commit. `state_unavailable` counts sessions whose `state` reads NULL because the role lacks pg_read_all_stats / pg_monitor membership; a non-zero value means every other bucket is under-counted by at least that much, so do NOT read `active: 0` next to it as an idle database. Plus `cluster_client_backends` (client backends across ALL databases -- those are what actually consume connection slots), `max_connections`, `superuser_reserved_connections`, and `used_fraction` (cluster_client_backends / max_connections). A raw connection count means nothing without the cap; read `used_fraction` first.\n- active_queries: `pid`, `state`, `query`, `application_name`, `backend_type`, `wait_event_type` / `wait_event` (both NULL when the backend is running rather than waiting -- the single most diagnostic pair in pg_stat_activity), `duration_seconds` (since query_start) and `transaction_age_seconds` (since xact_start). A large transaction_age_seconds next to a small duration_seconds is a long-open transaction, the usual root cause behind lock waits, bloat, and stalled autovacuum.\n- database_stats: pg_stat_database for the current database -- `deadlocks`, `temp_files` / `temp_bytes` (work_mem spills), `conflicts` (recovery conflicts, only ever non-zero on a replica), `blks_hit` / `blks_read` / `cache_hit_ratio`, and `stats_reset`. Every counter is CUMULATIVE since stats_reset, not a rate -- interpret them against that timestamp.\nSub-queries that fail (several of these are permission-gated on managed providers) append to `_warnings` and leave their field null; the rest of the snapshot still returns.",
|
|
37057
37891
|
annotations: {
|
|
37058
37892
|
title: "Database health snapshot",
|
|
37059
37893
|
readOnlyHint: true,
|
|
@@ -37067,7 +37901,7 @@ var healthTools = [
|
|
|
37067
37901
|
handler: async (input) => {
|
|
37068
37902
|
const { activeQueryLimit = 10 } = input;
|
|
37069
37903
|
return withSharedClient(async (run) => {
|
|
37070
|
-
const [versionRes, sizeRes, connsRes, activeRes, tableCountRes] = await Promise.all([
|
|
37904
|
+
const [versionRes, sizeRes, connsRes, activeRes, dbStatsRes, tableCountRes] = await Promise.all([
|
|
37071
37905
|
run(`SELECT version() AS version`),
|
|
37072
37906
|
run(
|
|
37073
37907
|
`SELECT
|
|
@@ -37076,19 +37910,97 @@ var healthTools = [
|
|
|
37076
37910
|
pg_database_size(current_database())::text AS size_bytes`
|
|
37077
37911
|
),
|
|
37078
37912
|
run(
|
|
37913
|
+
// The per-database counters keep their original meaning: the
|
|
37914
|
+
// old `WHERE datname = current_database()` moved into a FILTER on
|
|
37915
|
+
// each aggregate so the same statement can also count backends in
|
|
37916
|
+
// OTHER databases. Without that, a connection-cap check would
|
|
37917
|
+
// compare one database's session count against a cluster-wide
|
|
37918
|
+
// limit and under-report pressure on a multi-tenant server.
|
|
37919
|
+
//
|
|
37920
|
+
// used_fraction divides by max_connections using
|
|
37921
|
+
// `backend_type = 'client backend'`, not count(*): autovacuum
|
|
37922
|
+
// workers, background workers, walsenders and the checkpointer all
|
|
37923
|
+
// appear in pg_stat_activity but draw on their OWN process limits,
|
|
37924
|
+
// so counting them would inflate the fraction past 1.0 on an idle
|
|
37925
|
+
// server. The effective ceiling for a non-superuser is lower still
|
|
37926
|
+
// -- `max_connections - superuser_reserved_connections` (minus
|
|
37927
|
+
// reserved_connections on PG16+) -- which is why the reserve is
|
|
37928
|
+
// reported alongside rather than folded into the fraction.
|
|
37929
|
+
//
|
|
37930
|
+
// Visibility caveat: pg_stat_activity does NOT hide other users'
|
|
37931
|
+
// rows from an ordinary role. The row is still there -- existence,
|
|
37932
|
+
// datname and session user are visible to everyone -- but `state`
|
|
37933
|
+
// (along with query, wait_event, xact_start, ...) comes back NULL
|
|
37934
|
+
// unless the caller owns the session or holds pg_read_all_stats /
|
|
37935
|
+
// pg_monitor. So `total` and cluster_client_backends, which key off
|
|
37936
|
+
// datname / backend_type, are COMPLETE for any role, while the
|
|
37937
|
+
// state buckets below count only the caller's own sessions. Left
|
|
37938
|
+
// unmarked that renders as a confident `active: 0` on a busy
|
|
37939
|
+
// database, which reads as "nothing is running" rather than "you
|
|
37940
|
+
// cannot see it": state_unavailable counts the NULL-state rows so
|
|
37941
|
+
// the shortfall is visible. It is a visibility gap on a successful
|
|
37942
|
+
// query, not a failure, so it cannot be surfaced via _warnings.
|
|
37943
|
+
//
|
|
37944
|
+
// The buckets are meant to RECONCILE: every current-database row
|
|
37945
|
+
// lands in exactly one of active / idle / idle_in_transaction /
|
|
37946
|
+
// idle_in_transaction_aborted / other / state_unavailable, so those
|
|
37947
|
+
// six sum to `total`. Keep that invariant when editing -- it is the
|
|
37948
|
+
// only thing that makes a shortfall detectable.
|
|
37949
|
+
//
|
|
37950
|
+
// `other` is a NOT IN over the named buckets rather than one
|
|
37951
|
+
// counter per remaining documented state (starting, fastpath
|
|
37952
|
+
// function call, disabled): those are rare enough not to earn a
|
|
37953
|
+
// field, and a catch-all also absorbs any state a future major
|
|
37954
|
+
// adds, which an equality list would silently drop out of the sum.
|
|
37955
|
+
// 'idle in transaction (aborted)' does get its own field -- it is
|
|
37956
|
+
// an exact-equality state, not a prefix of 'idle in transaction',
|
|
37957
|
+
// and it is diagnostically distinct: it holds locks and blocks
|
|
37958
|
+
// vacuum while doing no work and can never commit.
|
|
37079
37959
|
`SELECT
|
|
37080
|
-
count(*)::text AS total,
|
|
37081
|
-
count(*) FILTER (WHERE state = 'active')::text AS active,
|
|
37082
|
-
count(*) FILTER (WHERE state = 'idle')::text AS idle,
|
|
37083
|
-
count(*) FILTER (WHERE state = 'idle in transaction')::text AS idle_in_transaction
|
|
37084
|
-
|
|
37085
|
-
|
|
37960
|
+
count(*) FILTER (WHERE datname = current_database())::text AS total,
|
|
37961
|
+
count(*) FILTER (WHERE datname = current_database() AND state = 'active')::text AS active,
|
|
37962
|
+
count(*) FILTER (WHERE datname = current_database() AND state = 'idle')::text AS idle,
|
|
37963
|
+
count(*) FILTER (WHERE datname = current_database() AND state = 'idle in transaction')::text AS idle_in_transaction,
|
|
37964
|
+
count(*) FILTER (WHERE datname = current_database() AND state = 'idle in transaction (aborted)')::text AS idle_in_transaction_aborted,
|
|
37965
|
+
count(*) FILTER (WHERE datname = current_database()
|
|
37966
|
+
AND state IS NOT NULL
|
|
37967
|
+
AND state NOT IN ('active', 'idle', 'idle in transaction', 'idle in transaction (aborted)'))::text AS other,
|
|
37968
|
+
count(*) FILTER (WHERE datname = current_database() AND state IS NULL)::text AS state_unavailable,
|
|
37969
|
+
count(*) FILTER (WHERE backend_type = 'client backend')::text AS cluster_client_backends,
|
|
37970
|
+
current_setting('max_connections')::int AS max_connections,
|
|
37971
|
+
current_setting('superuser_reserved_connections')::int AS superuser_reserved_connections,
|
|
37972
|
+
(count(*) FILTER (WHERE backend_type = 'client backend')::numeric
|
|
37973
|
+
/ NULLIF(current_setting('max_connections')::numeric, 0))::numeric(6, 4)::float8 AS used_fraction
|
|
37974
|
+
FROM pg_stat_activity`
|
|
37086
37975
|
),
|
|
37087
37976
|
run(
|
|
37977
|
+
// wait_event_type / wait_event are the diagnostic pair here: they
|
|
37978
|
+
// say WHY a backend is not making progress (Lock, LWLock, IO,
|
|
37979
|
+
// Client, ...) instead of leaving the caller to infer it from the
|
|
37980
|
+
// query text. Both are NULL when the backend is actually running.
|
|
37981
|
+
//
|
|
37982
|
+
// The values are passed straight through, never filtered or
|
|
37983
|
+
// switched on: postgres renames these literals between majors
|
|
37984
|
+
// (PG19 renames the `BUFFERPIN` wait event type to `BUFFER`), so
|
|
37985
|
+
// any `WHERE wait_event_type = '...'` or CASE over them here would
|
|
37986
|
+
// silently stop matching on a newer server. Interpretation belongs
|
|
37987
|
+
// to the caller, which can see the server version.
|
|
37988
|
+
//
|
|
37989
|
+
// transaction_age_seconds comes from xact_start, not query_start:
|
|
37990
|
+
// a backend that has been idle-in-transaction for an hour shows a
|
|
37991
|
+
// short (or NULL) query duration while holding snapshots and locks
|
|
37992
|
+
// the whole time. That gap is the tell for the long-open
|
|
37993
|
+
// transaction behind most of the rest of this snapshot. xact_start
|
|
37994
|
+
// is NULL for backends not in a transaction block.
|
|
37088
37995
|
`SELECT
|
|
37089
37996
|
pid,
|
|
37090
37997
|
state,
|
|
37091
37998
|
EXTRACT(EPOCH FROM (now() - query_start))::numeric(10, 2)::float8 AS duration_seconds,
|
|
37999
|
+
EXTRACT(EPOCH FROM (now() - xact_start))::numeric(10, 2)::float8 AS transaction_age_seconds,
|
|
38000
|
+
xact_start::text AS xact_start,
|
|
38001
|
+
wait_event_type,
|
|
38002
|
+
wait_event,
|
|
38003
|
+
backend_type,
|
|
37092
38004
|
query,
|
|
37093
38005
|
application_name
|
|
37094
38006
|
FROM pg_stat_activity
|
|
@@ -37100,6 +38012,32 @@ var healthTools = [
|
|
|
37100
38012
|
LIMIT $1`,
|
|
37101
38013
|
[activeQueryLimit]
|
|
37102
38014
|
),
|
|
38015
|
+
run(
|
|
38016
|
+
// Every column here is a bigint counter that has been accumulating
|
|
38017
|
+
// since stats_reset, so stats_reset ships with them -- "12
|
|
38018
|
+
// deadlocks" is alarming since yesterday and unremarkable since
|
|
38019
|
+
// 2019. stats_reset is NULL when the stats have never been reset.
|
|
38020
|
+
//
|
|
38021
|
+
// ::text on the counters: they are bigint, and node-pg would hand
|
|
38022
|
+
// back a JS number that silently loses precision past 2^53 on a
|
|
38023
|
+
// long-lived cluster's temp_bytes.
|
|
38024
|
+
//
|
|
38025
|
+
// cache_hit_ratio divides in numeric rather than float8 for the
|
|
38026
|
+
// same reason, and NULLIF guards the freshly-reset case where both
|
|
38027
|
+
// blks_hit and blks_read are 0 (division by zero, not a 0% ratio).
|
|
38028
|
+
`SELECT
|
|
38029
|
+
deadlocks::text AS deadlocks,
|
|
38030
|
+
temp_files::text AS temp_files,
|
|
38031
|
+
temp_bytes::text AS temp_bytes,
|
|
38032
|
+
pg_size_pretty(temp_bytes) AS temp_bytes_pretty,
|
|
38033
|
+
conflicts::text AS conflicts,
|
|
38034
|
+
blks_hit::text AS blks_hit,
|
|
38035
|
+
blks_read::text AS blks_read,
|
|
38036
|
+
(blks_hit::numeric / NULLIF(blks_hit + blks_read, 0))::numeric(6, 4)::float8 AS cache_hit_ratio,
|
|
38037
|
+
stats_reset::text AS stats_reset
|
|
38038
|
+
FROM pg_catalog.pg_stat_database
|
|
38039
|
+
WHERE datname = current_database()`
|
|
38040
|
+
),
|
|
37103
38041
|
run(
|
|
37104
38042
|
`SELECT count(*)::text AS count
|
|
37105
38043
|
FROM pg_catalog.pg_class c
|
|
@@ -37118,6 +38056,7 @@ var healthTools = [
|
|
|
37118
38056
|
if (!sizeRes.ok) warnings.push(`database fetch failed: ${sizeRes.error}`);
|
|
37119
38057
|
if (!connsRes.ok) warnings.push(`connections fetch failed: ${connsRes.error}`);
|
|
37120
38058
|
if (!activeRes.ok) warnings.push(`active_queries fetch failed: ${activeRes.error}`);
|
|
38059
|
+
if (!dbStatsRes.ok) warnings.push(`database_stats fetch failed: ${dbStatsRes.error}`);
|
|
37121
38060
|
if (!tableCountRes.ok) warnings.push(`table_count fetch failed: ${tableCountRes.error}`);
|
|
37122
38061
|
return {
|
|
37123
38062
|
ok: true,
|
|
@@ -37127,6 +38066,12 @@ var healthTools = [
|
|
|
37127
38066
|
database: sizeRes.ok ? sizeRes.data?.[0] : null,
|
|
37128
38067
|
connections: connsRes.ok ? connsRes.data?.[0] : null,
|
|
37129
38068
|
active_queries: activeRes.ok ? activeRes.data : [],
|
|
38069
|
+
// `?? null` matters here and not on the sibling single-row
|
|
38070
|
+
// lookups: pg_stat_database has no row for a database the role
|
|
38071
|
+
// cannot see, so a successful query can legitimately return zero
|
|
38072
|
+
// rows. Normalize that to null instead of undefined, which JSON
|
|
38073
|
+
// serialization would drop from the response entirely.
|
|
38074
|
+
database_stats: dbStatsRes.ok ? dbStatsRes.data?.[0] ?? null : null,
|
|
37130
38075
|
table_count: tableCountRes.ok ? tableCountRes.data?.[0]?.count : null,
|
|
37131
38076
|
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
37132
38077
|
}
|
|
@@ -37136,6 +38081,176 @@ var healthTools = [
|
|
|
37136
38081
|
}
|
|
37137
38082
|
];
|
|
37138
38083
|
|
|
38084
|
+
// src/tools/io.ts
|
|
38085
|
+
var ioTools = [
|
|
38086
|
+
{
|
|
38087
|
+
name: "pg_io_stats",
|
|
38088
|
+
description: "I/O observability: cumulative per-backend-type I/O from `pg_stat_io` (PostgreSQL 16+), plus in-flight asynchronous I/O handles from `pg_aios` (PostgreSQL 18+). This is the layer underneath `pg_top_queries` and `pg_health` -- it says WHICH subsystem is doing the I/O (client backends vs autovacuum vs checkpointer vs walwriter) and through which path, which a per-query or per-table view cannot.\n- io: one row per (`backend_type`, `io_object`, `io_context`) combination. Counters `reads` / `writes` / `extends` / `writebacks` / `hits` / `evictions` / `reuses` / `fsyncs` are bigints returned as decimal strings; `read_time_ms` / `write_time_ms` / `writeback_time_ms` / `extend_time_ms` / `fsync_time_ms` are float8 milliseconds. A timing of 0 next to a non-zero op count means `track_io_timing` is off, NOT that the I/O was free -- turn it on to get real numbers. A NULL counter means the operation is not possible for that combination, which is different from 0.\n- io[].read_bytes / write_bytes / extend_bytes: a normalized byte figure that means the same thing on every supported server. On PG16-17 it is computed as `op_bytes * <op count>`; on PG18 `op_bytes` was removed and the server reports bytes directly. The top-level `byte_accounting` field says which source produced the numbers.\n- io[].stats_reset: these are CUMULATIVE counters, so a row is only interpretable next to its reset point. Reported per row because that is how the view reports it; `pg_stat_reset_shared('io')` resets them together in practice, but this tool does not assert that.\n- Rows whose counters are all zero are omitted by default (`pg_stat_io` is mostly zeros on a quiet system, and the noise buries the handful of rows that matter). Pass `includeZeroRows: true` for the full matrix.\n- in_flight + io_method: PostgreSQL 18+ ONLY, and both keys are ABSENT on older servers rather than empty/null -- an empty `in_flight` array would read as 'nothing is stalled' when the truth is 'this server cannot tell you'. `in_flight` is live, currently-outstanding async I/O (`pid`, `io_id`, `op`, `state`, `off`, `length`, `target_desc`), which is what you want while a stall is happening rather than after it. `io_method` (`worker` / `io_uring` / `sync`) explains what `in_flight` can contain: with `io_method = sync` there is no asynchronous submission, so the array is legitimately empty no matter how much I/O is running.\nRequires PostgreSQL 16+. Sub-queries that fail (`pg_stat_io` and `pg_aios` are permission-gated on some managed providers) append to `_warnings` and set their field to null; the rest of the response still returns.",
|
|
38089
|
+
annotations: {
|
|
38090
|
+
title: "I/O statistics and in-flight async I/O",
|
|
38091
|
+
readOnlyHint: true,
|
|
38092
|
+
destructiveHint: false,
|
|
38093
|
+
idempotentHint: true,
|
|
38094
|
+
openWorldHint: true
|
|
38095
|
+
},
|
|
38096
|
+
inputSchema: external_exports.object({
|
|
38097
|
+
includeZeroRows: external_exports.boolean().default(false).describe(
|
|
38098
|
+
"If true, return every (backend_type, io_object, io_context) row including the ones with no recorded activity. Default false -- the view is mostly zeros on a quiet system."
|
|
38099
|
+
),
|
|
38100
|
+
limit: external_exports.number().int().min(1).max(1e3).default(200).describe(
|
|
38101
|
+
"Max rows per section (default 200). pg_stat_io has well under 200 combinations, so this effectively bounds the in-flight list on a busy PG18 server."
|
|
38102
|
+
)
|
|
38103
|
+
}),
|
|
38104
|
+
handler: async (input) => {
|
|
38105
|
+
const { includeZeroRows = false, limit = 200 } = input;
|
|
38106
|
+
const serverVersion = await getServerVersionNum();
|
|
38107
|
+
if (serverVersion < PG16) {
|
|
38108
|
+
const detected = serverVersion > 0 ? `this server reports server_version_num=${serverVersion}` : "this server's version could not be determined (the `server_version_num` probe failed, so the oldest supported behaviour is assumed) -- retry once before concluding the server is too old";
|
|
38109
|
+
return {
|
|
38110
|
+
ok: false,
|
|
38111
|
+
error: `pg_io_stats requires PostgreSQL 16 or newer: ${detected}. Unlike a missing extension there is nothing to install -- \`pg_stat_io\` was added in PostgreSQL 16 and has no backport. Until the server is upgraded, use \`pg_health\` (database_stats.blks_hit / blks_read) for the cluster-wide cache picture and \`pg_top_queries\` (io_read_time_ms / io_write_time_ms, needs pg_stat_statements >= 1.10 and track_io_timing = on) for per-query I/O.`
|
|
38112
|
+
};
|
|
38113
|
+
}
|
|
38114
|
+
const isPg18 = serverVersion >= PG18;
|
|
38115
|
+
const byteCols = isPg18 ? `read_bytes::text AS read_bytes,
|
|
38116
|
+
write_bytes::text AS write_bytes,
|
|
38117
|
+
extend_bytes::text AS extend_bytes,` : `(reads::numeric * op_bytes)::text AS read_bytes,
|
|
38118
|
+
(writes::numeric * op_bytes)::text AS write_bytes,
|
|
38119
|
+
(extends::numeric * op_bytes)::text AS extend_bytes,`;
|
|
38120
|
+
const byteAccounting = isPg18 ? "native read_bytes / write_bytes / extend_bytes columns (PG18+)" : "op_bytes * operation count (PG16-17; op_bytes was removed in PG18)";
|
|
38121
|
+
const zeroFilter = includeZeroRows ? "" : `WHERE (COALESCE(reads, 0) + COALESCE(writes, 0) + COALESCE(extends, 0)
|
|
38122
|
+
+ COALESCE(writebacks, 0) + COALESCE(hits, 0) + COALESCE(evictions, 0)
|
|
38123
|
+
+ COALESCE(reuses, 0) + COALESCE(fsyncs, 0)) > 0`;
|
|
38124
|
+
return withSharedClient(async (run) => {
|
|
38125
|
+
const [ioRes, aiosRes, methodRes] = await Promise.all([
|
|
38126
|
+
run(
|
|
38127
|
+
// `object` and `context` are quoted and renamed: both are keywords
|
|
38128
|
+
// (non-reserved, so bare use happens to parse today) and both are
|
|
38129
|
+
// far more legible to a caller as io_object / io_context, matching
|
|
38130
|
+
// how the postgres docs name the underlying enum types.
|
|
38131
|
+
//
|
|
38132
|
+
// Timings are NOT NULLIF'd to hide zeros, unlike pg_top_queries.
|
|
38133
|
+
// There, 0 was ambiguous between "timing off" and "no IO". Here the
|
|
38134
|
+
// op counters sit on the same row, so `read_time_ms: 0` next to
|
|
38135
|
+
// `reads: "48213"` is an unambiguous, actionable signal that
|
|
38136
|
+
// track_io_timing is off -- nulling it would destroy that.
|
|
38137
|
+
//
|
|
38138
|
+
// ORDER BY qualifies every counter with the table name to dodge the
|
|
38139
|
+
// alias-shadowing trap documented in pg_top_queries: the SELECT
|
|
38140
|
+
// aliases `reads::text AS reads`, and postgres resolves a BARE
|
|
38141
|
+
// output name in ORDER BY to the output alias first, which would
|
|
38142
|
+
// sort text ("9" ahead of "10"). Qualified names always route to
|
|
38143
|
+
// the source bigint column.
|
|
38144
|
+
`SELECT
|
|
38145
|
+
backend_type,
|
|
38146
|
+
"object" AS io_object,
|
|
38147
|
+
context AS io_context,
|
|
38148
|
+
reads::text AS reads,
|
|
38149
|
+
${byteCols}
|
|
38150
|
+
read_time::numeric(18, 2)::float8 AS read_time_ms,
|
|
38151
|
+
writes::text AS writes,
|
|
38152
|
+
write_time::numeric(18, 2)::float8 AS write_time_ms,
|
|
38153
|
+
writebacks::text AS writebacks,
|
|
38154
|
+
writeback_time::numeric(18, 2)::float8 AS writeback_time_ms,
|
|
38155
|
+
extends::text AS extends,
|
|
38156
|
+
extend_time::numeric(18, 2)::float8 AS extend_time_ms,
|
|
38157
|
+
hits::text AS hits,
|
|
38158
|
+
evictions::text AS evictions,
|
|
38159
|
+
reuses::text AS reuses,
|
|
38160
|
+
fsyncs::text AS fsyncs,
|
|
38161
|
+
fsync_time::numeric(18, 2)::float8 AS fsync_time_ms,
|
|
38162
|
+
stats_reset::text AS stats_reset
|
|
38163
|
+
FROM pg_catalog.pg_stat_io
|
|
38164
|
+
${zeroFilter}
|
|
38165
|
+
ORDER BY (COALESCE(pg_stat_io.reads, 0) + COALESCE(pg_stat_io.writes, 0)
|
|
38166
|
+
+ COALESCE(pg_stat_io.extends, 0) + COALESCE(pg_stat_io.hits, 0)) DESC,
|
|
38167
|
+
backend_type, "object", context
|
|
38168
|
+
LIMIT $1`,
|
|
38169
|
+
[limit]
|
|
38170
|
+
),
|
|
38171
|
+
// pg_aios does not exist before PG18 -- naming it there is a 42P01
|
|
38172
|
+
// that would take the whole tool down with it, so the query is not
|
|
38173
|
+
// issued at all rather than issued and caught. Promise.resolve(null)
|
|
38174
|
+
// keeps the Promise.all destructure positional on every version.
|
|
38175
|
+
isPg18 ? run(
|
|
38176
|
+
// These are handles outstanding RIGHT NOW, so there is no
|
|
38177
|
+
// cumulative counter to compare against and no stats_reset to
|
|
38178
|
+
// interpret them by -- the value is entirely in the snapshot.
|
|
38179
|
+
// Ordered by pid so a caller can line rows up against the
|
|
38180
|
+
// pg_health / pg_inspect_locks output for the same backend,
|
|
38181
|
+
// which is the actual diagnostic move: "pid 4821 is blocked ->
|
|
38182
|
+
// pid 4821 has 12 reads outstanding against this relation".
|
|
38183
|
+
//
|
|
38184
|
+
// `off` is quoted because OFF is a postgres keyword; the view's
|
|
38185
|
+
// own column is spelled that way, so it has to be quoted on the
|
|
38186
|
+
// input side rather than renamed away.
|
|
38187
|
+
`SELECT
|
|
38188
|
+
pid,
|
|
38189
|
+
io_id,
|
|
38190
|
+
operation AS op,
|
|
38191
|
+
state,
|
|
38192
|
+
"off"::text AS "off",
|
|
38193
|
+
length::text AS length,
|
|
38194
|
+
target_desc
|
|
38195
|
+
FROM pg_catalog.pg_aios
|
|
38196
|
+
ORDER BY pid, io_id
|
|
38197
|
+
LIMIT $1`,
|
|
38198
|
+
[limit]
|
|
38199
|
+
) : Promise.resolve(null),
|
|
38200
|
+
// `io_method` is a PG18 GUC. The two-argument form of
|
|
38201
|
+
// current_setting() returns NULL for an unknown setting instead of
|
|
38202
|
+
// raising 42704, which keeps this from becoming a hard failure if a
|
|
38203
|
+
// build reports >= 180000 without the parameter (an alternative
|
|
38204
|
+
// distribution, a future rename). Issued separately from pg_aios on
|
|
38205
|
+
// purpose: pg_aios is the permission-gated one, and losing it must
|
|
38206
|
+
// not also lose the setting that explains what it would have held.
|
|
38207
|
+
isPg18 ? run(`SELECT current_setting('io_method', true) AS io_method`) : Promise.resolve(null)
|
|
38208
|
+
]);
|
|
38209
|
+
const warnings = [];
|
|
38210
|
+
if (!ioRes.ok) warnings.push(`io fetch failed: ${ioRes.error}`);
|
|
38211
|
+
if (aiosRes && !aiosRes.ok) warnings.push(`in_flight fetch failed: ${aiosRes.error}`);
|
|
38212
|
+
if (methodRes && !methodRes.ok) warnings.push(`io_method fetch failed: ${methodRes.error}`);
|
|
38213
|
+
return {
|
|
38214
|
+
ok: true,
|
|
38215
|
+
data: {
|
|
38216
|
+
// Echoed so a caller can tell WHY the PG18-only keys below are
|
|
38217
|
+
// absent without a second round-trip to version().
|
|
38218
|
+
server_version_num: serverVersion,
|
|
38219
|
+
byte_accounting: byteAccounting,
|
|
38220
|
+
// Echoed because it changes what an empty `io` array means: with
|
|
38221
|
+
// the filter on, empty means "no recorded I/O anywhere"; with it
|
|
38222
|
+
// off, empty means the view itself returned nothing.
|
|
38223
|
+
include_zero_rows: includeZeroRows,
|
|
38224
|
+
// null, not [], on failure. An empty array is a real and reachable
|
|
38225
|
+
// answer here (a freshly reset cluster), so it must not double as
|
|
38226
|
+
// the error value -- a caller seeing [] would conclude the server
|
|
38227
|
+
// is idle when it actually refused to answer. The _warnings entry
|
|
38228
|
+
// above carries the reason.
|
|
38229
|
+
io: ioRes.ok ? ioRes.data ?? [] : null,
|
|
38230
|
+
// PG18+ keys are spread in, so on PG16/17 they are ABSENT from the
|
|
38231
|
+
// response rather than null or []. That distinction is the whole
|
|
38232
|
+
// point: `in_flight: []` reads as "nothing is stalled", which is a
|
|
38233
|
+
// confident wrong answer on a server that has no way to look. A
|
|
38234
|
+
// missing key forces the caller to notice.
|
|
38235
|
+
...isPg18 ? {
|
|
38236
|
+
// NULL here is legitimate (see current_setting's missing_ok
|
|
38237
|
+
// above) and is also the failure value -- disambiguated by
|
|
38238
|
+
// the _warnings entry rather than by the field.
|
|
38239
|
+
io_method: methodRes?.ok ? methodRes.data?.[0]?.io_method ?? null : null,
|
|
38240
|
+
// Same null-on-failure reasoning as `io`, and it matters more
|
|
38241
|
+
// here: this tool gets called BECAUSE something is stalling,
|
|
38242
|
+
// so a permission denial that renders as "no in-flight I/O"
|
|
38243
|
+
// would actively point the investigation the wrong way.
|
|
38244
|
+
in_flight: aiosRes?.ok ? aiosRes.data ?? [] : null
|
|
38245
|
+
} : {},
|
|
38246
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
38247
|
+
}
|
|
38248
|
+
};
|
|
38249
|
+
});
|
|
38250
|
+
}
|
|
38251
|
+
}
|
|
38252
|
+
];
|
|
38253
|
+
|
|
37139
38254
|
// src/tools/query.ts
|
|
37140
38255
|
var queryTools = [
|
|
37141
38256
|
{
|
|
@@ -37267,7 +38382,7 @@ var schemaTools = [
|
|
|
37267
38382
|
},
|
|
37268
38383
|
{
|
|
37269
38384
|
name: "pg_describe_table",
|
|
37270
|
-
description: "Describe a relation: kind (table / view / materialized_view / partitioned_table / foreign_table), columns (name, type, nullable, default), primary key, foreign keys (outgoing), `referenced_by` (other tables whose FKs point at this one), `constraints` (CHECK / UNIQUE non-PK / EXCLUDE), indexes, and partition info (`partition_of` parent, `partitions` children). Works on views and materialized views too -- PK/FK/constraint/index lists will simply be empty for a plain view. Use `kind` to disambiguate before assuming you can write to the relation.",
|
|
38385
|
+
description: "Describe a relation: kind (table / view / materialized_view / partitioned_table / foreign_table), columns (name, type, nullable, default, `generated`, `identity`), primary key, foreign keys (outgoing), `referenced_by` (other tables whose FKs point at this one), `constraints` (CHECK / UNIQUE non-PK / EXCLUDE), indexes, and partition info (`partition_of` parent, `partitions` children). Works on views and materialized views too -- PK/FK/constraint/index lists will simply be empty for a plain view. Use `kind` to disambiguate before assuming you can write to the relation. Generated columns (`generated`: 'stored' / 'virtual') and `identity`: 'always' columns are NOT writable -- omit them from INSERT/UPDATE column lists; a generated column's expression is reported as `generation_expression`, never as `default_value`. On PostgreSQL 18+ constraints also report `validated` / `enforced` / `has_period`, and columns report `not_null_validated` -- a NOT VALID not-null constraint means `nullable: false` can still hide NULLs.",
|
|
37271
38386
|
annotations: {
|
|
37272
38387
|
title: "Describe table",
|
|
37273
38388
|
readOnlyHint: true,
|
|
@@ -37281,6 +38396,8 @@ var schemaTools = [
|
|
|
37281
38396
|
}),
|
|
37282
38397
|
handler: async (input) => {
|
|
37283
38398
|
const { schema = "public", table } = input;
|
|
38399
|
+
const serverVersion = await getServerVersionNum();
|
|
38400
|
+
const isPg18 = serverVersion >= PG18;
|
|
37284
38401
|
const kindQuery = `
|
|
37285
38402
|
SELECT
|
|
37286
38403
|
CASE c.relkind
|
|
@@ -37295,12 +38412,21 @@ var schemaTools = [
|
|
|
37295
38412
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
37296
38413
|
WHERE n.nspname = $1 AND c.relname = $2
|
|
37297
38414
|
`;
|
|
38415
|
+
const notNullValidatedCol = isPg18 ? `,
|
|
38416
|
+
(SELECT COALESCE(bool_and(nn.convalidated), true)
|
|
38417
|
+
FROM pg_catalog.pg_constraint nn
|
|
38418
|
+
WHERE nn.contype = 'n'
|
|
38419
|
+
AND nn.conrelid = a.attrelid
|
|
38420
|
+
AND nn.conkey = ARRAY[a.attnum]) AS not_null_validated` : "";
|
|
37298
38421
|
const columnsQuery = `
|
|
37299
38422
|
SELECT
|
|
37300
38423
|
a.attname AS name,
|
|
37301
38424
|
pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
|
|
37302
|
-
NOT a.attnotnull AS nullable,
|
|
37303
|
-
pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS default_value,
|
|
38425
|
+
NOT a.attnotnull AS nullable${notNullValidatedCol},
|
|
38426
|
+
CASE WHEN a.attgenerated = '' THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid) END AS default_value,
|
|
38427
|
+
CASE WHEN a.attgenerated <> '' THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid) END AS generation_expression,
|
|
38428
|
+
CASE a.attgenerated WHEN 's' THEN 'stored' WHEN 'v' THEN 'virtual' END AS generated,
|
|
38429
|
+
CASE a.attidentity WHEN 'a' THEN 'always' WHEN 'd' THEN 'by_default' END AS identity,
|
|
37304
38430
|
a.attnum AS ordinal_position
|
|
37305
38431
|
FROM pg_catalog.pg_attribute a
|
|
37306
38432
|
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
@@ -37324,13 +38450,19 @@ var schemaTools = [
|
|
|
37324
38450
|
AND i.indisprimary
|
|
37325
38451
|
ORDER BY k.ord
|
|
37326
38452
|
`;
|
|
38453
|
+
const constraintMetaCols = isPg18 ? `,
|
|
38454
|
+
con.convalidated AS validated,
|
|
38455
|
+
con.conenforced AS enforced,
|
|
38456
|
+
con.conperiod AS has_period` : `,
|
|
38457
|
+
con.convalidated AS validated`;
|
|
38458
|
+
const constraintMetaGroupBy = isPg18 ? ", con.convalidated, con.conenforced, con.conperiod" : ", con.convalidated";
|
|
37327
38459
|
const foreignKeysQuery = `
|
|
37328
38460
|
SELECT
|
|
37329
38461
|
con.conname AS constraint_name,
|
|
37330
38462
|
array_agg(att.attname::text ORDER BY u.attposition) AS columns,
|
|
37331
38463
|
cl.relname AS foreign_table,
|
|
37332
38464
|
fn.nspname AS foreign_schema,
|
|
37333
|
-
array_agg(fatt.attname::text ORDER BY u.attposition) AS foreign_columns
|
|
38465
|
+
array_agg(fatt.attname::text ORDER BY u.attposition) AS foreign_columns${constraintMetaCols}
|
|
37334
38466
|
FROM pg_catalog.pg_constraint con
|
|
37335
38467
|
JOIN pg_catalog.pg_class c ON c.oid = con.conrelid
|
|
37336
38468
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
@@ -37347,7 +38479,7 @@ var schemaTools = [
|
|
|
37347
38479
|
WHERE n.nspname = $1
|
|
37348
38480
|
AND c.relname = $2
|
|
37349
38481
|
AND con.contype = 'f'
|
|
37350
|
-
GROUP BY con.conname, cl.relname, fn.nspname
|
|
38482
|
+
GROUP BY con.conname, cl.relname, fn.nspname${constraintMetaGroupBy}
|
|
37351
38483
|
ORDER BY con.conname
|
|
37352
38484
|
`;
|
|
37353
38485
|
const indexesQuery = `
|
|
@@ -37373,7 +38505,7 @@ var schemaTools = [
|
|
|
37373
38505
|
WHEN 'x' THEN 'exclude'
|
|
37374
38506
|
ELSE con.contype::text
|
|
37375
38507
|
END AS type,
|
|
37376
|
-
pg_catalog.pg_get_constraintdef(con.oid, true) AS definition
|
|
38508
|
+
pg_catalog.pg_get_constraintdef(con.oid, true) AS definition${constraintMetaCols}
|
|
37377
38509
|
FROM pg_catalog.pg_constraint con
|
|
37378
38510
|
JOIN pg_catalog.pg_class c ON c.oid = con.conrelid
|
|
37379
38511
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
@@ -37388,7 +38520,7 @@ var schemaTools = [
|
|
|
37388
38520
|
srcn.nspname AS schema,
|
|
37389
38521
|
src.relname AS "table",
|
|
37390
38522
|
array_agg(srcatt.attname::text ORDER BY u.attposition) AS columns,
|
|
37391
|
-
array_agg(refatt.attname::text ORDER BY u.attposition) AS referenced_columns
|
|
38523
|
+
array_agg(refatt.attname::text ORDER BY u.attposition) AS referenced_columns${constraintMetaCols}
|
|
37392
38524
|
FROM pg_catalog.pg_constraint con
|
|
37393
38525
|
JOIN pg_catalog.pg_class src ON src.oid = con.conrelid
|
|
37394
38526
|
JOIN pg_catalog.pg_namespace srcn ON srcn.oid = src.relnamespace
|
|
@@ -37405,7 +38537,7 @@ var schemaTools = [
|
|
|
37405
38537
|
WHERE refn.nspname = $1
|
|
37406
38538
|
AND ref.relname = $2
|
|
37407
38539
|
AND con.contype = 'f'
|
|
37408
|
-
GROUP BY con.conname, srcn.nspname, src.relname
|
|
38540
|
+
GROUP BY con.conname, srcn.nspname, src.relname${constraintMetaGroupBy}
|
|
37409
38541
|
ORDER BY srcn.nspname, src.relname, con.conname
|
|
37410
38542
|
`;
|
|
37411
38543
|
const partitionParentQuery = `
|
|
@@ -37457,6 +38589,11 @@ var schemaTools = [
|
|
|
37457
38589
|
}
|
|
37458
38590
|
const kind = kindRes.ok ? kindRes.data?.[0]?.kind ?? "table" : "table";
|
|
37459
38591
|
const warnings = [];
|
|
38592
|
+
if (serverVersion === 0) {
|
|
38593
|
+
warnings.push(
|
|
38594
|
+
"server version unknown; PG18-only fields omitted (constraint `enforced`/`has_period`, column `not_null_validated`)"
|
|
38595
|
+
);
|
|
38596
|
+
}
|
|
37460
38597
|
if (!kindRes.ok) warnings.push(`kind fetch failed, reported as "table": ${kindRes.error}`);
|
|
37461
38598
|
else if ((kindRes.data?.length ?? 0) === 0) warnings.push(`kind unavailable, reported as "table"`);
|
|
37462
38599
|
if (!pk.ok) warnings.push(`primary_key fetch failed: ${pk.error}`);
|
|
@@ -37624,10 +38761,24 @@ var schemaTools = [
|
|
|
37624
38761
|
];
|
|
37625
38762
|
|
|
37626
38763
|
// src/tools/stats.ts
|
|
38764
|
+
var STATS_RESET_SQL = `SELECT
|
|
38765
|
+
stats_reset::text AS stats_reset,
|
|
38766
|
+
EXTRACT(EPOCH FROM (now() - stats_reset))::numeric(14, 2)::float8 AS stats_reset_age_seconds
|
|
38767
|
+
FROM pg_catalog.pg_stat_database
|
|
38768
|
+
WHERE datname = current_database()`;
|
|
38769
|
+
var PG_STAT_STATEMENTS_INFO_MIN_VERSION = "1.9";
|
|
38770
|
+
function hasStatementsInfo(extVersion) {
|
|
38771
|
+
return compareVersions(extVersion, PG_STAT_STATEMENTS_INFO_MIN_VERSION) >= 0;
|
|
38772
|
+
}
|
|
38773
|
+
var STATEMENTS_STATS_RESET_SQL = `SELECT
|
|
38774
|
+
stats_reset::text AS stats_reset,
|
|
38775
|
+
EXTRACT(EPOCH FROM (now() - stats_reset))::numeric(14, 2)::float8 AS stats_reset_age_seconds,
|
|
38776
|
+
dealloc::text AS dealloc
|
|
38777
|
+
FROM pg_stat_statements_info`;
|
|
37627
38778
|
var statsTools = [
|
|
37628
38779
|
{
|
|
37629
38780
|
name: "pg_top_queries",
|
|
37630
|
-
description: "Top N queries by total or mean execution time. Requires the `pg_stat_statements` extension to be installed and enabled (most managed Postgres providers have it on by default). Returns normalized query text (constants replaced with `?`), call count, total/mean/min/max time in ms, rows returned, and cache hit ratio. Use this to find slow queries worth optimizing.
|
|
38781
|
+
description: "Top N queries by total or mean execution time. Requires the `pg_stat_statements` extension to be installed and enabled (most managed Postgres providers have it on by default). Returns `{rows, stats_reset, stats_reset_age_seconds, dealloc}`: each row has normalized query text (constants replaced with `?`), call count, total/mean/min/max time in ms, rows returned, and cache hit ratio. Use this to find slow queries worth optimizing.\n`calls` and `total_time_ms` are cumulative since the last `pg_stat_statements_reset()`, so this ranking only describes the window that started at the top-level `stats_reset` (with `stats_reset_age_seconds` beside it). This is pg_stat_statements' OWN reset clock, read from `pg_stat_statements_info` -- it is independent of the `stats_reset` reported by `pg_seq_scan_tables` / `pg_unused_indexes`, which comes from pg_stat_database, so do not compare the two timestamps or assume one implies the other. `stats_reset: null` means the start of the window is unknown, not that it covers all time.\nREAD `dealloc` BEFORE TRUSTING THE RANKING: it counts how many times entries for the LEAST-EXECUTED statements were evicted because more distinct statements were seen than `pg_stat_statements.max` allows. A non-zero `dealloc` means this ranking is drawn from an INCOMPLETE population -- queries may be missing from these results entirely, and an evicted query's counters restart from zero if it runs again, understating it. The larger `dealloc` is, the more churn, so 'not in the top N' stops being evidence that a query is cheap. Raise `pg_stat_statements.max` to get a complete picture.\nOn pg_stat_statements < 1.9 (before Postgres 14) `pg_stat_statements_info` does not exist, so `stats_reset`, `stats_reset_age_seconds` and `dealloc` are omitted entirely rather than returned as nulls, and a `_warnings` entry says so.\nOn pg_stat_statements >= 1.10 (Postgres 15+), also returns `io_read_time_ms` and `io_write_time_ms` to separate IO-bound from CPU-bound queries (null when track_io_timing = off or the query did no measurable IO -- enable track_io_timing in postgresql.conf to get non-null values). Scoped to the database in DATABASE_URL: pg_stat_statements is cluster-wide, so results are filtered by `dbid` to match every other tool here rather than leaking query text from unrelated databases sharing the cluster.",
|
|
37631
38782
|
annotations: {
|
|
37632
38783
|
title: "Top queries by execution time",
|
|
37633
38784
|
readOnlyHint: true,
|
|
@@ -37663,12 +38814,15 @@ var statsTools = [
|
|
|
37663
38814
|
NULLIF(${hasSharedBlkCols ? "shared_blk_read_time" : "blk_read_time"}, 0)::numeric(18, 2)::float8 AS io_read_time_ms,
|
|
37664
38815
|
NULLIF(${hasSharedBlkCols ? "shared_blk_write_time" : "blk_write_time"}, 0)::numeric(18, 2)::float8 AS io_write_time_ms` : "";
|
|
37665
38816
|
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "pg_stat_statements.calls";
|
|
37666
|
-
|
|
37667
|
-
|
|
37668
|
-
|
|
37669
|
-
|
|
37670
|
-
|
|
37671
|
-
|
|
38817
|
+
const infoSql = hasStatementsInfo(extVersion) ? STATEMENTS_STATS_RESET_SQL : null;
|
|
38818
|
+
return withSharedClient(async (run) => {
|
|
38819
|
+
const [rowsRes, infoRes] = await Promise.all([
|
|
38820
|
+
run(
|
|
38821
|
+
// bigint counters (calls, rows) come back as `.text` for lossless
|
|
38822
|
+
// serialization, matching pg_seq_scan_tables / pg_unused_indexes /
|
|
38823
|
+
// pg_table_bloat. Timing fields stay as float8 because they are
|
|
38824
|
+
// inherently fractional milliseconds.
|
|
38825
|
+
`SELECT
|
|
37672
38826
|
query,
|
|
37673
38827
|
calls::text AS calls,
|
|
37674
38828
|
${totalCol}::numeric(18, 2)::float8 AS total_time_ms,
|
|
@@ -37685,13 +38839,29 @@ var statsTools = [
|
|
|
37685
38839
|
WHERE dbid = (SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database())
|
|
37686
38840
|
ORDER BY ${orderCol} DESC NULLS LAST
|
|
37687
38841
|
LIMIT $1`,
|
|
37688
|
-
|
|
37689
|
-
|
|
38842
|
+
[limit]
|
|
38843
|
+
),
|
|
38844
|
+
infoSql ? run(infoSql) : null
|
|
38845
|
+
]);
|
|
38846
|
+
if (!rowsRes.ok) return { ok: false, error: rowsRes.error };
|
|
38847
|
+
if (!infoRes) {
|
|
38848
|
+
return {
|
|
38849
|
+
ok: true,
|
|
38850
|
+
data: {
|
|
38851
|
+
rows: rowsRes.data ?? [],
|
|
38852
|
+
_warnings: [
|
|
38853
|
+
`pg_stat_statements ${extVersion} predates pg_stat_statements_info (added in ${PG_STAT_STATEMENTS_INFO_MIN_VERSION}), so the reset point and the dealloc eviction count are unavailable: this ranking's window and completeness are unknown`
|
|
38854
|
+
]
|
|
38855
|
+
}
|
|
38856
|
+
};
|
|
38857
|
+
}
|
|
38858
|
+
return { ok: true, data: withStatsReset(rowsRes.data ?? [], infoRes, "pg_stat_statements_info") };
|
|
38859
|
+
});
|
|
37690
38860
|
}
|
|
37691
38861
|
},
|
|
37692
38862
|
{
|
|
37693
38863
|
name: "pg_seq_scan_tables",
|
|
37694
|
-
description: "Tables with high sequential-scan counts relative to index scans - the first place to look for missing-index candidates. Returns seq_scans, idx_scans, live tuples, and the ratio. A high ratio on a large table usually means a query is reading the whole table where an index would suffice. Pair with `pg_top_queries` to find which query is doing it.",
|
|
38864
|
+
description: "Tables with high sequential-scan counts relative to index scans - the first place to look for missing-index candidates. Returns `{rows, stats_reset, stats_reset_age_seconds}`: each row has seq_scans, idx_scans, live tuples, and the ratio. A high ratio on a large table usually means a query is reading the whole table where an index would suffice. Pair with `pg_top_queries` to find which query is doing it.\nThese counters are cumulative since the last statistics reset, so every ratio here is only meaningful relative to the top-level `stats_reset` (and `stats_reset_age_seconds`). A ratio measured over a window that was reset minutes ago describes that window, not the workload; `stats_reset: null` means the start of the window is unknown.\nOn PostgreSQL 16+ each row also carries `last_seq_scan` and `last_idx_scan` timestamps (null = no such scan since the reset), which separate 'scanned hard months ago' from 'being scanned right now' in a way the raw counts cannot.",
|
|
37695
38865
|
annotations: {
|
|
37696
38866
|
title: "Find tables with heavy sequential scans",
|
|
37697
38867
|
readOnlyHint: true,
|
|
@@ -37710,33 +38880,41 @@ var statsTools = [
|
|
|
37710
38880
|
minSize = 1e3,
|
|
37711
38881
|
limit = 20
|
|
37712
38882
|
} = input;
|
|
38883
|
+
const serverVersion = await getServerVersionNum();
|
|
37713
38884
|
const schemaFilter = schema ? "AND schemaname = $3" : "AND schemaname NOT IN ('pg_catalog', 'information_schema') AND schemaname NOT LIKE 'pg_%'";
|
|
37714
38885
|
const params = [minSize, limit];
|
|
37715
38886
|
if (schema) params.push(schema);
|
|
37716
|
-
return
|
|
37717
|
-
|
|
37718
|
-
|
|
37719
|
-
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
|
|
37724
|
-
|
|
37725
|
-
|
|
37726
|
-
|
|
37727
|
-
|
|
37728
|
-
|
|
37729
|
-
|
|
37730
|
-
|
|
37731
|
-
|
|
37732
|
-
|
|
37733
|
-
|
|
37734
|
-
|
|
38887
|
+
return withSharedClient(async (run) => {
|
|
38888
|
+
const [rowsRes, resetRes] = await Promise.all([
|
|
38889
|
+
run(
|
|
38890
|
+
`SELECT
|
|
38891
|
+
schemaname AS schema,
|
|
38892
|
+
relname AS "table",
|
|
38893
|
+
seq_scan::text AS seq_scans,
|
|
38894
|
+
COALESCE(idx_scan, 0)::text AS idx_scans,
|
|
38895
|
+
n_live_tup::text AS live_tuples,
|
|
38896
|
+
seq_tup_read::text AS seq_tup_read,
|
|
38897
|
+
CASE
|
|
38898
|
+
WHEN COALESCE(idx_scan, 0) = 0 THEN NULL
|
|
38899
|
+
ELSE (seq_scan::numeric / idx_scan)::numeric(10, 2)::float8
|
|
38900
|
+
END AS ratio${tableLastScanCols(serverVersion)}
|
|
38901
|
+
FROM pg_catalog.pg_stat_user_tables
|
|
38902
|
+
WHERE n_live_tup >= $1
|
|
38903
|
+
${schemaFilter}
|
|
38904
|
+
ORDER BY seq_scan DESC NULLS LAST
|
|
38905
|
+
LIMIT $2`,
|
|
38906
|
+
params
|
|
38907
|
+
),
|
|
38908
|
+
run(STATS_RESET_SQL)
|
|
38909
|
+
]);
|
|
38910
|
+
if (!rowsRes.ok) return { ok: false, error: rowsRes.error };
|
|
38911
|
+
return { ok: true, data: withStatsReset(rowsRes.data ?? [], resetRes) };
|
|
38912
|
+
});
|
|
37735
38913
|
}
|
|
37736
38914
|
},
|
|
37737
38915
|
{
|
|
37738
38916
|
name: "pg_unused_indexes",
|
|
37739
|
-
description: "Indexes that have never been scanned or have very low usage. Each unused index costs write amplification (every INSERT/UPDATE maintains it) and disk space.
|
|
38917
|
+
description: "Indexes that have never been scanned or have very low usage, largest first. Each unused index costs write amplification (every INSERT/UPDATE maintains it) and disk space, so before adding a new index, check whether the fix is to drop a dead one. Returns `{rows, stats_reset, stats_reset_age_seconds}`.\nREAD THIS BEFORE RECOMMENDING A DROP: `scans` is a counter, not a verdict. It only counts since the last statistics reset, which is why the top-level `stats_reset` and `stats_reset_age_seconds` are part of the answer. If the counters were reset an hour ago, EVERY index looks unused; if `stats_reset` is null, the start of the window is unknown and the counts prove nothing. This list is only trustworthy once the reset age comfortably exceeds the slowest cycle that could use the index - a monthly report, a quarterly close, a yearly job, a failover-only query path.\nPRIMARY KEY and UNIQUE indexes are already excluded from these results: they enforce a constraint and stay load-bearing at zero scans, so they never appear here and their absence is not evidence of anything.\nOn PostgreSQL 16+ each row also carries `last_idx_scan`, the timestamp of the most recent scan (null = never scanned since the reset). 'Not scanned since 2026-02-14' is a far better basis for a decision than a bare count.\nOn PostgreSQL 18+, do not fall back on the old 'the leading column is never filtered, so this index is dead weight' reasoning. Skip scan lets the planner use a multi-column btree whose leading column is unconstrained, so such an index can now be doing real work.",
|
|
37740
38918
|
annotations: {
|
|
37741
38919
|
title: "Find unused indexes",
|
|
37742
38920
|
readOnlyHint: true,
|
|
@@ -37755,31 +38933,74 @@ var statsTools = [
|
|
|
37755
38933
|
maxScans = 10,
|
|
37756
38934
|
limit = 50
|
|
37757
38935
|
} = input;
|
|
38936
|
+
const serverVersion = await getServerVersionNum();
|
|
37758
38937
|
const schemaFilter = schema ? "AND s.schemaname = $3" : "AND s.schemaname NOT IN ('pg_catalog', 'information_schema') AND s.schemaname NOT LIKE 'pg_%'";
|
|
37759
38938
|
const params = [maxScans, limit];
|
|
37760
38939
|
if (schema) params.push(schema);
|
|
37761
|
-
return
|
|
37762
|
-
|
|
37763
|
-
|
|
37764
|
-
|
|
37765
|
-
|
|
37766
|
-
|
|
37767
|
-
|
|
37768
|
-
|
|
37769
|
-
|
|
37770
|
-
|
|
37771
|
-
|
|
37772
|
-
|
|
37773
|
-
|
|
37774
|
-
|
|
37775
|
-
|
|
37776
|
-
|
|
37777
|
-
|
|
37778
|
-
|
|
37779
|
-
|
|
38940
|
+
return withSharedClient(async (run) => {
|
|
38941
|
+
const [rowsRes, resetRes] = await Promise.all([
|
|
38942
|
+
run(
|
|
38943
|
+
`SELECT
|
|
38944
|
+
s.schemaname AS schema,
|
|
38945
|
+
s.relname AS "table",
|
|
38946
|
+
s.indexrelname AS "index",
|
|
38947
|
+
s.idx_scan::text AS scans,
|
|
38948
|
+
pg_size_pretty(pg_relation_size(s.indexrelid)) AS size_pretty,
|
|
38949
|
+
pg_relation_size(s.indexrelid)::text AS size_bytes,
|
|
38950
|
+
pg_catalog.pg_get_indexdef(s.indexrelid) AS definition${indexLastScanCols(serverVersion)}
|
|
38951
|
+
FROM pg_catalog.pg_stat_user_indexes s
|
|
38952
|
+
JOIN pg_catalog.pg_index i ON i.indexrelid = s.indexrelid
|
|
38953
|
+
WHERE s.idx_scan <= $1
|
|
38954
|
+
AND NOT i.indisunique
|
|
38955
|
+
AND NOT i.indisprimary
|
|
38956
|
+
${schemaFilter}
|
|
38957
|
+
ORDER BY pg_relation_size(s.indexrelid) DESC
|
|
38958
|
+
LIMIT $2`,
|
|
38959
|
+
params
|
|
38960
|
+
),
|
|
38961
|
+
run(STATS_RESET_SQL)
|
|
38962
|
+
]);
|
|
38963
|
+
if (!rowsRes.ok) return { ok: false, error: rowsRes.error };
|
|
38964
|
+
return { ok: true, data: withStatsReset(rowsRes.data ?? [], resetRes) };
|
|
38965
|
+
});
|
|
37780
38966
|
}
|
|
37781
38967
|
}
|
|
37782
38968
|
];
|
|
38969
|
+
function withStatsReset(rows, resetRes, sourceView = "pg_stat_database") {
|
|
38970
|
+
if (!resetRes.ok) {
|
|
38971
|
+
return {
|
|
38972
|
+
rows,
|
|
38973
|
+
stats_reset: null,
|
|
38974
|
+
stats_reset_age_seconds: null,
|
|
38975
|
+
_warnings: [`stats_reset lookup failed, so counter age is unknown: ${resetRes.error}`]
|
|
38976
|
+
};
|
|
38977
|
+
}
|
|
38978
|
+
const row = resetRes.data?.[0];
|
|
38979
|
+
if (!row) {
|
|
38980
|
+
return {
|
|
38981
|
+
rows,
|
|
38982
|
+
stats_reset: null,
|
|
38983
|
+
stats_reset_age_seconds: null,
|
|
38984
|
+
_warnings: [`${sourceView} returned no row, so counter age is unknown`]
|
|
38985
|
+
};
|
|
38986
|
+
}
|
|
38987
|
+
const out = {
|
|
38988
|
+
rows,
|
|
38989
|
+
stats_reset: row.stats_reset,
|
|
38990
|
+
stats_reset_age_seconds: row.stats_reset_age_seconds
|
|
38991
|
+
};
|
|
38992
|
+
if (row.dealloc !== void 0) out.dealloc = row.dealloc;
|
|
38993
|
+
return out;
|
|
38994
|
+
}
|
|
38995
|
+
function indexLastScanCols(serverVersionNum2) {
|
|
38996
|
+
return serverVersionNum2 >= PG16 ? `,
|
|
38997
|
+
s.last_idx_scan::text AS last_idx_scan` : "";
|
|
38998
|
+
}
|
|
38999
|
+
function tableLastScanCols(serverVersionNum2) {
|
|
39000
|
+
return serverVersionNum2 >= PG16 ? `,
|
|
39001
|
+
last_seq_scan::text AS last_seq_scan,
|
|
39002
|
+
last_idx_scan::text AS last_idx_scan` : "";
|
|
39003
|
+
}
|
|
37783
39004
|
function compareVersions(a, b) {
|
|
37784
39005
|
const parse3 = (v) => v.split(".").map((seg) => {
|
|
37785
39006
|
const m = seg.match(/^\d+/);
|
|
@@ -37796,7 +39017,7 @@ function compareVersions(a, b) {
|
|
|
37796
39017
|
}
|
|
37797
39018
|
|
|
37798
39019
|
// src/index.ts
|
|
37799
|
-
var version2 = true ? "0.
|
|
39020
|
+
var version2 = true ? "0.11.1" : await readPackageVersion();
|
|
37800
39021
|
var subcommand = process.argv[2];
|
|
37801
39022
|
if (subcommand === "version" || subcommand === "--version") {
|
|
37802
39023
|
console.log(version2);
|
|
@@ -37815,17 +39036,33 @@ Usage:
|
|
|
37815
39036
|
writeSync(2, message);
|
|
37816
39037
|
process.exit(1);
|
|
37817
39038
|
}
|
|
37818
|
-
var allTools = [
|
|
39039
|
+
var allTools = [
|
|
39040
|
+
...queryTools,
|
|
39041
|
+
...schemaTools,
|
|
39042
|
+
...explainTools,
|
|
39043
|
+
...healthTools,
|
|
39044
|
+
...statsTools,
|
|
39045
|
+
...ioTools,
|
|
39046
|
+
...adminTools
|
|
39047
|
+
];
|
|
37819
39048
|
var server = new McpServer({
|
|
37820
39049
|
name: "@yawlabs/postgres-mcp",
|
|
37821
39050
|
version: version2
|
|
37822
39051
|
});
|
|
37823
39052
|
for (const tool of allTools) {
|
|
37824
|
-
server.
|
|
39053
|
+
server.registerTool(
|
|
37825
39054
|
tool.name,
|
|
37826
|
-
|
|
37827
|
-
|
|
37828
|
-
|
|
39055
|
+
{
|
|
39056
|
+
// `title` at the top level is where the current spec puts a tool's
|
|
39057
|
+
// display name; `annotations.title` is the older location. Emit BOTH --
|
|
39058
|
+
// dropping the annotations copy would regress hosts that only read it,
|
|
39059
|
+
// and omitting the top-level one leaves newer hosts showing the raw
|
|
39060
|
+
// `pg_*` name.
|
|
39061
|
+
title: tool.annotations.title,
|
|
39062
|
+
description: tool.description,
|
|
39063
|
+
inputSchema: tool.inputSchema.shape,
|
|
39064
|
+
annotations: tool.annotations
|
|
39065
|
+
},
|
|
37829
39066
|
wrapToolHandler(tool.handler)
|
|
37830
39067
|
);
|
|
37831
39068
|
}
|