@yawlabs/postgres-mcp 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
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 consumeIsZone(buffer) {
3144
- buffer.length = 0;
3145
- return true;
3146
- }
3147
- function consumeHextets(buffer, address, output) {
3148
- if (buffer.length) {
3149
- const hex3 = stringArrayToHexStripped(buffer);
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
- buffer.length = 0;
3172
+ return false;
3157
3173
  }
3158
3174
  return true;
3159
3175
  }
3160
- function getIPV6(input) {
3161
- let tokenCount = 0;
3162
- const output = { error: false, address: "", zone: "" };
3163
- const address = [];
3164
- const buffer = [];
3165
- let endipv6Encountered = false;
3166
- let endIpv6 = false;
3167
- let consume = consumeHextets;
3168
- for (let i = 0; i < input.length; i++) {
3169
- const cursor = input[i];
3170
- if (cursor === "[" || cursor === "]") {
3171
- continue;
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
- buffer.push(cursor);
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 (buffer.length) {
3200
- if (consume === consumeIsZone) {
3201
- output.zone = buffer.join("");
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
- output.address = address.join("");
3209
- return output;
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
- if (findToken(host, ":") < 2) {
3213
- return { host, isIPV6: false };
3214
- }
3215
- const ipv63 = getIPV6(host);
3216
- if (!ipv63.error) {
3217
- let newHost = ipv63.address;
3218
- let escapedHost = ipv63.address;
3219
- if (ipv63.zone) {
3220
- newHost += "%" + ipv63.zone;
3221
- escapedHost += "%25" + ipv63.zone;
3222
- }
3223
- return { host: newHost, isIPV6: true, escapedHost };
3224
- } else {
3225
- return { host, isIPV6: false };
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
- if (input[i] === "%" && i + 2 < input.length) {
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(input[i])) {
3360
- output += input[i];
3392
+ if (isPathCharacter(ch)) {
3393
+ output += ch;
3361
3394
  } else {
3362
- output += escape(input[i]);
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 = unescape(component.host);
3564
+ let host = component.host;
3390
3565
  if (!isIPv4(host)) {
3391
- const ipV6res = normalizeIPv6(host);
3392
- if (ipV6res.isIPV6 === true) {
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 = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
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 [path, query] = wsComponent.resourceName.split("?");
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 = 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, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
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 resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
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.toLowerCase() === normalizedB.toLowerCase();
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 = escapePreservingEscapes(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
- parsed.host = ipv6result.host.toLowerCase();
3826
- isIP = ipv6result.isIPV6;
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
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
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.scheme !== void 0) {
3856
- parsed.scheme = unescape(parsed.scheme);
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
- try {
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 === "string") {
3896
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3897
- return malformedAuthorityOrPort ? void 0 : normalized;
4189
+ if (typeof uri !== "string" && typeof uri !== "object") {
4190
+ return void 0;
3898
4191
  }
3899
- if (typeof uri === "object") {
3900
- return serialize(uri, opts);
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 util2 = __require("util");
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 = result + ",";
7910
- }
7911
- if (val[i] === null || typeof val[i] === "undefined") {
7912
- result = result + "NULL";
7913
- } else if (Array.isArray(val[i])) {
7914
- result = result + arrayString(val[i]);
7915
- } else if (ArrayBuffer.isView(val[i])) {
7916
- let item = val[i];
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
- const buf = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
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(val[i]));
8221
+ result += escapeElement(prepareValue(item));
7928
8222
  }
7929
8223
  }
7930
- result = 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
- const buf = Buffer.from(val.buffer, val.byteOffset, val.byteLength);
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-legacy.js
8053
- var require_utils_legacy = __commonJS({
8054
- "node_modules/pg/lib/crypto/utils-legacy.js"(exports, module) {
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 startSession(mechanisms, stream) {
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).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse);
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.addInt32(Buffer.byteLength(mappedVal));
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
- writeValues(values, config2.valueMapper);
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++] !== 0) {
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.int32();
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 = exports.parse = void 0;
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 { getStream, getSecureStream } = require_stream();
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
- const options = {
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
- attachListeners(stream) {
10102
- parse3(stream, (msg) => {
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
- con.requestSsl();
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(msg.mechanisms, this.enableChannelBinding && this.connection.stream);
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
- let readTimeout;
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
- } else if (typeof config2.submit === "function") {
10999
- readTimeout = config2.query_timeout || this.connectionParameters.query_timeout;
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 (this._getActiveQuery() || !this._queryable) {
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", this.end.bind(this, cb));
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
- this.native.end(function() {
11868
- self._connected = false;
11869
- self._errorAllQueries(new Error("Connection terminated"));
11870
- process.nextTick(() => {
11871
- self.emit("end");
11872
- if (cb) cb();
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
- let methodValue;
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
- this._readBuffer.append(chunk);
36013
- this.processReadBuffer();
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 three categories of findings:\n- sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class.\n- tables_without_primary_key: user tables (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.\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.",
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 indexAccessMethod = external_exports.enum(["btree", "hash", "gin", "gist", "brin", "spgist"]);
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
- const flags = [];
37009
- if (analyze) flags.push("ANALYZE");
37010
- if (format === "json") flags.push("FORMAT JSON");
37011
- const explainSql = flags.length > 0 ? `EXPLAIN (${flags.join(", ")}) ${sql}` : `EXPLAIN ${sql}`;
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 count, active queries, and table count. Useful as a connection sanity check and to spot runaway queries.",
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
- FROM pg_stat_activity
37085
- WHERE datname = current_database()`
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. On 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.",
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
- return runInternal(
37667
- // bigint counters (calls, rows) come back as `.text` for lossless
37668
- // serialization, matching pg_seq_scan_tables / pg_unused_indexes /
37669
- // pg_table_bloat. Timing fields stay as float8 because they are
37670
- // inherently fractional milliseconds.
37671
- `SELECT
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
- [limit]
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 runInternal(
37717
- `SELECT
37718
- schemaname AS schema,
37719
- relname AS "table",
37720
- seq_scan::text AS seq_scans,
37721
- COALESCE(idx_scan, 0)::text AS idx_scans,
37722
- n_live_tup::text AS live_tuples,
37723
- seq_tup_read::text AS seq_tup_read,
37724
- CASE
37725
- WHEN COALESCE(idx_scan, 0) = 0 THEN NULL
37726
- ELSE (seq_scan::numeric / idx_scan)::numeric(10, 2)::float8
37727
- END AS ratio
37728
- FROM pg_catalog.pg_stat_user_tables
37729
- WHERE n_live_tup >= $1
37730
- ${schemaFilter}
37731
- ORDER BY seq_scan DESC NULLS LAST
37732
- LIMIT $2`,
37733
- params
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. Excludes primary keys and unique constraints (which are load-bearing even with zero scans). Use this before adding new indexes - sometimes the fix is to drop a dead one.",
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 runInternal(
37762
- `SELECT
37763
- s.schemaname AS schema,
37764
- s.relname AS "table",
37765
- s.indexrelname AS "index",
37766
- s.idx_scan::text AS scans,
37767
- pg_size_pretty(pg_relation_size(s.indexrelid)) AS size_pretty,
37768
- pg_relation_size(s.indexrelid)::text AS size_bytes,
37769
- pg_catalog.pg_get_indexdef(s.indexrelid) AS definition
37770
- FROM pg_catalog.pg_stat_user_indexes s
37771
- JOIN pg_catalog.pg_index i ON i.indexrelid = s.indexrelid
37772
- WHERE s.idx_scan <= $1
37773
- AND NOT i.indisunique
37774
- AND NOT i.indisprimary
37775
- ${schemaFilter}
37776
- ORDER BY pg_relation_size(s.indexrelid) DESC
37777
- LIMIT $2`,
37778
- params
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.10.0" : await readPackageVersion();
39020
+ var version2 = true ? "0.11.0" : 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 = [...queryTools, ...schemaTools, ...explainTools, ...healthTools, ...statsTools, ...adminTools];
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.tool(
39053
+ server.registerTool(
37825
39054
  tool.name,
37826
- tool.description,
37827
- tool.inputSchema.shape,
37828
- tool.annotations,
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
  }